From d98e68be5b8e5f642c8a4d7b117a3c25278eb0be Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Nov 2021 11:52:55 +0000 Subject: [PATCH 1/7] hs2019 same as rsa256 --- httpsig.py | 31 ++++++++++++++++++++++--------- tests.py | 3 ++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/httpsig.py b/httpsig.py index 5b1e1f37b..26d9f40cc 100644 --- a/httpsig.py +++ b/httpsig.py @@ -312,7 +312,8 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, if v.startswith('('): requestTargetKey = k requestTargetStr = v[1:-1] - break + elif v.startswith('"'): + signatureDict[k] = v[1:-1] if not requestTargetKey: return False signatureDict[requestTargetKey] = requestTargetStr @@ -354,6 +355,8 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, elif signedHeader == 'algorithm': if headers.get(signedHeader): algorithm = headers[signedHeader] + if debug: + print('http signature algorithm: ' + algorithm) elif signedHeader == 'digest': if messageBodyDigest: bodyDigest = messageBodyDigest @@ -447,20 +450,30 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, print('signature: ' + algorithm + ' ' + signatureDict['signature']) + # log unusual signing algorithms + if signatureDict.get('alg'): + print('http signature algorithm: ' + signatureDict['alg']) + # If extra signing algorithms need to be added then do it here - if algorithm == 'rsa-sha256': - headerDigest = getSHA256(signedHeaderText.encode('ascii')) - paddingStr = padding.PKCS1v15() + if not signatureDict.get('alg'): alg = hazutils.Prehashed(hashes.SHA256()) - elif algorithm == 'rsa-sha512': - headerDigest = getSHA512(signedHeaderText.encode('ascii')) - paddingStr = padding.PKCS1v15() + elif signatureDict['alg'] == 'rsa-sha256': + alg = hazutils.Prehashed(hashes.SHA256()) + elif signatureDict['alg'] == 'hs2019': + alg = hazutils.Prehashed(hashes.SHA256()) + elif signatureDict['alg'] == 'rsa-sha512': alg = hazutils.Prehashed(hashes.SHA512()) else: - print('Unknown http signature algorithm: ' + algorithm) - paddingStr = padding.PKCS1v15() alg = hazutils.Prehashed(hashes.SHA256()) + + if algorithm == 'rsa-sha256' or algorithm == 'hs2019': + headerDigest = getSHA256(signedHeaderText.encode('ascii')) + elif algorithm == 'rsa-sha512': + headerDigest = getSHA512(signedHeaderText.encode('ascii')) + else: + print('Unknown http signature algorithm: ' + algorithm) headerDigest = '' + paddingStr = padding.PKCS1v15() try: pubkey.verify(signature, headerDigest, paddingStr, alg) diff --git a/tests.py b/tests.py index 058935dda..898f535dc 100644 --- a/tests.py +++ b/tests.py @@ -623,9 +623,10 @@ def _testHttpsigBase(withDigest: bool, baseDir: str): headers['signature'] = signatureHeader GETmethod = not withDigest + debug = True assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, boxpath, GETmethod, None, - messageBodyJsonStr, False) + messageBodyJsonStr, debug) if withDigest: # everything correct except for content-length headers['content-length'] = str(contentLength + 2) From 442c4f4daa4a77345e36f67553b227a51cbdad0e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Nov 2021 11:59:41 +0000 Subject: [PATCH 2/7] Check for invalid ciphertext later --- daemon.py | 6 ------ inbox.py | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/daemon.py b/daemon.py index a18c29fb8..a541ecdec 100644 --- a/daemon.py +++ b/daemon.py @@ -242,7 +242,6 @@ from like import updateLikesCollection from reaction import updateReactionCollection from utils import undoReactionCollectionEntry from utils import getNewPostEndpoints -from utils import malformedCiphertext from utils import hasActor from utils import setReplyIntervalHours from utils import canReplyTo @@ -1503,11 +1502,6 @@ class PubServer(BaseHTTPRequestHandler): # save the json for later queue processing messageBytesDecoded = messageBytes.decode('utf-8') - if malformedCiphertext(messageBytesDecoded): - print('WARN: post contains malformed ciphertext ' + - str(originalMessageJson)) - return 4 - if containsInvalidLocalLinks(messageBytesDecoded): print('WARN: post contains invalid local links ' + str(originalMessageJson)) diff --git a/inbox.py b/inbox.py index 3a88b42b7..a2cf0ba8a 100644 --- a/inbox.py +++ b/inbox.py @@ -17,6 +17,7 @@ from languages import understoodPostLanguage from like import updateLikesCollection from reaction import updateReactionCollection from reaction import validEmojiContent +from utils import malformedCiphertext from utils import removeHtml from utils import fileLastModified from utils import hasObjectString @@ -2258,6 +2259,9 @@ def _validPostContent(baseDir: str, nickname: str, domain: str, print('REJECT: reply to post which does not ' + 'allow comments: ' + originalPostId) return False + if malformedCiphertext(messageJson['object']['content']): + print('REJECT: malformed ciphertext in content') + return False if debug: print('ACCEPT: post content is valid') return True From b6fda529d12b22fb6a68974faa7a155b554cf477 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Nov 2021 12:05:09 +0000 Subject: [PATCH 3/7] More standard terminology --- inbox.py | 4 ++-- utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inbox.py b/inbox.py index a2cf0ba8a..d780530ff 100644 --- a/inbox.py +++ b/inbox.py @@ -17,7 +17,7 @@ from languages import understoodPostLanguage from like import updateLikesCollection from reaction import updateReactionCollection from reaction import validEmojiContent -from utils import malformedCiphertext +from utils import invalidCiphertext from utils import removeHtml from utils import fileLastModified from utils import hasObjectString @@ -2259,7 +2259,7 @@ def _validPostContent(baseDir: str, nickname: str, domain: str, print('REJECT: reply to post which does not ' + 'allow comments: ' + originalPostId) return False - if malformedCiphertext(messageJson['object']['content']): + if invalidCiphertext(messageJson['object']['content']): print('REJECT: malformed ciphertext in content') return False if debug: diff --git a/utils.py b/utils.py index 5ac0351b6..7592bd1c1 100644 --- a/utils.py +++ b/utils.py @@ -2653,8 +2653,8 @@ def isPGPEncrypted(content: str) -> bool: return False -def malformedCiphertext(content: str) -> bool: - """Returns true if the given content contains a malformed key +def invalidCiphertext(content: str) -> bool: + """Returns true if the given content contains an invalid key """ if '----BEGIN ' in content or '----END ' in content: if not containsPGPPublicKey(content) and \ From 44308df9998663436fb9b60a783570ed58439204 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Nov 2021 12:10:23 +0000 Subject: [PATCH 4/7] Check announced posts for invalid ciphertext --- posts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/posts.py b/posts.py index 8c81ae68c..422a3b2d8 100644 --- a/posts.py +++ b/posts.py @@ -32,6 +32,7 @@ from webfinger import webfingerHandle from httpsig import createSignedHeader from siteactive import siteIsActive from languages import understoodPostLanguage +from utils import invalidCiphertext from utils import hasObjectStringType from utils import removeIdEnding from utils import replaceUsersWithAt @@ -4602,6 +4603,11 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str, recentPostsCache) return None + if invalidCiphertext(contentStr): + print('WARN: Invalid ciphertext within announce ' + + str(announcedJson)) + return None + # remove any long words contentStr = removeLongWords(contentStr, 40, []) From e48e2a0fd1e16f79886d7119bcb19b04431e45f4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Nov 2021 12:25:35 +0000 Subject: [PATCH 5/7] Mark announces with bad ciphertext as rejected --- posts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/posts.py b/posts.py index 422a3b2d8..2837a224f 100644 --- a/posts.py +++ b/posts.py @@ -4604,6 +4604,9 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str, return None if invalidCiphertext(contentStr): + _rejectAnnounce(announceFilename, + baseDir, nickname, domain, postId, + recentPostsCache) print('WARN: Invalid ciphertext within announce ' + str(announcedJson)) return None From 0ce75731047035ac6682df699f85ed5e477c47a7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Nov 2021 18:30:05 +0000 Subject: [PATCH 6/7] Improve support for hs2019 http signatures --- daemon.py | 2 + httpsig.py | 83 +++++++++++------- tests.py | 250 +++++++++++++++++++++++++++++++++++------------------ 3 files changed, 218 insertions(+), 117 deletions(-) diff --git a/daemon.py b/daemon.py index a541ecdec..9b3da6fc3 100644 --- a/daemon.py +++ b/daemon.py @@ -450,6 +450,8 @@ class PubServer(BaseHTTPRequestHandler): # https://tools.ietf.org/html/ # draft-ietf-httpbis-message-signatures-01 return self.headers['Signature-Input'] + elif self.headers.get('signature-input'): + return self.headers['signature-input'] elif self.headers.get('signature'): # Ye olde Masto http sig return self.headers['signature'] diff --git a/httpsig.py b/httpsig.py index 26d9f40cc..779855eda 100644 --- a/httpsig.py +++ b/httpsig.py @@ -11,7 +11,7 @@ __module_group__ = "Security" # see https://tools.ietf.org/html/draft-cavage-http-signatures-06 # # This might change in future -# see https://tools.ietf.org/html/draft-ietf-httpbis-message-signatures-01 +# see https://tools.ietf.org/html/draft-ietf-httpbis-message-signatures from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import load_pem_private_key @@ -116,11 +116,11 @@ def signPostHeadersNew(dateStr: str, privateKeyPem: str, path: str, httpPrefix: str, messageBodyJsonStr: str, - algorithm: str) -> (str, str): + algorithm: str, debug: bool) -> (str, str): """Returns a raw signature strings that can be plugged into a header as "Signature-Input" and "Signature" used to verify the authenticity of an HTTP transmission. - See https://tools.ietf.org/html/draft-ietf-httpbis-message-signatures-01 + See https://tools.ietf.org/html/draft-ietf-httpbis-message-signatures """ domain = getFullDomain(domain, port) @@ -137,18 +137,17 @@ def signPostHeadersNew(dateStr: str, privateKeyPem: str, keyID = localActorUrl(httpPrefix, nickname, domain) + '#main-key' if not messageBodyJsonStr: headers = { - '*request-target': f'post {path}', - '*created': str(secondsSinceEpoch), + '@request-target': f'get {path}', + '@created': str(secondsSinceEpoch), 'host': toDomain, - 'date': dateStr, - 'content-type': 'application/json' + 'date': dateStr } else: bodyDigest = messageContentDigest(messageBodyJsonStr) contentLength = len(messageBodyJsonStr) headers = { - '*request-target': f'post {path}', - '*created': str(secondsSinceEpoch), + '@request-target': f'post {path}', + '@created': str(secondsSinceEpoch), 'host': toDomain, 'date': dateStr, 'digest': f'SHA-256={bodyDigest}', @@ -164,6 +163,10 @@ def signPostHeadersNew(dateStr: str, privateKeyPem: str, signedHeaderText += f'{headerKey}: {headers[headerKey]}\n' signedHeaderText = signedHeaderText.strip() + if debug: + print('\nsignPostHeadersNew signedHeaderText:\n' + + signedHeaderText + '\nEND\n') + # Sign the digest. Potentially other signing algorithms can be added here. signature = '' if algorithm == 'rsa-sha512': @@ -298,8 +301,11 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, pubkey = load_pem_public_key(publicKeyPem.encode('utf-8'), backend=default_backend()) # Build a dictionary of the signature values - if headers.get('Signature-Input'): - signatureHeader = headers['Signature-Input'] + if headers.get('Signature-Input') or headers.get('signature-input'): + if headers.get('Signature-Input'): + signatureHeader = headers['Signature-Input'] + else: + signatureHeader = headers['signature-input'] fieldSep2 = ',' # split the signature input into separate fields signatureDict = { @@ -342,15 +348,23 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, # original Mastodon http signature appendStr = f'(request-target): {method.lower()} {path}' signedHeaderList.append(appendStr) - elif '*request-target' in signedHeader: + elif '@request-target' in signedHeader: # https://tools.ietf.org/html/ - # draft-ietf-httpbis-message-signatures-01 - appendStr = f'*request-target: {method.lower()} {path}' - # remove () - # if appendStr.startswith('('): - # appendStr = appendStr.split('(')[1] - # if ')' in appendStr: - # appendStr = appendStr.split(')')[0] + # draft-ietf-httpbis-message-signatures + appendStr = f'@request-target: {method.lower()} {path}' + signedHeaderList.append(appendStr) + elif '@created' in signedHeader: + if signatureDict.get('created'): + createdStr = str(signatureDict['created']) + appendStr = f'@created: {createdStr}' + signedHeaderList.append(appendStr) + elif '@expires' in signedHeader: + if signatureDict.get('expires'): + expiresStr = str(signatureDict['expires']) + appendStr = f'@expires: {expiresStr}' + signedHeaderList.append(appendStr) + elif '@method' in signedHeader: + appendStr = f'@expires: {method}' signedHeaderList.append(appendStr) elif signedHeader == 'algorithm': if headers.get(signedHeader): @@ -430,14 +444,18 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, # Now we have our header data digest signedHeaderText = '\n'.join(signedHeaderList) if debug: - print('signedHeaderText:\n' + signedHeaderText + 'END') + print('\nverifyPostHeaders signedHeaderText:\n' + + signedHeaderText + '\nEND\n') # Get the signature, verify with public key, return result - signature = None - if headers.get('Signature-Input') and headers.get('Signature'): + if (headers.get('Signature-Input') and headers.get('Signature')) or \ + (headers.get('signature-input') and headers.get('signature')): # https://tools.ietf.org/html/ - # draft-ietf-httpbis-message-signatures-01 - headersSig = headers['Signature'] + # draft-ietf-httpbis-message-signatures + if headers.get('Signature'): + headersSig = headers['Signature'] + else: + headersSig = headers['signature'] # remove sig1=: if requestTargetKey + '=:' in headersSig: headersSig = headersSig.split(requestTargetKey + '=:')[1] @@ -445,10 +463,10 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, signature = base64.b64decode(headersSig) else: # Original Mastodon signature - signature = base64.b64decode(signatureDict['signature']) - if debug: - print('signature: ' + algorithm + ' ' + - signatureDict['signature']) + headersSig = signatureDict['signature'] + signature = base64.b64decode(headersSig) + if debug: + print('signature: ' + algorithm + ' ' + headersSig) # log unusual signing algorithms if signatureDict.get('alg'): @@ -457,11 +475,12 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, # If extra signing algorithms need to be added then do it here if not signatureDict.get('alg'): alg = hazutils.Prehashed(hashes.SHA256()) - elif signatureDict['alg'] == 'rsa-sha256': + elif (signatureDict['alg'] == 'rsa-sha256' or + signatureDict['alg'] == 'rsa-v1_5-sha256' or + signatureDict['alg'] == 'hs2019'): alg = hazutils.Prehashed(hashes.SHA256()) - elif signatureDict['alg'] == 'hs2019': - alg = hazutils.Prehashed(hashes.SHA256()) - elif signatureDict['alg'] == 'rsa-sha512': + elif (signatureDict['alg'] == 'rsa-sha512' or + signatureDict['alg'] == 'rsa-pss-sha512'): alg = hazutils.Prehashed(hashes.SHA512()) else: alg = hazutils.Prehashed(hashes.SHA256()) diff --git a/tests.py b/tests.py index 898f535dc..704accece 100644 --- a/tests.py +++ b/tests.py @@ -392,8 +392,20 @@ def _testSignAndVerify() -> None: def _testHttpSigNew(): print('testHttpSigNew') + httpPrefix = 'https' + port = 443 + debug = True messageBodyJson = {"hello": "world"} messageBodyJsonStr = json.dumps(messageBodyJson) + nickname = 'foo' + pathStr = "/" + nickname + "?param=value&pet=dog HTTP/1.1" + domain = 'example.com' + dateStr = 'Tue, 20 Apr 2021 02:07:55 GMT' + digestStr = 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=' + bodyDigest = messageContentDigest(messageBodyJsonStr) + assert bodyDigest in digestStr + contentLength = 18 + contentType = 'application/activity+json' publicKeyPem = \ '-----BEGIN RSA PUBLIC KEY-----\n' + \ 'MIIBCgKCAQEAhAKYdtoeoy8zcAcR874L8' + \ @@ -462,101 +474,49 @@ def _testHttpSigNew(): 'EQeNC8fHGg4UXU8mhHnSBt3EA10qQJfRD' + \ 's15M38eG2cYwB1PZpDHScDnDA0=\n' + \ '-----END RSA PRIVATE KEY-----' - sigInput = \ - 'sig1=(date); alg=rsa-sha256; keyId="test-key-b"' - sig = \ - 'sig1=:HtXycCl97RBVkZi66ADKnC9c5eSSlb57GnQ4KFqNZplOpNfxqk62' + \ - 'JzZ484jXgLvoOTRaKfR4hwyxlcyb+BWkVasApQovBSdit9Ml/YmN2IvJDPncrlhPD' + \ - 'VDv36Z9/DiSO+RNHD7iLXugdXo1+MGRimW1RmYdenl/ITeb7rjfLZ4b9VNnLFtVWw' + \ - 'rjhAiwIqeLjodVImzVc5srrk19HMZNuUejK6I3/MyN3+3U8tIRW4LWzx6ZgGZUaEE' + \ - 'P0aBlBkt7Fj0Tt5/P5HNW/Sa/m8smxbOHnwzAJDa10PyjzdIbywlnWIIWtZKPPsoV' + \ - 'oKVopUWEU3TNhpWmaVhFrUL/O6SN3w==:' - # "hs2019", using RSASSA-PSS [RFC8017] and SHA-512 [RFC6234] - # sigInput = \ - # 'sig1=(*request-target, *created, host, date, ' + \ - # 'cache-control, x-empty-header, x-example); keyId="test-key-a"; ' + \ - # 'alg=hs2019; created=1402170695; expires=1402170995' - # sig = \ - # 'sig1=:K2qGT5srn2OGbOIDzQ6kYT+ruaycnDAAUpKv+ePFfD0RAxn/1BUe' + \ - # 'Zx/Kdrq32DrfakQ6bPsvB9aqZqognNT6be4olHROIkeV879RrsrObury8L9SCEibe' + \ - # 'oHyqU/yCjphSmEdd7WD+zrchK57quskKwRefy2iEC5S2uAH0EPyOZKWlvbKmKu5q4' + \ - # 'CaB8X/I5/+HLZLGvDiezqi6/7p2Gngf5hwZ0lSdy39vyNMaaAT0tKo6nuVw0S1MVg' + \ - # '1Q7MpWYZs0soHjttq0uLIA3DIbQfLiIvK6/l0BdWTU7+2uQj7lBkQAsFZHoA96ZZg' + \ - # 'FquQrXRlmYOh+Hx5D9fJkXcXe5tmAg==:' - nickname = 'foo' - boxpath = '/' + nickname - # headers = { - # "*request-target": "get " + boxpath, - # "*created": "1402170695", - # "host": "example.org", - # "date": "Tue, 07 Jun 2014 20:51:35 GMT", - # "cache-control": "max-age=60, must-revalidate", - # "x-emptyheader": "", - # "x-example": "Example header with some whitespace.", - # "x-dictionary": "b=2", - # "x-dictionary": "a=1", - # "x-list": "(a, b, c)", - # "Signature-Input": sigInput, - # "Signature": sig - # } - dateStr = "Tue, 07 Jun 2014 20:51:35 GMT" - secondsSinceEpoch = 1402174295 - domain = "example.com" - port = 443 - headers = { - "*created": str(secondsSinceEpoch), - "*request-target": "post /foo?param=value&pet=dog", - "host": domain, - "date": dateStr, - "content-type": "application/json", - "digest": "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", - "content-length": "18", - "Signature-Input": sigInput, - "Signature": sig - } - httpPrefix = 'https' - debug = False - assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, - boxpath, False, None, - messageBodyJsonStr, debug, True) - # make a deliberate mistake - headers['Signature'] = headers['Signature'].replace('V', 'B') - assert not verifyPostHeaders(httpPrefix, publicKeyPem, headers, - boxpath, False, None, - messageBodyJsonStr, debug, True) - # test signing - bodyDigest = messageContentDigest(messageBodyJsonStr) - contentLength = len(messageBodyJsonStr) headers = { "host": domain, "date": dateStr, "digest": f'SHA-256={bodyDigest}', - "content-type": "application/json", + "content-type": contentType, "content-length": str(contentLength) } signatureIndexHeader, signatureHeader = \ signPostHeadersNew(dateStr, privateKeyPem, nickname, domain, port, domain, port, - boxpath, httpPrefix, messageBodyJsonStr, - 'rsa-sha256') - expectedIndexHeader = \ - 'keyId="https://example.com/users/foo#main-key"; ' + \ - 'alg=hs2019; created=' + str(secondsSinceEpoch) + '; ' + \ - 'sig1=(*request-target, *created, host, date, ' + \ - 'digest, content-type, content-length)' - if signatureIndexHeader != expectedIndexHeader: - print('Unexpected new http header: ' + signatureIndexHeader) - print('Should be: ' + expectedIndexHeader) - assert signatureIndexHeader == expectedIndexHeader - assert signatureHeader == \ - 'sig1=:euX3O1KSTYXN9/oR2qFezswWm9FbrjtRymK7xBpXNQvTs' + \ - 'XehtrNdD8nELZKzPXMvMz7PaJd6V+fjzpHoZ9upTdqqQLK2Iwml' + \ - 'p4BlHqW6Aopd7sZFCWFq7/Amm5oaizpp3e0jb5XISS5m3cRKuoi' + \ - 'LM0x+OudmAoYGi0TEEJk8bpnJAXfVCDfmOyL3XNqQeShQHeOANG' + \ - 'okiKktj8ff+KLYLaPTAJkob1k/EhoPIkbw/YzAY8IZjWQNMkf+F' + \ - 'JChApQ5HnDCQPwD5xV9eGzBpAf6D0G19xiTmQye4Hn6tAs3fy3V' + \ - '/aYa/GhW2pSrctDnAKIi4imj9joppr3CB8gqgXZOPQ==:' + pathStr, httpPrefix, messageBodyJsonStr, + 'rsa-sha256', debug) + print('signatureIndexHeader1: ' + str(signatureIndexHeader)) + print('signatureHeader1: ' + str(signatureHeader)) + sigInput = "keyId=\"https://example.com/users/foo#main-key\"; " + \ + "alg=hs2019; created=1618884475; " + \ + "sig1=(@request-target, @created, host, date, digest, " + \ + "content-type, content-length)" + assert signatureIndexHeader == sigInput + sig = "sig1=:NXAQ7AtDMR2iwhmH1qCwiZw5PVTjOw5+5kSu0Tsx/3gqz0D" + \ + "py7OQbWqFHrNB7MmS4TukX/vDyQOFdElY5yxnEhbgRwKACq0AP4QH9H" + \ + "CiRyCE8UXDdAkY4VUd6jrWjRHKRoqQN7I+Q5tb2Fu5cDfifw/PQc86Z" + \ + "NmMhPrg3OjUJ9Q2Gj29NhgJ+4el1ECg0cAy4yG1M9AQ3KvQooQFvlg1" + \ + "vp0H2xfbJQjv8FsR/lKiRdaVHqGR2CKrvxvPRPaOsFANp2wzEtiMk3O" + \ + "TrBTYU+Zb53mIspfEeLxsNtcGmBDmQKZ9Pud8f99XGJrP+uDd3zKtnr" + \ + "f3fUnRRqy37yhB7WVwkg==:" + assert signatureHeader == sig + + debug = True + headers['path'] = pathStr + headers['signature'] = sig + headers['signature-input'] = sigInput + assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, + pathStr, False, None, + messageBodyJsonStr, debug, True) + + # make a deliberate mistake + debug = False + headers['signature'] = headers['signature'].replace('V', 'B') + assert not verifyPostHeaders(httpPrefix, publicKeyPem, headers, + pathStr, False, None, + messageBodyJsonStr, debug, True) def _testHttpsigBase(withDigest: bool, baseDir: str): @@ -5920,6 +5880,124 @@ def _testValidEmojiContent() -> None: assert validEmojiContent('😄') +def _testHttpsigBaseNew(withDigest: bool, baseDir: str): + print('testHttpsigNew(' + str(withDigest) + ')') + + debug = True + path = baseDir + '/.testHttpsigBaseNew' + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=False, onerror=None) + os.mkdir(path) + os.chdir(path) + + contentType = 'application/activity+json' + nickname = 'socrates' + hostDomain = 'someother.instance' + domain = 'argumentative.social' + httpPrefix = 'https' + port = 5576 + password = 'SuperSecretPassword' + privateKeyPem, publicKeyPem, person, wfEndpoint = \ + createPerson(path, nickname, domain, port, httpPrefix, + False, False, password) + assert privateKeyPem + if withDigest: + messageBodyJson = { + "a key": "a value", + "another key": "A string", + "yet another key": "Another string" + } + messageBodyJsonStr = json.dumps(messageBodyJson) + else: + messageBodyJsonStr = '' + + headersDomain = getFullDomain(hostDomain, port) + + dateStr = strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime()) + boxpath = '/inbox' + if not withDigest: + headers = { + 'host': headersDomain, + 'date': dateStr, + 'accept': contentType + } + signatureIndexHeader, signatureHeader = \ + signPostHeadersNew(dateStr, privateKeyPem, nickname, + domain, port, + hostDomain, port, + boxpath, httpPrefix, messageBodyJsonStr, + 'rsa-sha256', debug) + else: + bodyDigest = messageContentDigest(messageBodyJsonStr) + contentLength = len(messageBodyJsonStr) + headers = { + 'host': headersDomain, + 'date': dateStr, + 'digest': f'SHA-256={bodyDigest}', + 'content-type': contentType, + 'content-length': str(contentLength) + } + signatureIndexHeader, signatureHeader = \ + signPostHeadersNew(dateStr, privateKeyPem, nickname, + domain, port, + hostDomain, port, + boxpath, httpPrefix, messageBodyJsonStr, + 'rsa-sha256', debug) + + headers['signature'] = signatureHeader + headers['signature-input'] = signatureIndexHeader + print('headers: ' + str(headers)) + + GETmethod = not withDigest + debug = True + assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, + boxpath, GETmethod, None, + messageBodyJsonStr, debug) + debug = False + if withDigest: + # everything correct except for content-length + headers['content-length'] = str(contentLength + 2) + assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, + boxpath, GETmethod, None, + messageBodyJsonStr, debug) is False + assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, + '/parambulator' + boxpath, GETmethod, None, + messageBodyJsonStr, debug) is False + assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, + boxpath, not GETmethod, None, + messageBodyJsonStr, debug) is False + if not withDigest: + # fake domain + headers = { + 'host': 'bogon.domain', + 'date': dateStr, + 'content-type': contentType + } + else: + # correct domain but fake message + messageBodyJsonStr = \ + '{"a key": "a value", "another key": "Fake GNUs", ' + \ + '"yet another key": "More Fake GNUs"}' + contentLength = len(messageBodyJsonStr) + bodyDigest = messageContentDigest(messageBodyJsonStr) + headers = { + 'host': domain, + 'date': dateStr, + 'digest': f'SHA-256={bodyDigest}', + 'content-type': contentType, + 'content-length': str(contentLength) + } + headers['signature'] = signatureHeader + headers['signature-input'] = signatureIndexHeader + pprint(headers) + assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, + boxpath, not GETmethod, None, + messageBodyJsonStr, False) is False + + os.chdir(baseDir) + shutil.rmtree(path, ignore_errors=False, onerror=None) + + def runAllTests(): baseDir = os.getcwd() print('Running tests...') @@ -5991,6 +6069,8 @@ def runAllTests(): _testHttpsig(baseDir) _testHttpSignedGET(baseDir) _testHttpSigNew() + _testHttpsigBaseNew(True, baseDir) + _testHttpsigBaseNew(False, baseDir) _testCache() _testThreads() _testCreatePerson(baseDir) From 438bb45c7a233519f7d810dc05d781068d195948 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Nov 2021 19:46:28 +0000 Subject: [PATCH 7/7] Extra signature fields --- daemon.py | 4 +++- httpsig.py | 14 +++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/daemon.py b/daemon.py index 9b3da6fc3..ff6562e19 100644 --- a/daemon.py +++ b/daemon.py @@ -729,7 +729,9 @@ class PubServer(BaseHTTPRequestHandler): return False # verify the GET request without any digest - if verifyPostHeaders(self.server.httpPrefix, pubKey, self.headers, + if verifyPostHeaders(self.server.httpPrefix, + self.server.domainFull, + pubKey, self.headers, self.path, True, None, '', self.server.debug): return True diff --git a/httpsig.py b/httpsig.py index 779855eda..3b1623019 100644 --- a/httpsig.py +++ b/httpsig.py @@ -272,7 +272,8 @@ def _verifyRecentSignature(signedDateStr: str) -> bool: return True -def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, +def verifyPostHeaders(httpPrefix: str, + publicKeyPem: str, headers: dict, path: str, GETmethod: bool, messageBodyDigest: str, messageBodyJsonStr: str, debug: bool, @@ -366,6 +367,17 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, elif '@method' in signedHeader: appendStr = f'@expires: {method}' signedHeaderList.append(appendStr) + elif '@scheme' in signedHeader: + signedHeaderList.append('@scheme: http') + elif '@authority' in signedHeader: + authorityStr = None + if signatureDict.get('authority'): + authorityStr = str(signatureDict['authority']) + elif signatureDict.get('Authority'): + authorityStr = str(signatureDict['Authority']) + if authorityStr: + appendStr = f'@authority: {authorityStr}' + signedHeaderList.append(appendStr) elif signedHeader == 'algorithm': if headers.get(signedHeader): algorithm = headers[signedHeader]