Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions docs/source/user/use-cases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,41 @@ If not testing, obtain a valid STIR/SHAKEN certificate and place them in the /et

11. Click Save

The STIR/SHAKEN page should look like this:
Configure Dynamic SIP Credentials (SUBSCRIPTION REQUIRED)
---------------------------------------------------------

.. image:: images/stir_shaken_settings.png
:align: center
dSIPRouter enables an organization to provide users and systems with SIP credentials that will expire after a time-to-live (ttl). The goal is to minimize the attack surface and user error that would cause SIP credentials to become compromised. The steps to configure is below:

1. Login to dSIPRouter
2. Purchase a core subscription license from the `dSIPRouter Marketplace <https://dopensource.com/product-category/dsiprouter/>`_
3. Click System Settings -> License Manager
4. Add the license to the system
5. Get or set the API Token. You can set the API token using the command below:

.. code-block:: bash

DSIP_HOSTNAME=<your ip or hostname>
DSIP_TOKEN=<set your token>
dsiprouter setcredentials -ac $DSIP_TOKEN

6. Invoke the Lease API, which can be found in the API section of the `dSIPRouter Postman <https://www.postman.com/dopensource/workspace/dsiprouter/collection/4319695-9c09dea3-0b4b-4a20-a615-fb8fc16811af>`_. There are two types of SIP Credentials supported, user/pass credentials and IP based.

User/Pass Credential

.. code-block:: bash

curl -k -H "Authorization: Bearer $DSIP_TOKEN" -H "Content-Type: application/json" -X GET "https://$DSIP_HOSTNAME:5000/api/v1/lease/endpoint?ttl=15&email=mack@dsiprouter.org"

IP Based Credential

.. code-block:: bash

curl -k -H "Authorization: Bearer $DSIP_TOKEN" -H "Content-Type: application/json" -X GET "https://$DSIP_HOSTNAME:5000/api/v1/lease/endpoint?email=mack@goflyball.com&ttl=15m&type=ip&auth_ip=172.145.24.2"

7. You can revoke the lease id that was returned when the lease was created:

.. code-block:: bash

curl -k -H "Authorization: Bearer $DSIP_TOKEN" -H "Content-Type: application/json" -X DELETE "https://$DSIP_HOSTNAME:5000/api/v1/lease/endpoint/<<lease id>/revoke"

