Support for new style of http signatures

main
Bob Mottram 2021-02-22 18:20:33 +00:00
parent 56e9130287
commit 3d1c440584
2 changed files with 180 additions and 56 deletions

View File

@ -18,6 +18,7 @@ from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import utils as hazutils from cryptography.hazmat.primitives.asymmetric import utils as hazutils
import calendar
import base64 import base64
from time import gmtime, strftime from time import gmtime, strftime
import datetime import datetime
@ -99,6 +100,89 @@ def signPostHeaders(dateStr: str, privateKeyPem: str,
return signatureHeader return signatureHeader
def signPostHeadersNew(dateStr: str, privateKeyPem: str,
nickname: str,
domain: str, port: int,
toDomain: str, toPort: int,
path: str,
httpPrefix: str,
messageBodyJsonStr: str,
algorithm: str) -> (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
"""
domain = getFullDomain(domain, port)
toDomain = getFullDomain(toDomain, toPort)
timeFormat = "%a, %d %b %Y %H:%M:%S %Z"
if not dateStr:
currTime = gmtime()
secondsSinceEpoch = int(calendar.timegm(currTime))
dateStr = strftime(timeFormat, currTime)
else:
currTime = datetime.datetime.strptime(dateStr, timeFormat)
secondsSinceEpoch = int(currTime.timestamp())
keyID = httpPrefix + '://' + domain + '/users/' + nickname + '#main-key'
if not messageBodyJsonStr:
headers = {
'*request-target': f'post {path}',
'*created': str(secondsSinceEpoch),
'host': toDomain,
'date': dateStr,
'content-type': 'application/json'
}
else:
bodyDigest = messageContentDigest(messageBodyJsonStr)
contentLength = len(messageBodyJsonStr)
headers = {
'*request-target': f'post {path}',
'*created': str(secondsSinceEpoch),
'host': toDomain,
'date': dateStr,
'digest': f'SHA-256={bodyDigest}',
'content-type': 'application/activity+json',
'content-length': str(contentLength)
}
key = load_pem_private_key(privateKeyPem.encode('utf-8'),
None, backend=default_backend())
# build a digest for signing
signedHeaderKeys = headers.keys()
signedHeaderText = ''
for headerKey in signedHeaderKeys:
signedHeaderText += f'{headerKey}: {headers[headerKey]}\n'
signedHeaderText = signedHeaderText.strip()
headerDigest = getSHA256(signedHeaderText.encode('ascii'))
# Sign the digest. Potentially other signing algorithms can be added here.
signature = ''
if algorithm == 'rsa-sha256':
rawSignature = key.sign(headerDigest,
padding.PKCS1v15(),
hazutils.Prehashed(hashes.SHA256()))
signature = base64.b64encode(rawSignature).decode('ascii')
sigKey = 'sig1'
# Put it into a valid HTTP signature format
signatureInputDict = {
'keyId': keyID,
}
signatureIndexHeader = '; '.join(
[f'{k}="{v}"' for k, v in signatureInputDict.items()])
signatureIndexHeader += '; alg=hs2019'
signatureIndexHeader += '; created=' + str(secondsSinceEpoch)
signatureIndexHeader += \
'; ' + sigKey + '=(' + ', '.join(signedHeaderKeys) + ')'
signatureDict = {
sigKey: signature
}
signatureHeader = '; '.join(
[f'{k}=:{v}:' for k, v in signatureDict.items()])
return signatureIndexHeader, signatureHeader
def createSignedHeader(privateKeyPem: str, nickname: str, def createSignedHeader(privateKeyPem: str, nickname: str,
domain: str, port: int, domain: str, port: int,
toDomain: str, toPort: int, toDomain: str, toPort: int,

152
tests.py
View File

@ -13,6 +13,7 @@ import json
from time import gmtime, strftime from time import gmtime, strftime
from pprint import pprint from pprint import pprint
from httpsig import signPostHeaders from httpsig import signPostHeaders
from httpsig import signPostHeadersNew
from httpsig import verifyPostHeaders from httpsig import verifyPostHeaders
from httpsig import messageContentDigest from httpsig import messageContentDigest
from cache import storePersonInCache from cache import storePersonInCache
@ -125,59 +126,59 @@ def testHttpSigNew():
'PSXSfBDiUGhwOw76WuSSsf1D4b/vLoJ10wIDAQAB\n' + \ 'PSXSfBDiUGhwOw76WuSSsf1D4b/vLoJ10wIDAQAB\n' + \
'-----END RSA PUBLIC KEY-----\n' '-----END RSA PUBLIC KEY-----\n'
# privKey = \ privateKeyPem = \
# '-----BEGIN RSA PRIVATE KEY-----\n' + \ '-----BEGIN RSA PRIVATE KEY-----\n' + \
# 'MIIEqAIBAAKCAQEAhAKYdtoeoy8zcAcR8' + \ 'MIIEqAIBAAKCAQEAhAKYdtoeoy8zcAcR8' + \
# '74L8cnZxKzAGwd7v36APp7Pv6Q2jdsP\n' + \ '74L8cnZxKzAGwd7v36APp7Pv6Q2jdsP\n' + \
# 'BRrwWEBnez6d0UDKDwGbc6nxfEXAy5mbh' + \ 'BRrwWEBnez6d0UDKDwGbc6nxfEXAy5mbh' + \
# 'gajzrw3MOEt8uA5txSKobBpKDeBLOsd\n' + \ 'gajzrw3MOEt8uA5txSKobBpKDeBLOsd\n' + \
# 'JKFqMGmXCQvEG7YemcxDTRPxAleIAgYYR' + \ 'JKFqMGmXCQvEG7YemcxDTRPxAleIAgYYR' + \
# 'jTSd/QBwVW9OwNFhekro3RtlinV0a75\n' + \ 'jTSd/QBwVW9OwNFhekro3RtlinV0a75\n' + \
# 'jfZgkne/YiktSvLG34lw2zqXBDTC5NHRO' + \ 'jfZgkne/YiktSvLG34lw2zqXBDTC5NHRO' + \
# 'UqGTlML4PlNZS5Ri2U4aCNx2rUPRcKI\n' + \ 'UqGTlML4PlNZS5Ri2U4aCNx2rUPRcKI\n' + \
# 'lE0PuKxI4T+HIaFpv8+rdV6eUgOrB2xeI' + \ 'lE0PuKxI4T+HIaFpv8+rdV6eUgOrB2xeI' + \
# '1dSFFn/nnv5OoZJEIB+VmuKn3DCUcCZ\n' + \ '1dSFFn/nnv5OoZJEIB+VmuKn3DCUcCZ\n' + \
# 'SFlQPSXSfBDiUGhwOw76WuSSsf1D4b/vL' + \ 'SFlQPSXSfBDiUGhwOw76WuSSsf1D4b/vL' + \
# 'oJ10wIDAQABAoIBAG/JZuSWdoVHbi56\n' + \ 'oJ10wIDAQABAoIBAG/JZuSWdoVHbi56\n' + \
# 'vjgCgkjg3lkO1KrO3nrdm6nrgA9P9qaPj' + \ 'vjgCgkjg3lkO1KrO3nrdm6nrgA9P9qaPj' + \
# 'xuKoWaKO1cBQlE1pSWp/cKncYgD5WxE\n' + \ 'xuKoWaKO1cBQlE1pSWp/cKncYgD5WxE\n' + \
# 'CpAnRUXG2pG4zdkzCYzAh1i+c34L6oZoH' + \ 'CpAnRUXG2pG4zdkzCYzAh1i+c34L6oZoH' + \
# 'sirK6oNcEnHveydfzJL5934egm6p8DW\n' + \ 'sirK6oNcEnHveydfzJL5934egm6p8DW\n' + \
# '+m1RQ70yUt4uRc0YSor+q1LGJvGQHReF0' + \ '+m1RQ70yUt4uRc0YSor+q1LGJvGQHReF0' + \
# 'WmJBZHrhz5e63Pq7lE0gIwuBqL8SMaA\n' + \ 'WmJBZHrhz5e63Pq7lE0gIwuBqL8SMaA\n' + \
# 'yRXtK+JGxZpImTq+NHvEWWCu09SCq0r83' + \ 'yRXtK+JGxZpImTq+NHvEWWCu09SCq0r83' + \
# '8ceQI55SvzmTkwqtC+8AT2zFviMZkKR\n' + \ '8ceQI55SvzmTkwqtC+8AT2zFviMZkKR\n' + \
# 'Qo6SPsrqItxZWRty2izawTF0Bf5S2VAx7' + \ 'Qo6SPsrqItxZWRty2izawTF0Bf5S2VAx7' + \
# 'O+6t3wBsQ1sLptoSgX3QblELY5asI0J\n' + \ 'O+6t3wBsQ1sLptoSgX3QblELY5asI0J\n' + \
# 'YFz7LJECgYkAsqeUJmqXE3LP8tYoIjMIA' + \ 'YFz7LJECgYkAsqeUJmqXE3LP8tYoIjMIA' + \
# 'KiTm9o6psPlc8CrLI9CH0UbuaA2JCOM\n' + \ 'KiTm9o6psPlc8CrLI9CH0UbuaA2JCOM\n' + \
# 'cCNq8SyYbTqgnWlB9ZfcAm/cFpA8tYci9' + \ 'cCNq8SyYbTqgnWlB9ZfcAm/cFpA8tYci9' + \
# 'm5vYK8HNxQr+8FS3Qo8N9RJ8d0U5Csw\n' + \ 'm5vYK8HNxQr+8FS3Qo8N9RJ8d0U5Csw\n' + \
# 'DzMYfRghAfUGwmlWj5hp1pQzAuhwbOXFt' + \ 'DzMYfRghAfUGwmlWj5hp1pQzAuhwbOXFt' + \
# 'xKHVsMPhz1IBtF9Y8jvgqgYHLbmyiu1\n' + \ 'xKHVsMPhz1IBtF9Y8jvgqgYHLbmyiu1\n' + \
# 'mwJ5AL0pYF0G7x81prlARURwHo0Yf52kE' + \ 'mwJ5AL0pYF0G7x81prlARURwHo0Yf52kE' + \
# 'w1dxpx+JXER7hQRWQki5/NsUEtv+8RT\n' + \ 'w1dxpx+JXER7hQRWQki5/NsUEtv+8RT\n' + \
# 'qn2m6qte5DXLyn83b1qRscSdnCCwKtKWU' + \ 'qn2m6qte5DXLyn83b1qRscSdnCCwKtKWU' + \
# 'ug5q2ZbwVOCJCtmRwmnP131lWRYfj67\n' + \ 'ug5q2ZbwVOCJCtmRwmnP131lWRYfj67\n' + \
# 'B/xJ1ZA6X3GEf4sNReNAtaucPEelgR2ns' + \ 'B/xJ1ZA6X3GEf4sNReNAtaucPEelgR2ns' + \
# 'N0gKQKBiGoqHWbK1qYvBxX2X3kbPDkv\n' + \ 'N0gKQKBiGoqHWbK1qYvBxX2X3kbPDkv\n' + \
# '9C+celgZd2PW7aGYLCHq7nPbmfDV0yHcW' + \ '9C+celgZd2PW7aGYLCHq7nPbmfDV0yHcW' + \
# 'jOhXZ8jRMjmANVR/eLQ2EfsRLdW69bn\n' + \ 'jOhXZ8jRMjmANVR/eLQ2EfsRLdW69bn\n' + \
# 'f3ZD7JS1fwGnO3exGmHO3HZG+6AvberKY' + \ 'f3ZD7JS1fwGnO3exGmHO3HZG+6AvberKY' + \
# 'VYNHahNFEw5TsAcQWDLRpkGybBcxqZo\n' + \ 'VYNHahNFEw5TsAcQWDLRpkGybBcxqZo\n' + \
# '81YCqlqidwfeO5YtlO7etx1xLyqa2NsCe' + \ '81YCqlqidwfeO5YtlO7etx1xLyqa2NsCe' + \
# 'G9A86UjG+aeNnXEIDk1PDK+EuiThIUa\n' + \ 'G9A86UjG+aeNnXEIDk1PDK+EuiThIUa\n' + \
# '/2IxKzJKWl1BKr2d4xAfR0ZnEYuRrbeDQ' + \ '/2IxKzJKWl1BKr2d4xAfR0ZnEYuRrbeDQ' + \
# 'YgTImOlfW6/GuYIxKYgEKCFHFqJATAG\n' + \ 'YgTImOlfW6/GuYIxKYgEKCFHFqJATAG\n' + \
# 'IxHrq1PDOiSwXd2GmVVYyEmhZnbcp8Cxa' + \ 'IxHrq1PDOiSwXd2GmVVYyEmhZnbcp8Cxa' + \
# 'EMQoevxAta0ssMK3w6UsDtvUvYvF22m\n' + \ 'EMQoevxAta0ssMK3w6UsDtvUvYvF22m\n' + \
# 'qQKBiD5GwESzsFPy3Ga0MvZpn3D6EJQLg' + \ 'qQKBiD5GwESzsFPy3Ga0MvZpn3D6EJQLg' + \
# 'snrtUPZx+z2Ep2x0xc5orneB5fGyF1P\n' + \ 'snrtUPZx+z2Ep2x0xc5orneB5fGyF1P\n' + \
# 'WtP+fG5Q6Dpdz3LRfm+KwBCWFKQjg7uTx' + \ 'WtP+fG5Q6Dpdz3LRfm+KwBCWFKQjg7uTx' + \
# 'cjerhBWEYPmEMKYwTJF5PBG9/ddvHLQ\n' + \ 'cjerhBWEYPmEMKYwTJF5PBG9/ddvHLQ\n' + \
# 'EQeNC8fHGg4UXU8mhHnSBt3EA10qQJfRD' + \ 'EQeNC8fHGg4UXU8mhHnSBt3EA10qQJfRD' + \
# 's15M38eG2cYwB1PZpDHScDnDA0=\n' + \ 's15M38eG2cYwB1PZpDHScDnDA0=\n' + \
# '-----END RSA PRIVATE KEY-----' '-----END RSA PRIVATE KEY-----'
sigInput = \ sigInput = \
'sig1=(date); alg=rsa-sha256; keyId="test-key-b"' 'sig1=(date); alg=rsa-sha256; keyId="test-key-b"'
sig = \ sig = \
@ -199,7 +200,8 @@ def testHttpSigNew():
# 'CaB8X/I5/+HLZLGvDiezqi6/7p2Gngf5hwZ0lSdy39vyNMaaAT0tKo6nuVw0S1MVg' + \ # 'CaB8X/I5/+HLZLGvDiezqi6/7p2Gngf5hwZ0lSdy39vyNMaaAT0tKo6nuVw0S1MVg' + \
# '1Q7MpWYZs0soHjttq0uLIA3DIbQfLiIvK6/l0BdWTU7+2uQj7lBkQAsFZHoA96ZZg' + \ # '1Q7MpWYZs0soHjttq0uLIA3DIbQfLiIvK6/l0BdWTU7+2uQj7lBkQAsFZHoA96ZZg' + \
# 'FquQrXRlmYOh+Hx5D9fJkXcXe5tmAg==:' # 'FquQrXRlmYOh+Hx5D9fJkXcXe5tmAg==:'
boxpath = '/foo' nickname = 'foo'
boxpath = '/' + nickname
# headers = { # headers = {
# "*request-target": "get " + boxpath, # "*request-target": "get " + boxpath,
# "*created": "1402170695", # "*created": "1402170695",
@ -214,11 +216,14 @@ def testHttpSigNew():
# "Signature-Input": sigInput, # "Signature-Input": sigInput,
# "Signature": sig # "Signature": sig
# } # }
dateStr = "Tue, 07 Jun 2014 20:51:35 GMT"
domain = "example.com"
port = 443
headers = { headers = {
"*created": "1402170695", "*created": "1402170695",
"*request-target": "post /foo?param=value&pet=dog", "*request-target": "post /foo?param=value&pet=dog",
"host": "example.com", "host": domain,
"date": "Tue, 07 Jun 2014 20:51:35 GMT", "date": dateStr,
"content-type": "application/json", "content-type": "application/json",
"digest": "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", "digest": "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
"content-length": "18", "content-length": "18",
@ -231,6 +236,41 @@ def testHttpSigNew():
boxpath, False, None, boxpath, False, None,
messageBodyJsonStr, debug, messageBodyJsonStr, debug,
True) 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-length": str(contentLength)
}
signatureIndexHeader, signatureHeader = \
signPostHeadersNew(dateStr, privateKeyPem, nickname,
domain, port,
domain, port,
boxpath, httpPrefix, messageBodyJsonStr,
'rsa-sha256')
assert signatureIndexHeader == \
'keyId="https://example.com/users/foo#main-key"; ' + \
'alg=hs2019; created=1402170695; ' + \
'sig1=(*request-target, *created, host, date, ' + \
'digest, content-type, content-length)'
assert signatureHeader == \
'sig1=:LQU1PcJILSp1Q30GWINusfftYYKfTtam7InSu2c+ZzfGC' + \
'bTSevRgifZFuG2asFi8ubG/uUVHiBwIxxIz1u/JyWC3lYIFgjQF' + \
'RFM6As2b/ytnMA0LQhNebvk05iUNsz5izSoNTp5h9J7+roWkl6l' + \
'8d5EA7vPMTQTJZnyU1cXBlvP1MtuVAKR6MbB3Aa/iZ4XOeaNK5E' + \
'1VuPfNFrdnizIELE3nGVoVqNNImgMY3DWhtF3vvezrcT0J2vNGZ' + \
'cvhBfgn/xeAsNxz67SIHMgiXvLL6TFqEI1en9dl9A3ihB6ZO6+W' + \
'gUoW7OobZNlPxAUkQCc2A6oVjCYOdpKdrMAXQp2TQQ==:'
def _testHttpsigBase(withDigest): def _testHttpsigBase(withDigest):