Skip to content
Open
116 changes: 116 additions & 0 deletions bin/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# constants from draft-ietf-ace-oauth-authz-13

# Figure 12 draft-ietf-ace-oauth-authz-13: CBOR mappings used in token requests
ACE_PARAMETERS_LABELS_AUD = 3 # text string
ACE_PARAMETERS_LABELS_CLIENT_ID = 8 # text string
ACE_PARAMETERS_LABELS_CLIENT_SECRET = 9 # byte string
ACE_PARAMETERS_LABELS_RESPONSE_TYPE = 10 # text string
ACE_PARAMETERS_LABELS_REDIRECT_URI = 11 # text string
ACE_PARAMETERS_LABELS_SCOPE = 12 # text or byte string
ACE_PARAMETERS_LABELS_STATE = 13 # text string
ACE_PARAMETERS_LABELS_CODE = 14 # byte string
ACE_PARAMETERS_LABELS_ERROR = 15 # unsigned integer
ACE_PARAMETERS_LABELS_ERROR_DESCRIPTION = 16 # text string
ACE_PARAMETERS_LABELS_ERROR_URI = 17 # text string
ACE_PARAMETERS_LABELS_GRANT_TYPE = 18 # unsigned integer
ACE_PARAMETERS_LABELS_ACCESS_TOKEN = 19 # byte string
ACE_PARAMETERS_LABELS_TOKEN_TYPE = 20 # unsigned integer
ACE_PARAMETERS_LABELS_EXPIRES_IN = 21 # unsigned integer
ACE_PARAMETERS_LABELS_USERNAME = 22 # text string
ACE_PARAMETERS_LABELS_PASSWORD = 23 # text string
ACE_PARAMETERS_LABELS_REFRESH_TOKEN = 24 # byte string
ACE_PARAMETERS_LABELS_CNF = 25 # map
ACE_PARAMETERS_LABELS_PROFILE = 26 # unsigned integer
ACE_PARAMETERS_LABELS_RS_CNF = 31 # map

# Figure 11 from draft-ietf-ace-oauth-authz-13: CBOR abbreviations for common grant types
ACE_CBOR_ABBREVIATIONS_PASSWORD = 0
ACE_CBOR_ABBREVIATIONS_AUTHORIZATION_CODE = 1
ACE_CBOR_ABBREVIATIONS_CLIENT_CREDENTIALS = 2
ACE_CBOR_ABBREVIATIONS_REFRESH_TOKEN = 3

ACE_ACCESS_TOKEN_TYPE_BEARER = 1
ACE_ACCESS_TOKEN_TYPE_POP = 2

# from https://tools.ietf.org/html/draft-ietf-ace-cwt-proof-of-possession-03#section-3.1
ACE_CWT_CNF_COSE_KEY = 1
ACE_CWT_CNF_ENCRYPTED_COSE_KEY = 2
ACE_CWT_CNF_KID = 3

# Figure 2 from draft-ietf-ace-oauth-authz-13:
ACE_AS_INFO_LABEL_AS = 0
ACE_AS_INFO_LABEL_NONCE = 5

# values from RFC8152

# COSE key labels
COSE_KEY_LABEL_KTY = 1
COSE_KEY_LABEL_KID = 2
COSE_KEY_LABEL_ALG = 3
COSE_KEY_LABEL_KEYOPS = 4
COSE_KEY_LABEL_BASEIV = 5
COSE_KEY_LABEL_K = -1
COSE_KEY_LABEL_CLIENT_ID = 6 # value TBD by IANA, registered in draft-ietf-ace-oscore-profile-02
COSE_KEY_LABEL_SERVER_ID = 7 # value TBD by IANA, registered in draft-ietf-ace-oscore-profile-02
COSE_KEY_LABEL_KDF = 8 # value TBD by IANA, registered in draft-ietf-ace-oscore-profile-02
COSE_KEY_LABEL_SLT = 9 # value TBD by IANA, registered in draft-ietf-ace-oscore-profile-02
COSE_KEY_LABEL_ALL = [
COSE_KEY_LABEL_KTY,
COSE_KEY_LABEL_KID,
COSE_KEY_LABEL_ALG,
COSE_KEY_LABEL_KEYOPS,
COSE_KEY_LABEL_BASEIV,
COSE_KEY_LABEL_K,
COSE_KEY_LABEL_CLIENT_ID,
COSE_KEY_LABEL_SERVER_ID,
COSE_KEY_LABEL_KDF,
COSE_KEY_LABEL_SLT,
]

# COSE key values
COSE_KEY_VALUE_OKP = 1
COSE_KEY_VALUE_EC2 = 2
COSE_KEY_VALUE_SYMMETRIC = 4
COSE_KEY_VALUE_ALL = [
COSE_KEY_VALUE_OKP,
COSE_KEY_VALUE_EC2,
COSE_KEY_VALUE_SYMMETRIC,
]

COSE_ALG_AES_CCM_16_64_128 = 10
COSE_ALG_AES_CCM_16_64_256 = 11
COSE_ALG_AES_CCM_64_64_128 = 12
COSE_ALG_AES_CCM_64_64_256 = 13
COSE_ALG_AES_CCM_16_128_128 = 30
COSE_ALG_AES_CCM_16_128_256 = 31
COSE_ALG_AES_CCM_64_128_128 = 32
COSE_ALG_AES_CCM_64_128_256 = 33

COSE_ALG_AES_CCM_ALL = [
COSE_ALG_AES_CCM_16_64_128,
COSE_ALG_AES_CCM_16_64_256,
COSE_ALG_AES_CCM_64_64_128,
COSE_ALG_AES_CCM_64_64_256,
COSE_ALG_AES_CCM_16_128_128,
COSE_ALG_AES_CCM_16_128_256,
COSE_ALG_AES_CCM_64_128_128,
COSE_ALG_AES_CCM_64_128_256,
]

COSE_COMMON_HEADER_PARAMETERS_ALG = 1
COSE_COMMON_HEADER_PARAMETERS_CRIT = 2
COSE_COMMON_HEADER_PARAMETERS_CONTENT_TYPE = 3
COSE_COMMON_HEADER_PARAMETERS_KID = 4
COSE_COMMON_HEADER_PARAMETERS_IV = 5
COSE_COMMON_HEADER_PARAMETERS_PIV = 6
COSE_COMMON_HEADER_PARAMETERS_COUNTER_SIGNATURE = 7

COSE_COMMON_HEADER_PARAMETERS_ALL = [
COSE_COMMON_HEADER_PARAMETERS_ALG,
COSE_COMMON_HEADER_PARAMETERS_CRIT,
COSE_COMMON_HEADER_PARAMETERS_CONTENT_TYPE,
COSE_COMMON_HEADER_PARAMETERS_KID,
COSE_COMMON_HEADER_PARAMETERS_IV,
COSE_COMMON_HEADER_PARAMETERS_PIV,
COSE_COMMON_HEADER_PARAMETERS_COUNTER_SIGNATURE,
]
183 changes: 183 additions & 0 deletions bin/test_ace_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import os
import sys
here = sys.path[0]
sys.path.insert(0, os.path.join(here,'..'))

import time
import binascii
import cbor

from coap import coap
from coap import coapOption as o
from coap import coapObjectSecurity as oscoap
from coap import coapDefines as d
from coap import coapUtils as u
from coap import coapException as e

import constants

import logging_setup

RS_IP = 'bbbb::1415:92cc:0:2'
SCOPE = 'resource1'
AUTHZ_INFO = 'authz-info'

# open
c = coap.coap(udpPort=5000)

context = oscoap.SecurityContext(masterSecret=binascii.unhexlify('000102030405060708090A0B0C0D0E0F'),
senderID=binascii.unhexlify('636c69656e74'),
recipientID='JRC',
aeadAlgorithm=oscoap.AES_CCM_16_64_128())

objectSecurity = o.ObjectSecurity(context=context)

contentFormat = o.ContentFormat(cformat=[d.FORMAT_CBOR])

def abort(c):
c.close()
time.sleep(0.500)
raw_input("Done. Press enter to close.")
sys.exit()

def ask_user(question):
check = str(raw_input("{0} ? (y/n): ".format(question))).lower().strip()
try:
if check[0] == 'y':
return True
elif check[0] == 'n':
return False
else:
print('Invalid Input')
return ask_user(question)
except Exception as error:
print("Please enter valid inputs")
print(error)
return ask_user(question)

try:
# Step 0. Request resource without OSCORE
(respCode, respOptions, respPayload) = c.GET('coap://[{0}]/{1}'.format(RS_IP, SCOPE),
confirmable=True,
options=[],
)

print '===== GET to coap://[{0}]/{1} returned ====='.format(RS_IP, SCOPE)
print binascii.hexlify(cbor.dumps(respPayload))
print '====='

except e.coapRcUnauthorized as err:
as_info = cbor.loads(err.reason)
print '====='
print "Resource Server responded with 4.01 Unauthorized."
print "AS information object:"
print as_info
print '====='
if not ask_user("Shall we request the access token from the AS?"):
abort(c)
print '====='

as_uri = str(as_info[constants.ACE_AS_INFO_LABEL_AS])
print as_uri

# Step 1: Request authorization from the AS to access "resource1"
request_payload = {}
request_payload[constants.ACE_PARAMETERS_LABELS_GRANT_TYPE] = constants.ACE_CBOR_ABBREVIATIONS_CLIENT_CREDENTIALS
request_payload[constants.ACE_PARAMETERS_LABELS_AUD] = unicode(RS_IP)
request_payload[constants.ACE_PARAMETERS_LABELS_SCOPE] = unicode(SCOPE)

print '====== Requesting access token from the AS ======'
print "Request payload:"
print binascii.hexlify(cbor.dumps(request_payload))


# obtain an access token
(respCode, respOptions, respPayload) = c.POST(as_uri,
confirmable=True,
options=[contentFormat, objectSecurity],
payload=u.str2buf(cbor.dumps(request_payload))
)
print '====='
print "Resource Server responded with 2.04 Changed."
payload_hex = u.buf2str(respPayload)
print 'Response payload:'
print binascii.hexlify(payload_hex)
print '====='