8. (Optional) You can set the SIP Domain used for generating the User/Pass credentials by changing the DEFAULT_AUTH_DOMAIN parameter in /etc/dsiprouter/gui/settings.py
4 changes: 2 additions & 2 deletions gui/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,12 @@ class dSIPLeases(object):
maintains a list of active leases based on seconds
"""

def __init__(self, gwid, sid, ttl):
def __init__(self, gwid, sid, ttl, addrid=None):
self.gwid = gwid
self.sid = sid
t = datetime.now() + timedelta(seconds=ttl)
self.expiration = t.strftime('%Y-%m-%d %H:%M:%S')

self.addrid = addrid
pass


Expand Down
1 change: 1 addition & 0 deletions gui/modules/api/api.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE `dsip_endpoint_lease` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`gwid` int(10) unsigned NOT NULL,
`sid` int(10) unsigned NOT NULL,
`addrid` int(10) unsigned NULL,
`expiration` datetime NOT NULL,
PRIMARY KEY (`id`)
);
Expand Down
10 changes: 9 additions & 1 deletion gui/modules/api/api_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from werkzeug import exceptions as http_exceptions
from shared import debugException, StatusCodes
from util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict
from util.security import APIToken
from util.security import APIToken, validSlackSignature
from modules.api.licensemanager.functions import getLicenseStatus
import settings

Expand Down Expand Up @@ -110,6 +110,14 @@ def wrapper(*args, **kwargs):
msg='Unauthorized - Core Subscription Required. Purchase from https://dopensource.com/product/dsiprouter-core/',
status_code=StatusCodes.HTTP_UNAUTHORIZED
)
# Check if request is from Slack and the endpoint is the leasing endpoint
if request.headers.get('X-Slack-Signature'):
#flask_rule[:17] == "/api/v1/lease/endpoint":
if validSlackSignature(settings.SLACK_SIGNING_SECRET,request):
# checks succeeded allow the request
return func(*args, **kwargs)
else:
return jsonify(msg='Unauthorized: Slack signing key is missing or invalid')
# Check if token is valid
if not apiToken.isValid():
return createApiResponse(
Expand Down
129 changes: 101 additions & 28 deletions gui/modules/api/api_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict
from modules.api.api_functions import createApiResponse, showApiError, api_security
from modules.api.kamailio.functions import reloadKamailio
from util.networking import getExternalIP, hostToIP, safeUriToHost, safeStripPort
from util.networking import getExternalIP, hostToIP, safeUriToHost, safeStripPort, isValidIP
from util.notifications import sendEmail
from util.security import AES_CTR, urandomChars, KeyCertPair
from util.file_handling import change_owner
Expand Down Expand Up @@ -130,7 +130,7 @@ def handleReloadDsiprouter():
# it should be renamed and changed to use POST method
# TODO: the last lease id/username generated must be tracked (just query DB)
# and used to determine next lease id, otherwise conflicts may occur
@api.route("/api/v1/lease/endpoint", methods=['GET'])
@api.route("/api/v1/lease/endpoint", methods=['GET','POST'])
@api_security
def getEndpointLease():
db = DummySession()
Expand All @@ -145,13 +145,62 @@ def getEndpointLease():

db = startSession()

email = request.args.get('email')
if not email:
raise Exception("email parameter is missing")

ttl = request.args.get('ttl', None)
if ttl is None:
raise http_exceptions.BadRequest("time to live (ttl) parameter is missing")
# Grab the form parameters from Slack request

# Set up some defaults
ttl = "5m"
type = "userpwd"
auth_ip = None

if request.headers.get('X-Slack-Signature'):
text = request.form.get('text', None)
if text is not None and len(text) > 0:
print("**** {}".format(text))
# Split text up
x=text.split(" ")
for req in x:
if "m" in req:
ttl = req
if "userpwd" == req or "ip" == req:
type = req
if "/" in req:
ip,subnet=req.split("/")
if isValidIP(ip):
auth_ip = req

if type == "ip" and auth_ip is None:
raise http_exceptions.BadRequest("auth_ip must be provided if the type is ip")
elif type == "userpwd" and auth_ip is not None:
raise http_exceptions.BadRequest("auth_ip is not valid when type is userpwd")

#Grab email address
email = request.form.get('email', None)
#Set content type equal to application/json
request.ContentType = 'application/json'

# Grab request parameters from everywhere else
else:
ttl = request.args.get('ttl')
if ttl is None:
raise http_exceptions.BadRequest("time to live (ttl) parameter is missing")

email = request.args.get('email')
if not email:
raise Exception("email parameter is missing")

type = request.args.get('type', None)
if type is None:
type = "userpwd"
elif type != "userpwd" and type != "ip":
raise http_exceptions.BadRequest("type can only have a value of ip or userpwd")

auth_ip = request.args.get('auth_ip', None)
if type == "ip" and auth_ip is None:
raise http_exceptions.BadRequest("auth_ip must be provided if the type is ip")
elif type == "userpwd" and auth_ip is not None:
raise http_exceptions.BadRequest("auth_ip is not valid when type is userpwd")


# Convert TTL to Seconds
r = re.compile('\d*m|M')
Expand All @@ -161,33 +210,57 @@ def getEndpointLease():
# Generate some values
rand_num = random.randint(1, 200)
name = "lease" + str(rand_num)
auth_username = name
auth_password = urandomChars(DEF_PASSWORD_LEN)
auth_domain = settings.DEFAULT_AUTH_DOMAIN

if type == "userpwd":
auth_username = name
auth_password = urandomChars(DEF_PASSWORD_LEN)

# Set some defaults
host_addr = ''
strip = 0
prefix = ''

# Add the Gateways table
Gateway = Gateways(name, host_addr, strip, prefix, settings.FLT_PBX)
db.add(Gateway)
db.flush()

# Set some defaults
host_addr = ''
strip = 0
prefix = ''
# Add the Subscribers table
Subscriber = Subscribers(auth_username, auth_password, auth_domain, Gateway.gwid, email)
db.add(Subscriber)
db.flush()

# Add to the Leases table
Lease = dSIPLeases(Gateway.gwid, Subscriber.id, int(ttl))

if type == "ip":
# Check for Subnet
if "/" in auth_ip:
ip,subnet = auth_ip.split("/",1)
else:
ip = auth_ip
# Define a default subnet address of 32
subnet = 32
Addr = Address(name, ip, subnet, settings.FLT_PBX, gwgroup=2200)
db.add(Addr)
db.flush()

# Add the Gateways table
Gateway = Gateways(name, host_addr, strip, prefix, settings.FLT_PBX)
db.add(Gateway)
db.flush()
# Add to the Leases table
Lease = dSIPLeases(gwid=0, sid=0, ttl=int(ttl), addrid=Addr.id)

# Add the Subscribers table
Subscriber = Subscribers(auth_username, auth_password, auth_domain, Gateway.gwid, email)
db.add(Subscriber)
db.flush()


# Add to the Leases table
Lease = dSIPLeases(Gateway.gwid, Subscriber.id, int(ttl))
db.add(Lease)
db.flush()

lease_data['leaseid'] = Lease.id
lease_data['username'] = auth_username
lease_data['password'] = auth_password
if type == "userpwd":
lease_data['username'] = auth_username
lease_data['password'] = auth_password
elif type == "ip":
lease_data['allowed_ip'] = auth_ip

lease_data['domain'] = auth_domain
lease_data['ttl'] = ttl

Expand All @@ -198,11 +271,11 @@ def getEndpointLease():
#if not addTaggedCronjob("lease_management", "* * * * *", cron_cmd):
# raise Exception('Crontab entry could not be created')

getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True
getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = False
return createApiResponse(
msg='Lease created',
data=[lease_data],
kamreload=True,
kamreload=False,
)

except Exception as ex:
Expand Down
4 changes: 4 additions & 0 deletions gui/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,8 @@
# example for ldap module:
# AUTH_MODULES = {"ldap": {"LDAP_HOST":"ldap://ldap.dopensource.com", "USER_SEARCH_BASE":"ou=People,dc=dopensource,dc=com", "GROUP_SEARCH_BASE":"dc=dopensource,dc=com", "GROUP_MEMBER_ATTRIBUTE":"memberUid", "REQUIRED_GROUP":"support", "USER_ATTRIBUTE":"uid"}}
AUTH_MODULES = {}

# slack settings
SLACK_SIGNING_SECRET = ''
############### End Local-Only Settings ##################

32 changes: 31 additions & 1 deletion gui/util/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
if sys.path[0] != '/etc/dsiprouter/gui':
sys.path.insert(0, '/etc/dsiprouter/gui')

import os, hashlib, binascii, string, ssl, OpenSSL, secrets, re
import os, hashlib, binascii, string, ssl, OpenSSL, secrets, re, hmac
import urllib.parse
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from shared import updateConfig, StatusCodes
Expand Down Expand Up @@ -482,3 +483,32 @@ def dumpPkey(self, encoding=OpenSSL.crypto.FILETYPE_PEM):

def dumpCerts(self, encoding=OpenSSL.crypto.FILETYPE_PEM):
return b'\n'.join([OpenSSL.crypto.dump_certificate(encoding, cert) for cert in self.certs])

def validSlackSignature(slack_signing_secret,request):

timestamp = request.headers['X-Slack-Request-Timestamp']
slack_payload = request.form
dict_slack = slack_payload.to_dict()

payload= "&".join(['='.join([key, urllib.parse.quote(val, safe='')]) for key, val in dict_slack.items()])

### compose the message:
sig_basestring = 'v0:' + timestamp + ':' + payload

sig_basestring = sig_basestring.encode('utf-8')

## secret
signing_secret = slack_signing_secret.encode('utf-8') # I had an env variable declared with slack_signing_secret

my_signature = 'v0=' + hmac.new(
signing_secret,
sig_basestring,
hashlib.sha256
).hexdigest()

slack_signature = request.headers['x-slack-signature']
if hmac.compare_digest(my_signature, slack_signature):
return True
else:
return False