From bfa1a53836a8287d0245163cc14e4b5787ffb45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Thu, 26 Jul 2018 15:05:11 +0200 Subject: [PATCH 1/9] COAP-41: Return a tuple of (code, options, payload) to the client as part of the response --- bin/test_client.py | 7 +++++-- coap/coap.py | 6 +++--- tests/func/test_BADREQUEST.py | 2 +- tests/func/test_INTERNALSERVERERROR.py | 2 +- tests/func/test_METHODNOTALLOWED.py | 2 +- tests/func/test_NOTFOUND.py | 2 +- tests/func/test_UNAUTHORIZED.py | 4 ++-- tests/func/test_multiple_CON.py | 4 ++-- tests/func/test_multiple_NON.py | 4 ++-- tests/func/test_single_CON.py | 4 ++-- tests/func/test_single_NON.py | 4 ++-- tests/func/test_timeout_CON.py | 2 +- tests/func/test_timeout_NON.py | 2 +- 13 files changed, 24 insertions(+), 21 deletions(-) diff --git a/bin/test_client.py b/bin/test_client.py index 0164861..7f3d91c 100644 --- a/bin/test_client.py +++ b/bin/test_client.py @@ -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 @@ -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 diff --git a/coap/coap.py b/coap/coap.py index 57884ee..ad29563 100644 --- a/coap/coap.py +++ b/coap/coap.py @@ -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( @@ -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( @@ -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( diff --git a/tests/func/test_BADREQUEST.py b/tests/func/test_BADREQUEST.py index d48cee2..7da409b 100644 --- a/tests/func/test_BADREQUEST.py +++ b/tests/func/test_BADREQUEST.py @@ -43,7 +43,7 @@ def test_BADREQUEST(logFixture, snoopyDispatcher, twoEndPoints, confirmableFixtu clientOptions = [o.ObjectSecurity(context=clientContext)] with pytest.raises(e.coapRcBadRequest): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri='coap://[{0}]:{1}/{2}/'.format(IPADDRESS1, d.DEFAULT_UDP_PORT, RESOURCE), confirmable=confirmableFixture, options=clientOptions diff --git a/tests/func/test_INTERNALSERVERERROR.py b/tests/func/test_INTERNALSERVERERROR.py index 91d931b..004a8a2 100644 --- a/tests/func/test_INTERNALSERVERERROR.py +++ b/tests/func/test_INTERNALSERVERERROR.py @@ -71,7 +71,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get with pytest.raises(e.coapRcInternalServerError): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,'buggy'), confirmable = True, options=clientOptions diff --git a/tests/func/test_METHODNOTALLOWED.py b/tests/func/test_METHODNOTALLOWED.py index d038b4d..785b0c1 100644 --- a/tests/func/test_METHODNOTALLOWED.py +++ b/tests/func/test_METHODNOTALLOWED.py @@ -40,7 +40,7 @@ def test_METHODNOTALLOWED(logFixture,snoopyDispatcher,twoEndPoints,confirmableFi # have coap2 do a post with pytest.raises(e.coapRcMethodNotAllowed): - reply = coap2.POST( + (respCode, respOptions, respPayload) = coap2.POST( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = confirmableFixture, options=options diff --git a/tests/func/test_NOTFOUND.py b/tests/func/test_NOTFOUND.py index 09c9567..5a08c08 100644 --- a/tests/func/test_NOTFOUND.py +++ b/tests/func/test_NOTFOUND.py @@ -41,7 +41,7 @@ def test_NOTFOUND(logFixture,snoopyDispatcher,twoEndPoints,confirmableFixture): # have coap2 do a get with pytest.raises(e.coapRcNotFound): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE_INVALID), confirmable = confirmableFixture, options=options, diff --git a/tests/func/test_UNAUTHORIZED.py b/tests/func/test_UNAUTHORIZED.py index 716a8fd..9e5bbd8 100644 --- a/tests/func/test_UNAUTHORIZED.py +++ b/tests/func/test_UNAUTHORIZED.py @@ -36,7 +36,7 @@ def test_UNAUTHORIZED_1(logFixture,snoopyDispatcher,twoEndPoints,confirmableFixt if securityEnabled: # have coap2 do a get without including an Object-Security option with pytest.raises(e.coapRcUnauthorized): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = confirmableFixture, options=[] @@ -61,7 +61,7 @@ def test_UNAUTHORIZED_2(logFixture, snoopyDispatcher, twoEndPoints, confirmableF clientOptions = [o.ObjectSecurity(context=clientContext)] with pytest.raises(e.coapRcUnauthorized): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri='coap://[{0}]:{1}/{2}/'.format(IPADDRESS1, d.DEFAULT_UDP_PORT, RESOURCE), confirmable=confirmableFixture, options=clientOptions diff --git a/tests/func/test_multiple_CON.py b/tests/func/test_multiple_CON.py index 66da71f..74ecae2 100644 --- a/tests/func/test_multiple_CON.py +++ b/tests/func/test_multiple_CON.py @@ -36,10 +36,10 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get for _ in range(20): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options=options ) - assert reply==DUMMYVAL + assert respPayload==DUMMYVAL diff --git a/tests/func/test_multiple_NON.py b/tests/func/test_multiple_NON.py index 8c5a469..fe2e00f 100644 --- a/tests/func/test_multiple_NON.py +++ b/tests/func/test_multiple_NON.py @@ -35,9 +35,9 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get for _ in range(20): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options = options ) - assert reply==DUMMYVAL + assert respPayload==DUMMYVAL diff --git a/tests/func/test_single_CON.py b/tests/func/test_single_CON.py index 4e49492..1d5e9de 100644 --- a/tests/func/test_single_CON.py +++ b/tests/func/test_single_CON.py @@ -33,10 +33,10 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): options = [o.ObjectSecurity(context=context)] # have coap2 do a get - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri='coap://[{0}]:{1}/{2}/'.format(IPADDRESS1, d.DEFAULT_UDP_PORT, RESOURCE), confirmable=False, options=options ) - assert reply == DUMMYVAL + assert respPayload == DUMMYVAL diff --git a/tests/func/test_single_NON.py b/tests/func/test_single_NON.py index aeb5e32..a6b05df 100644 --- a/tests/func/test_single_NON.py +++ b/tests/func/test_single_NON.py @@ -34,10 +34,10 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): options = [o.ObjectSecurity(context=context)] # have coap2 do a get - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options=options, ) - assert reply==DUMMYVAL + assert respPayload==DUMMYVAL diff --git a/tests/func/test_timeout_CON.py b/tests/func/test_timeout_CON.py index 0b6264b..941a5d9 100644 --- a/tests/func/test_timeout_CON.py +++ b/tests/func/test_timeout_CON.py @@ -46,7 +46,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get with pytest.raises(e.coapTimeout): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS_INVALID,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = True, options=options, diff --git a/tests/func/test_timeout_NON.py b/tests/func/test_timeout_NON.py index be6cb08..4820aee 100644 --- a/tests/func/test_timeout_NON.py +++ b/tests/func/test_timeout_NON.py @@ -46,7 +46,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get with pytest.raises(e.coapTimeout): - reply = coap2.GET( + (respCode, respOptions, respPayload) = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS_INVALID,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options=options From dd90fbbb7027edca1624104f6a15184aea143a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Thu, 26 Jul 2018 15:53:37 +0200 Subject: [PATCH 2/9] COAP-42: Make hkdfDeriveParameter a public method --- coap/coapObjectSecurity.py | 59 ++++++++++++++------------------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/coap/coapObjectSecurity.py b/coap/coapObjectSecurity.py index df44cf1..ddeccd2 100644 --- a/coap/coapObjectSecurity.py +++ b/coap/coapObjectSecurity.py @@ -345,7 +345,7 @@ class AES_CCM_16_64_128(CCMAlgorithm): class SecurityContext: REPLAY_WINDOW_SIZE = 64 - def __init__(self, masterSecret, senderID, recipientID, aeadAlgorithm = AES_CCM_64_64_128(), masterSalt = '', hashFunction = hashlib.sha256): + def __init__(self, masterSecret='', senderID='', recipientID='', aeadAlgorithm=AES_CCM_64_64_128(), masterSalt='', hashFunction=hashlib.sha256): # Common context self.aeadAlgorithm = aeadAlgorithm @@ -355,43 +355,27 @@ def __init__(self, masterSecret, senderID, recipientID, aeadAlgorithm = AES_CCM_ # Sender context self.senderID = senderID - self.senderKey = self._hkdfDeriveParameter(self.hashFunction, - self.masterSecret, - self.masterSalt, - self.senderID, - self.aeadAlgorithm.value, - 'Key', - self.aeadAlgorithm.keyLength - ) - - self.senderIV = self._hkdfDeriveParameter(self.hashFunction, - self.masterSecret, - self.masterSalt, - self.senderID, - self.aeadAlgorithm.value, - 'IV', - self.aeadAlgorithm.ivLength + self.senderKey = self.hkdfDeriveParameter(self.senderID, + 'Key', + self.aeadAlgorithm.keyLength ) + + self.senderIV = self.hkdfDeriveParameter(self.senderID, + 'IV', + self.aeadAlgorithm.ivLength + ) self.sequenceNumber = 0 # Recipient context self.recipientID = recipientID - self.recipientKey = self._hkdfDeriveParameter(self.hashFunction, - self.masterSecret, - self.masterSalt, - self.recipientID, - self.aeadAlgorithm.value, - 'Key', - self.aeadAlgorithm.keyLength + self.recipientKey = self.hkdfDeriveParameter(self.recipientID, + 'Key', + self.aeadAlgorithm.keyLength + ) + self.recipientIV = self.hkdfDeriveParameter(self.recipientID, + 'IV', + self.aeadAlgorithm.ivLength ) - self.recipientIV = self._hkdfDeriveParameter(self.hashFunction, - self.masterSecret, - self.masterSalt, - self.recipientID, - self.aeadAlgorithm.value, - 'IV', - self.aeadAlgorithm.ivLength - ) self.replayWindow = [0] # ======================== public ========================================== @@ -423,19 +407,18 @@ def replayWindowUpdate(self, sequenceNumber): self.replayWindow += [sequenceNumber] - # ======================== private ========================================== - - def _hkdfDeriveParameter(self, hashFunction, masterSecret, masterSalt, id, algorithm, type, length): + def hkdfDeriveParameter(self, id='', type='', length=16): info = cbor.dumps([ id, - algorithm, + self.aeadAlgorithm.value, unicode(type), # encode as text string length ]) - extract = hkdf.hkdf_extract(salt=masterSalt, input_key_material=masterSecret, hash=hashFunction) - expand = hkdf.hkdf_expand(pseudo_random_key=extract, info=info, length=length, hash=hashFunction) + extract = hkdf.hkdf_extract(salt=self.masterSalt, input_key_material=self.masterSecret, hash=self.hashFunction) + expand = hkdf.hkdf_expand(pseudo_random_key=extract, info=info, length=length, hash=self.hashFunction) return expand + # ======================== private ========================================== From d1f3c2a386f9fcbcaaf194d547628802c001efc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Thu, 26 Jul 2018 17:17:43 +0200 Subject: [PATCH 3/9] COAP-40: First example implementation of ACE client Assumes Authorization Server (AS) is listening on a loopback interface. Client requests access to SCOPE resource without knowing where this resource is located. The IPv6 address of the RS is communicated to the client in the audience parameter of the response. Upon reception of the response, client installs a new OSCORE context and uses it to access the SCOPE resource on the RS. --- bin/constants.py | 112 +++++++++++++++++++++++++++++++++++++++ bin/test_ace_client.py | 117 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 bin/constants.py create mode 100644 bin/test_ace_client.py diff --git a/bin/constants.py b/bin/constants.py new file mode 100644 index 0000000..476562f --- /dev/null +++ b/bin/constants.py @@ -0,0 +1,112 @@ +# 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 + +# 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, +] diff --git a/bin/test_ace_client.py b/bin/test_ace_client.py new file mode 100644 index 0000000..1b1b5f2 --- /dev/null +++ b/bin/test_ace_client.py @@ -0,0 +1,117 @@ +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 + +import constants + +import logging_setup + +AS_IP = '::1' +SCOPE = 'resource1' +AUTHZ_INFO = 'authz-info' + +# open +c = coap.coap(udpPort=5000) + +context = oscoap.SecurityContext(masterSecret=binascii.unhexlify('000102030405060708090A0B0C0D0E0F'), + senderID=binascii.unhexlify('141592cc00000001'), + recipientID='JRC', + aeadAlgorithm=oscoap.AES_CCM_16_64_128()) + +objectSecurity = o.ObjectSecurity(context=context) + +contentFormat = o.ContentFormat(cformat=[d.FORMAT_CBOR]) + +try: + + # 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_SCOPE] = unicode(SCOPE) + + print '====== Request payload ======' + print binascii.hexlify(cbor.dumps(request_payload)) + print '=====' + + # obtain an access token + (respCode, respOptions, respPayload) = c.POST('coap://[{0}]/token'.format(AS_IP), + confirmable=True, + options=[contentFormat, objectSecurity], + payload=u.str2buf(cbor.dumps(request_payload)) + ) + + 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, AS_IP) # if audience is not given, what RS should we default to? + + # 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 + + # 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 '=====' + +# this includes CoAP errors +except Exception as err: + print err + + +# close +c.close() + +time.sleep(0.500) + +raw_input("Done. Press enter to close.") From e05d32cc779c879959be848307406e43dc57bc6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Sat, 28 Jul 2018 01:19:23 +0200 Subject: [PATCH 4/9] COAP-40. JRC within the Openvisualizer acts as an AS TODO: Obtain this dynamically when first contacting the RS. --- bin/test_ace_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/test_ace_client.py b/bin/test_ace_client.py index 1b1b5f2..5fe00f8 100644 --- a/bin/test_ace_client.py +++ b/bin/test_ace_client.py @@ -17,7 +17,7 @@ import logging_setup -AS_IP = '::1' +AS_IP = 'bbbb::1415:92cc:0:1' SCOPE = 'resource1' AUTHZ_INFO = 'authz-info' @@ -25,7 +25,7 @@ c = coap.coap(udpPort=5000) context = oscoap.SecurityContext(masterSecret=binascii.unhexlify('000102030405060708090A0B0C0D0E0F'), - senderID=binascii.unhexlify('141592cc00000001'), + senderID=binascii.unhexlify('636c69656e74'), recipientID='JRC', aeadAlgorithm=oscoap.AES_CCM_16_64_128()) From 982e50238c9690d05e38863d0b4aa3e5062079f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Fri, 16 Jun 2017 18:09:12 +0200 Subject: [PATCH 5/9] COAP-40. Pass error payload as part of the exception --- coap/coapException.py | 4 ++-- coap/coapTransmitter.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coap/coapException.py b/coap/coapException.py index 997d631..a774446 100644 --- a/coap/coapException.py +++ b/coap/coapException.py @@ -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: @@ -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): diff --git a/coap/coapTransmitter.py b/coap/coapTransmitter.py index 4586924..5858667 100644 --- a/coap/coapTransmitter.py +++ b/coap/coapTransmitter.py @@ -206,7 +206,7 @@ def receiveMessage(self, timestamp, srcIp, srcPort, message): # turn message into exception if needed if message['code'] not in d.METHOD_ALL+d.COAP_RC_ALL_SUCCESS: - message = e.coapRcFactory(message['code']) + message = e.coapRcFactory(rc=message['code'], reason=u.buf2str(message['payload'])) # store packet with self.dataLock: From 4662cbc2bb69021cbdd294ba469a30f78af1b3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Sun, 29 Jul 2018 02:02:46 +0200 Subject: [PATCH 6/9] COAP-40: ACE client who already knows the IP address of the RS Client contacts the RS directly and obtains the URI of the AS where it should request the access token. Exception handling needs to be improved. --- bin/constants.py | 4 +++ bin/test_ace_client.py | 74 ++++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/bin/constants.py b/bin/constants.py index 476562f..994381b 100644 --- a/bin/constants.py +++ b/bin/constants.py @@ -37,6 +37,10 @@ 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 diff --git a/bin/test_ace_client.py b/bin/test_ace_client.py index 5fe00f8..1deefb6 100644 --- a/bin/test_ace_client.py +++ b/bin/test_ace_client.py @@ -12,12 +12,13 @@ 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 -AS_IP = 'bbbb::1415:92cc:0:1' +RS_IP = 'bbbb::1415:92cc:0:2' SCOPE = 'resource1' AUTHZ_INFO = 'authz-info' @@ -34,10 +35,32 @@ contentFormat = o.ContentFormat(cformat=[d.FORMAT_CBOR]) 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: + + print "Unauthorized exception handling." + as_info = cbor.loads(err.reason) + + print '====== Response payload ======' + print as_info + 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 '====== Request payload ======' @@ -45,11 +68,11 @@ print '=====' # obtain an access token - (respCode, respOptions, respPayload) = c.POST('coap://[{0}]/token'.format(AS_IP), - confirmable=True, - options=[contentFormat, objectSecurity], - payload=u.str2buf(cbor.dumps(request_payload)) - ) + (respCode, respOptions, respPayload) = c.POST(as_uri, + confirmable=True, + options=[contentFormat, objectSecurity], + payload=u.str2buf(cbor.dumps(request_payload)) + ) payload_hex = u.buf2str(respPayload) print '====== Response payload ======' @@ -66,7 +89,8 @@ 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: + 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() @@ -81,28 +105,36 @@ access_token = as_response[constants.ACE_PARAMETERS_LABELS_ACCESS_TOKEN] - audience = as_response.get(constants.ACE_PARAMETERS_LABELS_AUD, AS_IP) # if audience is not given, what RS should we default to? + 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 # 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) - ) + confirmable=True, + options=[], + payload=u.str2buf(access_token) + ) if respCode != d.COAP_RC_2_01_CREATED: raise NotImplementedError - # 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 '=====' + try: + # 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: From 410c43dc7aabadb1ab354333833eb27f594b3075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Mon, 30 Jul 2018 03:19:13 +0200 Subject: [PATCH 7/9] COAP-40. Bug fix when payload is not present in the error response --- coap/coapTransmitter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coap/coapTransmitter.py b/coap/coapTransmitter.py index 5858667..1542c7a 100644 --- a/coap/coapTransmitter.py +++ b/coap/coapTransmitter.py @@ -206,7 +206,7 @@ def receiveMessage(self, timestamp, srcIp, srcPort, message): # turn message into exception if needed if message['code'] not in d.METHOD_ALL+d.COAP_RC_ALL_SUCCESS: - message = e.coapRcFactory(rc=message['code'], reason=u.buf2str(message['payload'])) + message = e.coapRcFactory(rc=message['code'], reason=u.buf2str(message.get('payload', []))) # store packet with self.dataLock: From 91af7779789b713af4801e166450c74c09a532e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Mon, 30 Jul 2018 15:55:09 +0200 Subject: [PATCH 8/9] COAP-40. Handle exception in case POST to RS fails --- bin/test_ace_client.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/bin/test_ace_client.py b/bin/test_ace_client.py index 1deefb6..da9d210 100644 --- a/bin/test_ace_client.py +++ b/bin/test_ace_client.py @@ -108,18 +108,17 @@ 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 - # 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 + try: + # 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 - try: # Step 4: Request the resource over OSCORE oscore = o.ObjectSecurity(context=context_c_rs) (respCode, respOptions, respPayload) = c.GET('{0}/{1}'.format(audience, SCOPE), From 93863f3ccf01b075d701d59c459f6ce0b061f9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mali=C5=A1a=20Vu=C4=8Dini=C4=87?= Date: Tue, 31 Jul 2018 12:12:38 +0200 Subject: [PATCH 9/9] COAP-40. Ask for client confirmation before proceding to each step. --- bin/test_ace_client.py | 63 ++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/bin/test_ace_client.py b/bin/test_ace_client.py index da9d210..eaca6b1 100644 --- a/bin/test_ace_client.py +++ b/bin/test_ace_client.py @@ -34,6 +34,27 @@ 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), @@ -46,13 +67,15 @@ print '=====' except e.coapRcUnauthorized as err: - - print "Unauthorized exception handling." as_info = cbor.loads(err.reason) - - print '====== Response payload ======' + 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 @@ -63,9 +86,10 @@ request_payload[constants.ACE_PARAMETERS_LABELS_AUD] = unicode(RS_IP) request_payload[constants.ACE_PARAMETERS_LABELS_SCOPE] = unicode(SCOPE) - print '====== Request payload ======' + print '====== Requesting access token from the AS ======' + print "Request payload:" print binascii.hexlify(cbor.dumps(request_payload)) - print '=====' + # obtain an access token (respCode, respOptions, respPayload) = c.POST(as_uri, @@ -73,9 +97,10 @@ 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 'Response payload:' print binascii.hexlify(payload_hex) print '=====' @@ -109,6 +134,12 @@ "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, @@ -119,6 +150,15 @@ 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), @@ -139,10 +179,5 @@ except Exception as err: print err +abort(c) -# close -c.close() - -time.sleep(0.500) - -raw_input("Done. Press enter to close.")