# Step 2: Decode the response, install the OSCORE security context and parse the access token for the RS
as_response = cbor.loads(payload_hex)

cnf = as_response[constants.ACE_PARAMETERS_LABELS_CNF]

cose_key = cnf[constants.ACE_CWT_CNF_COSE_KEY]

if cose_key[constants.COSE_KEY_LABEL_KTY] != constants.COSE_KEY_VALUE_SYMMETRIC:
raise NotImplementedError

if cose_key.get(constants.COSE_KEY_LABEL_ALG,
constants.COSE_ALG_AES_CCM_16_64_128) != constants.COSE_ALG_AES_CCM_16_64_128:
raise NotImplementedError
else:
aeadAlgo = oscoap.AES_CCM_16_64_128()

context_c_rs = oscoap.SecurityContext(
masterSecret=cose_key.get(constants.COSE_KEY_LABEL_K),
senderID=cose_key.get(constants.COSE_KEY_LABEL_CLIENT_ID),
recipientID=cose_key.get(constants.COSE_KEY_LABEL_SERVER_ID),
masterSalt=cose_key.get(constants.COSE_KEY_LABEL_SLT, ""),
aeadAlgorithm=aeadAlgo,
)

access_token = as_response[constants.ACE_PARAMETERS_LABELS_ACCESS_TOKEN]

audience = as_response.get(constants.ACE_PARAMETERS_LABELS_AUD,
"coap://[{0}]".format(RS_IP)) # if audience is not given, default to the RS we contacted in the first place

try:
print '====='
if not ask_user("Shall we send the access token to the RS?"):
abort(c)
print '====='


# Step 3: POST the access token to the RS over unprotected channel
(respCode, respOptions, respPayload) = c.POST('{0}/{1}'.format(audience, AUTHZ_INFO),
confirmable=True,
options=[],
payload=u.str2buf(access_token)
)

if respCode != d.COAP_RC_2_01_CREATED:
raise NotImplementedError

print '====='
print "Resource Server responded with 2.01 Created."
print '====='

print '====='
if not ask_user("Shall we request the resource again, using the obtained token?"):
abort(c)
print '====='

# Step 4: Request the resource over OSCORE
oscore = o.ObjectSecurity(context=context_c_rs)
(respCode, respOptions, respPayload) = c.GET('{0}/{1}'.format(audience, SCOPE),
confirmable=True,
options=[
oscore
],
)

print '===== GET to {0}/{1} returned ====='.format(audience, SCOPE)
print ''.join([chr(b) for b in respPayload])
print '====='

except Exception as err:
print err

# this includes CoAP errors
except Exception as err:
print err

abort(c)

7 changes: 5 additions & 2 deletions bin/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from coap import coap
from coap import coapOption as o
from coap import coapObjectSecurity as oscoap
from coap import coapUtils as u

import logging_setup

Expand All @@ -26,13 +27,15 @@

try:
# retrieve value of 'test' resource
p = c.GET('coap://[{0}]/test'.format(SERVER_IP),
(respCode, respOptions, respPayload) = c.GET('coap://[{0}]/test'.format(SERVER_IP),
confirmable=True,
options=[objectSecurity])

print '====='
print ''.join([chr(b) for b in p])
print ''.join([chr(b) for b in respPayload])
print binascii.hexlify(u.buf2str(respPayload))
print '====='

except Exception as err:
print err

Expand Down
6 changes: 3 additions & 3 deletions coap/coap.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def GET(self,uri,confirmable=True,options=[]):
options = options,
)
log.debug('response: {0}'.format(response))
return response['payload']
return (response['code'], response['options'], response['payload'])

def PUT(self,uri,confirmable=True,options=[],payload=None):
response = self._transmit(
Expand All @@ -87,7 +87,7 @@ def PUT(self,uri,confirmable=True,options=[],payload=None):
payload = payload
)
log.debug('response: {0}'.format(response))
return response['payload']
return (response['code'], response['options'], response['payload'])

def POST(self,uri,confirmable=True,options=[],payload=None):
response = self._transmit(
Expand All @@ -98,7 +98,7 @@ def POST(self,uri,confirmable=True,options=[],payload=None):
payload = payload
)
log.debug('response: {0}'.format(response))
return response['payload']
return (response['code'], response['options'], response['payload'])

def DELETE(self,uri,confirmable=True,options=[]):
self._transmit(
Expand Down
4 changes: 2 additions & 2 deletions coap/coapException.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(self,reason=''):
coapException.__init__(self,reason=reason)

class coapRcFactory(object):
def __new__(klass,rc):
def __new__(klass,rc,reason=''):
coapRcClasses = []
for (i,j) in globals().iteritems():
try:
Expand All @@ -65,7 +65,7 @@ def __new__(klass,rc):
pass
for coapRcClass in coapRcClasses:
if coapRcClass.rc==rc:
return coapRcClass()
return coapRcClass(reason=reason)
return coapRcUnknown(rc)

class coapRcUnknown(coapRc):
Expand Down
Loading