mirror of https://gitlab.com/bashrc2/epicyon
Support for new style of http signatures
parent
56e9130287
commit
3d1c440584
84
httpsig.py
84
httpsig.py
|
@ -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
152
tests.py
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue