2020-06-15 12:37:53 +00:00
|
|
|
__filename__ = "jsonldsig.py"
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
__credits__ = ['Based on ' +
|
|
|
|
'https://github.com/WebOfTrustInfo/ld-signatures-python']
|
|
|
|
__license__ = "AGPL3+"
|
|
|
|
__version__ = "1.1.0"
|
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
__email__ = "bob@freedombone.net"
|
|
|
|
__status__ = "Production"
|
|
|
|
|
|
|
|
from copy import deepcopy
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
import pytz
|
|
|
|
|
|
|
|
try:
|
|
|
|
from Cryptodome.PublicKey import RSA
|
|
|
|
from Cryptodome.Hash import SHA256
|
2020-06-15 12:52:46 +00:00
|
|
|
from Cryptodome.Signature import pkcs1_5 as PKCS1_v1_5
|
2020-06-15 12:37:53 +00:00
|
|
|
except ImportError:
|
|
|
|
from Crypto.PublicKey import RSA
|
|
|
|
from Crypto.Hash import SHA256
|
|
|
|
from Crypto.Signature import PKCS1_v1_5
|
|
|
|
|
2020-10-11 15:18:48 +00:00
|
|
|
from pyjsonld import normalize
|
2020-06-15 12:37:53 +00:00
|
|
|
|
|
|
|
import base64
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _b64safeEncode(payload: {}) -> str:
|
2020-06-15 12:37:53 +00:00
|
|
|
"""
|
|
|
|
b64 url safe encoding with the padding removed.
|
|
|
|
"""
|
|
|
|
return base64.urlsafe_b64encode(payload).rstrip(b'=')
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _b64safeDecode(payload: {}) -> str:
|
2020-06-15 12:37:53 +00:00
|
|
|
"""
|
|
|
|
b64 url safe decoding with the padding added.
|
|
|
|
"""
|
|
|
|
return base64.urlsafe_b64decode(payload + b'=' * (4 - len(payload) % 4))
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _normalizeJson(payload: {}) -> str:
|
2020-06-15 21:37:10 +00:00
|
|
|
"""
|
|
|
|
Normalize with URDNA2015
|
|
|
|
"""
|
2020-06-15 12:37:53 +00:00
|
|
|
return json.dumps(payload, separators=(',', ':'),
|
|
|
|
sort_keys=True).encode('utf-8')
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _signRs256(payload: {}, privateKeyPem: str) -> str:
|
2020-06-15 12:37:53 +00:00
|
|
|
"""
|
|
|
|
Produce a RS256 signature of the payload
|
|
|
|
"""
|
2020-06-15 21:34:32 +00:00
|
|
|
key = RSA.importKey(privateKeyPem)
|
2020-06-15 12:37:53 +00:00
|
|
|
signer = PKCS1_v1_5.new(key)
|
|
|
|
signature = signer.sign(SHA256.new(payload))
|
|
|
|
return signature
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _verifyRs256(payload: {}, signature: str, publicKeyPem: str) -> bool:
|
2020-06-15 12:37:53 +00:00
|
|
|
"""
|
|
|
|
Verifies a RS256 signature
|
|
|
|
"""
|
2020-06-15 12:52:46 +00:00
|
|
|
key = RSA.importKey(publicKeyPem)
|
2020-06-15 12:37:53 +00:00
|
|
|
verifier = PKCS1_v1_5.new(key)
|
|
|
|
return verifier.verify(SHA256.new(payload), signature)
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _signJws(payload: {}, privateKeyPem: str) -> str:
|
2020-06-15 21:34:32 +00:00
|
|
|
"""
|
|
|
|
Prepare payload to sign
|
2020-06-15 12:37:53 +00:00
|
|
|
"""
|
|
|
|
header = {
|
|
|
|
'alg': 'RS256',
|
|
|
|
'b64': False,
|
|
|
|
'crit': ['b64']
|
|
|
|
}
|
2020-12-22 18:06:23 +00:00
|
|
|
normalizedJson = _normalizeJson(header)
|
|
|
|
encodedHeader = _b64safeEncode(normalizedJson)
|
2020-06-15 12:37:53 +00:00
|
|
|
preparedPayload = b'.'.join([encodedHeader, payload])
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
signature = _signRs256(preparedPayload, privateKeyPem)
|
|
|
|
encodedSignature = _b64safeEncode(signature)
|
2020-06-15 12:37:53 +00:00
|
|
|
jwsSignature = b'..'.join([encodedHeader, encodedSignature])
|
|
|
|
|
|
|
|
return jwsSignature
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _verifyJws(payload: {}, jwsSignature: str, publicKeyPem: str) -> bool:
|
2020-06-15 21:34:32 +00:00
|
|
|
"""
|
|
|
|
Verifies a signature using the given public key
|
|
|
|
"""
|
2020-06-15 12:52:46 +00:00
|
|
|
encodedHeader, encodedSignature = jwsSignature.split(b'..')
|
2020-12-22 18:06:23 +00:00
|
|
|
signature = _b64safeDecode(encodedSignature)
|
2020-06-15 12:37:53 +00:00
|
|
|
payload = b'.'.join([encodedHeader, payload])
|
2020-12-22 18:06:23 +00:00
|
|
|
return _verifyRs256(payload, signature, publicKeyPem)
|
2020-06-15 12:37:53 +00:00
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _jsonldNormalize(jldDocument: str):
|
2020-06-15 12:37:53 +00:00
|
|
|
"""
|
|
|
|
Normalize and hash the json-ld document
|
|
|
|
"""
|
|
|
|
options = {
|
|
|
|
'algorithm': 'URDNA2015',
|
|
|
|
'format': 'application/nquads'
|
|
|
|
}
|
2020-10-11 15:18:48 +00:00
|
|
|
normalized = normalize(jldDocument, options=options)
|
2020-06-15 12:37:53 +00:00
|
|
|
normalizedHash = SHA256.new(data=normalized.encode('utf-8')).digest()
|
|
|
|
return normalizedHash
|
|
|
|
|
|
|
|
|
|
|
|
def jsonldSign(jldDocument: {}, privateKeyPem: str) -> {}:
|
|
|
|
"""
|
|
|
|
Produces a signed JSON-LD document with a Json Web Signature
|
|
|
|
"""
|
|
|
|
jldDocument = deepcopy(jldDocument)
|
2020-12-22 18:06:23 +00:00
|
|
|
normalizedJldHash = _jsonldNormalize(jldDocument)
|
|
|
|
jwsSignature = _signJws(normalizedJldHash, privateKeyPem)
|
2020-06-15 12:37:53 +00:00
|
|
|
|
|
|
|
# construct the signature document and add it to jsonld
|
|
|
|
signature = {
|
|
|
|
'type': 'RsaSignatureSuite2017',
|
|
|
|
'created': datetime.now(tz=pytz.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
|
|
'signatureValue': jwsSignature.decode('utf-8')
|
|
|
|
}
|
|
|
|
jldDocument.update({'signature': signature})
|
|
|
|
|
|
|
|
return jldDocument
|
|
|
|
|
|
|
|
|
|
|
|
def jsonldVerify(signedJldDocument: {}, publicKeyPem: str) -> bool:
|
|
|
|
"""
|
|
|
|
Verifies the Json Web Signature of a signed JSON-LD Document
|
|
|
|
"""
|
|
|
|
signedJldDocument = deepcopy(signedJldDocument)
|
|
|
|
signature = signedJldDocument.pop('signature')
|
|
|
|
jwsSignature = signature['signatureValue'].encode('utf-8')
|
2020-12-22 18:06:23 +00:00
|
|
|
normalizedJldHash = _jsonldNormalize(signedJldDocument)
|
2020-06-15 12:37:53 +00:00
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
return _verifyJws(normalizedJldHash, jwsSignature, publicKeyPem)
|
2020-06-15 12:37:53 +00:00
|
|
|
|
|
|
|
|
2020-06-15 20:32:57 +00:00
|
|
|
def testSignJsonld(jldDocument: {}, privateKeyPem: str) -> {}:
|
2020-06-15 21:37:10 +00:00
|
|
|
"""
|
|
|
|
Creates a test signature
|
2020-06-15 20:32:57 +00:00
|
|
|
"""
|
2020-06-15 12:37:53 +00:00
|
|
|
signedJldDocument = jsonldSign(jldDocument, privateKeyPem)
|
2020-06-15 20:32:57 +00:00
|
|
|
|
2020-06-15 12:37:53 +00:00
|
|
|
# pop the created time key since its dynamic
|
|
|
|
signedJldDocument['signature'].pop('created')
|
|
|
|
|
2020-06-15 20:32:57 +00:00
|
|
|
return signedJldDocument
|