http signature fixes

master
Bob Mottram 2019-08-15 22:34:25 +01:00
parent ed050d48f5
commit 977160eecf
5 changed files with 46 additions and 45 deletions

View File

@ -400,6 +400,7 @@ class PubServer(BaseHTTPRequestHandler):
messageJson, messageJson,
self.headers['host'], self.headers['host'],
self.headers['signature'], self.headers['signature'],
self.headers,
'/'+self.path.split('/')[-1], '/'+self.path.split('/')[-1],
self.server.debug) self.server.debug)
if queueFilename: if queueFilename:

View File

@ -28,20 +28,21 @@ def signPostHeaders(privateKeyPem: str, nickname: str, domain: str, \
dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime()) dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime())
keyID = httpPrefix+'://'+domain+'/users/'+nickname+'#main-key' keyID = httpPrefix+'://'+domain+'/users/'+nickname+'#main-key'
if not messageBodyJson: if not messageBodyJson:
headers = {'host': domain} headers = {'(request-target)': f'post {path}','host': domain,'date': dateStr,'content-type': 'application/json'}
else: else:
bodyDigest = \ bodyDigest = \
base64.b64encode(SHA256.new(messageBodyJson.encode()).digest()) base64.b64encode(SHA256.new(messageBodyJson.encode()).digest())
headers = {'host': domain,'digest': f'SHA-256={bodyDigest}'} headers = {'(request-target)': f'post {path}','host': domain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': 'application/json'}
privateKeyPem = RSA.import_key(privateKeyPem) privateKeyPem = RSA.import_key(privateKeyPem)
headers.update({ #headers.update({
'(request-target)': f'post {path}', # '(request-target)': f'post {path}',
}) #})
# build a digest for signing # build a digest for signing
signedHeaderKeys = headers.keys() signedHeaderKeys = headers.keys()
signedHeaderText = '' signedHeaderText = ''
for headerKey in signedHeaderKeys: for headerKey in signedHeaderKeys:
signedHeaderText += f'{headerKey}: {headers[headerKey]}\n' signedHeaderText += f'{headerKey}: {headers[headerKey]}\n'
#print(f'headerKey: {headerKey}: {headers[headerKey]}')
signedHeaderText = signedHeaderText.strip() signedHeaderText = signedHeaderText.strip()
headerDigest = SHA256.new(signedHeaderText.encode('ascii')) headerDigest = SHA256.new(signedHeaderText.encode('ascii'))
@ -53,13 +54,11 @@ def signPostHeaders(privateKeyPem: str, nickname: str, domain: str, \
signatureDict = { signatureDict = {
'keyId': keyID, 'keyId': keyID,
'algorithm': 'rsa-sha256', 'algorithm': 'rsa-sha256',
# 'date': dateStr,
'headers': ' '.join(signedHeaderKeys), 'headers': ' '.join(signedHeaderKeys),
'signature': signature 'signature': signature
} }
signatureHeader = ','.join( signatureHeader = ','.join(
[f'{k}="{v}"' for k, v in signatureDict.items()]) [f'{k}="{v}"' for k, v in signatureDict.items()])
# print('signatureHeader: '+str(signatureHeader))
return signatureHeader return signatureHeader
def createSignedHeader(privateKeyPem: str,nickname: str,domain: str,port: int, \ def createSignedHeader(privateKeyPem: str,nickname: str,domain: str,port: int, \
@ -67,27 +66,27 @@ def createSignedHeader(privateKeyPem: str,nickname: str,domain: str,port: int, \
messageBodyJson: {}) -> {}: messageBodyJson: {}) -> {}:
headerDomain=domain headerDomain=domain
if port!=80 and port!=443: if port:
headerDomain=headerDomain+':'+str(port) if port!=80 and port!=443:
headerDomain=headerDomain+':'+str(port)
dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime()) dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime())
path='/inbox'
if not withDigest: if not withDigest:
headers = {'host': headerDomain} headers = {'(request-target)': f'post {path}','host': headerDomain,'date': dateStr}
else: else:
messageBodyJsonStr=json.dumps(messageBodyJson) messageBodyJsonStr=json.dumps(messageBodyJson)
bodyDigest = \ bodyDigest = \
base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()) base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest())
headers = {'host': headerDomain, 'digest': f'SHA-256={bodyDigest}'} headers = {'(request-target)': f'post {path}','host': headerDomain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': 'application/json'}
path='/inbox'
signatureHeader = signPostHeaders(privateKeyPem, nickname, domain, port, \ signatureHeader = signPostHeaders(privateKeyPem, nickname, domain, port, \
path, httpPrefix, None) path, httpPrefix, None)
headers['date'] = dateStr
headers['signature'] = signatureHeader headers['signature'] = signatureHeader
headers['Content-type'] = 'application/json' #print('******************************************************http headers: '+str(headers))
return headers return headers
def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, \ def verifyPostHeaders(httpPrefix: str,publicKeyPem: str,headers: dict, \
path: str, GETmethod: bool, \ path: str,GETmethod: bool, \
messageBodyJsonStr: str) -> bool: messageBodyJsonStr: str) -> bool:
"""Returns true or false depending on if the key that we plugged in here """Returns true or false depending on if the key that we plugged in here
validates against the headers, method, and path. validates against the headers, method, and path.
@ -109,6 +108,8 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, \
k: v[1:-1] k: v[1:-1]
for k, v in [i.split('=', 1) for i in signatureHeader.split(',')] for k, v in [i.split('=', 1) for i in signatureHeader.split(',')]
} }
#print('signatureHeader: '+str(signatureHeader))
#print('signatureDict: '+str(signatureDict))
# Unpack the signed headers and set values based on current headers and # Unpack the signed headers and set values based on current headers and
# body (if a digest was included) # body (if a digest was included)
@ -117,21 +118,19 @@ def verifyPostHeaders(httpPrefix: str, publicKeyPem: str, headers: dict, \
if signedHeader == '(request-target)': if signedHeader == '(request-target)':
signedHeaderList.append( signedHeaderList.append(
f'(request-target): {method.lower()} {path}') f'(request-target): {method.lower()} {path}')
elif signedHeader.lower() == 'content-type':
continue
elif signedHeader == 'date':
continue
elif signedHeader == 'digest': elif signedHeader == 'digest':
bodyDigest = \ bodyDigest = \
base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()) base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest())
signedHeaderList.append(f'digest: SHA-256={bodyDigest}') signedHeaderList.append(f'digest: SHA-256={bodyDigest}')
else: else:
try: if headers.get(signedHeader):
signedHeaderList.append( signedHeaderList.append(
f'{signedHeader}: {headers[signedHeader]}') f'{signedHeader}: {headers[signedHeader]}')
except Exception as e: else:
print('http signature check failure') signedHeaderCap=signedHeader.capitalize()
print(e) if headers.get(signedHeaderCap):
signedHeaderList.append(
f'{signedHeader}: {headers[signedHeaderCap]}')
# Now we have our header data digest # Now we have our header data digest
signedHeaderText = '\n'.join(signedHeaderList) signedHeaderText = '\n'.join(signedHeaderList)

View File

@ -164,7 +164,7 @@ def validPublishedDate(published) -> bool:
return False return False
return True return True
def savePostToInboxQueue(baseDir: str,httpPrefix: str,nickname: str, domain: str,postJsonObject: {},host: str,headers: str,postPath: str,debug: bool) -> str: def savePostToInboxQueue(baseDir: str,httpPrefix: str,nickname: str, domain: str,postJsonObject: {},host: str,headers: str,httpHeaders: str,postPath: str,debug: bool) -> str:
"""Saves the give json to the inbox queue for the person """Saves the give json to the inbox queue for the person
keyId specifies the actor sending the post keyId specifies the actor sending the post
""" """
@ -225,6 +225,7 @@ def savePostToInboxQueue(baseDir: str,httpPrefix: str,nickname: str, domain: str
'published': published, 'published': published,
'host': host, 'host': host,
'headers': headers, 'headers': headers,
'httpHeaders': httpHeaders,
'path': postPath, 'path': postPath,
'post': postJsonObject, 'post': postJsonObject,
'filename': filename, 'filename': filename,
@ -1135,15 +1136,11 @@ def runInboxQueue(projectVersion: str, \
continue continue
# check the signature # check the signature
verifyHeaders={
'host': queueJson['host'],
'signature': queueJson['headers']
}
if debug: if debug:
print('DEBUG: checking http headers') print('DEBUG: checking http headers')
pprint(verifyHeaders) pprint(queueJson['headers'])
if not verifyPostHeaders(httpPrefix, \ if not verifyPostHeaders(httpPrefix, \
pubKey,verifyHeaders, \ pubKey,queueJson['headers'], \
queueJson['path'],False, \ queueJson['path'],False, \
json.dumps(queueJson['post'])): json.dumps(queueJson['post'])):
if debug: if debug:

View File

@ -40,7 +40,7 @@ def getJson(session,url: str,headers: {},params: {}, \
print('WARN: no session specified for getJson') print('WARN: no session specified for getJson')
session.cookies.clear() session.cookies.clear()
try: try:
result=session.get(url, headers=sessionHeaders, params=sessionParams, allow_redirects=True) result=session.get(url, headers=sessionHeaders, params=sessionParams)
return result.json() return result.json()
except Exception as e: except Exception as e:
print('ERROR: getJson failed') print('ERROR: getJson failed')
@ -62,7 +62,7 @@ def postJson(session,postJsonObject: {},federationList: [],inboxUrl: str,headers
print('postJson: '+inboxUrl+' not permitted') print('postJson: '+inboxUrl+' not permitted')
return None return None
postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers, allow_redirects=True) postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers)
return postResult.text return postResult.text
def postImage(session,attachImageFilename: str,federationList: [],inboxUrl: str,headers: {},capability: str) -> str: def postImage(session,attachImageFilename: str,federationList: [],inboxUrl: str,headers: {},capability: str) -> str:

View File

@ -82,7 +82,7 @@ def testHttpsigBase(withDigest):
privateKeyPem,publicKeyPem,person,wfEndpoint= \ privateKeyPem,publicKeyPem,person,wfEndpoint= \
createPerson(path,nickname,domain,port,httpPrefix,False,password) createPerson(path,nickname,domain,port,httpPrefix,False,password)
assert privateKeyPem assert privateKeyPem
messageBodyJsonStr = '{"a key": "a value", "another key": "A string","yet another key": "A string"}' messageBodyJsonStr = '{"a key": "a value", "another key": "A string","yet another key": "Another string"}'
headersDomain=domain headersDomain=domain
if port!=80 and port !=443: if port!=80 and port !=443:
@ -90,39 +90,43 @@ def testHttpsigBase(withDigest):
dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime()) dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime())
if not withDigest: if not withDigest:
headers = {'host': headersDomain} headers = {'host': headersDomain,'date': dateStr,'content-type': 'application/json'}
else: else:
bodyDigest = \ bodyDigest = \
base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()) base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest())
headers = {'host': headersDomain, 'date': dateStr, 'digest': f'SHA-256={bodyDigest}'} headers = {'host': headersDomain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': 'application/json'}
boxpath='/inbox' boxpath='/inbox'
signatureHeader = \ signatureHeader = \
signPostHeaders(privateKeyPem, nickname, domain, port, boxpath, httpPrefix, None) signPostHeaders(privateKeyPem, nickname, domain, port, boxpath, httpPrefix, None)
headers['signature'] = signatureHeader headers['signature'] = signatureHeader
assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, \ assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, \
'/inbox' ,False, messageBodyJsonStr) '/inbox' ,False, \
messageBodyJsonStr)
assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, \ assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, \
'/parambulator/inbox', False , messageBodyJsonStr) == False '/parambulator/inbox',False, \
messageBodyJsonStr) == False
assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, \ assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, \
'/inbox', True, messageBodyJsonStr) == False '/inbox',True, \
messageBodyJsonStr) == False
if not withDigest: if not withDigest:
# fake domain # fake domain
headers = {'host': 'bogon.domain'} headers = {'host': 'bogon.domain','date': dateStr,'content-type': 'application/json'}
else: else:
# correct domain but fake message # correct domain but fake message
messageBodyJsonStr = '{"a key": "a value", "another key": "Fake GNUs", "yet another key": "Fake GNUs"}' messageBodyJsonStr = '{"a key": "a value", "another key": "Fake GNUs", "yet another key": "More Fake GNUs"}'
bodyDigest = base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()) bodyDigest = base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest())
headers = {'host': domain, 'date': dateStr, 'digest': f'SHA-256={bodyDigest}'} headers = {'host': domain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': 'application/json'}
headers['signature'] = signatureHeader headers['signature'] = signatureHeader
assert verifyPostHeaders(httpPrefix, publicKeyPem, headers, \ assert verifyPostHeaders(httpPrefix,publicKeyPem,headers, \
'/inbox', True, messageBodyJsonStr) == False '/inbox',True, \
messageBodyJsonStr) == False
os.chdir(baseDir) os.chdir(baseDir)
shutil.rmtree(path) shutil.rmtree(path)
def testHttpsig(): def testHttpsig():
testHttpsigBase(False)
testHttpsigBase(True) testHttpsigBase(True)
testHttpsigBase(False)
def testCache(): def testCache():
print('testCache') print('testCache')