2021-01-04 19:02:24 +00:00
|
|
|
__filename__ = "linked_data_sig.py"
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
__credits__ = ['Based on ' +
|
|
|
|
'https://github.com/tsileo/little-boxes']
|
|
|
|
__license__ = "AGPL3+"
|
2021-01-26 10:07:42 +00:00
|
|
|
__version__ = "1.2.0"
|
2021-01-04 19:02:24 +00:00
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
__email__ = "bob@freedombone.net"
|
|
|
|
__status__ = "Production"
|
2021-06-15 15:08:12 +00:00
|
|
|
__module_group__ = "Security"
|
2021-01-04 19:02:24 +00:00
|
|
|
|
|
|
|
import base64
|
|
|
|
import hashlib
|
|
|
|
from datetime import datetime
|
2021-02-04 18:18:31 +00:00
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
|
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
|
|
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
|
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
|
|
from cryptography.hazmat.primitives import hashes
|
|
|
|
from cryptography.hazmat.primitives.asymmetric import utils as hazutils
|
2021-01-04 19:02:24 +00:00
|
|
|
from pyjsonld import normalize
|
2021-01-05 20:11:16 +00:00
|
|
|
from context import hasValidContext
|
2021-02-04 18:18:31 +00:00
|
|
|
from utils import getSHA256
|
2021-01-04 19:02:24 +00:00
|
|
|
|
|
|
|
|
2021-01-05 17:27:11 +00:00
|
|
|
def _options_hash(doc: {}) -> str:
|
|
|
|
"""Returns a hash of the signature, with a few fields removed
|
|
|
|
"""
|
|
|
|
docSig = dict(doc["signature"])
|
|
|
|
|
|
|
|
# remove fields from signature
|
2021-01-04 19:02:24 +00:00
|
|
|
for k in ["type", "id", "signatureValue"]:
|
2021-01-05 17:27:11 +00:00
|
|
|
if k in docSig:
|
|
|
|
del docSig[k]
|
|
|
|
|
|
|
|
docSig["@context"] = "https://w3id.org/identity/v1"
|
2021-01-04 19:02:24 +00:00
|
|
|
options = {
|
|
|
|
"algorithm": "URDNA2015",
|
|
|
|
"format": "application/nquads"
|
|
|
|
}
|
2021-01-05 17:27:11 +00:00
|
|
|
|
|
|
|
normalized = normalize(docSig, options)
|
2021-01-04 19:02:24 +00:00
|
|
|
h = hashlib.new("sha256")
|
|
|
|
h.update(normalized.encode("utf-8"))
|
|
|
|
return h.hexdigest()
|
|
|
|
|
|
|
|
|
2021-01-05 17:27:11 +00:00
|
|
|
def _doc_hash(doc: {}) -> str:
|
|
|
|
"""Returns a hash of the ActivityPub post
|
|
|
|
"""
|
2021-01-04 19:02:24 +00:00
|
|
|
doc = dict(doc)
|
2021-01-05 17:27:11 +00:00
|
|
|
|
|
|
|
# remove the signature
|
2021-01-04 19:02:24 +00:00
|
|
|
if "signature" in doc:
|
|
|
|
del doc["signature"]
|
2021-01-05 17:27:11 +00:00
|
|
|
|
2021-01-04 19:02:24 +00:00
|
|
|
options = {
|
|
|
|
"algorithm": "URDNA2015",
|
|
|
|
"format": "application/nquads"
|
|
|
|
}
|
2021-01-05 17:27:11 +00:00
|
|
|
|
2021-01-04 19:02:24 +00:00
|
|
|
normalized = normalize(doc, options)
|
|
|
|
h = hashlib.new("sha256")
|
|
|
|
h.update(normalized.encode("utf-8"))
|
|
|
|
return h.hexdigest()
|
|
|
|
|
|
|
|
|
2021-01-05 17:27:11 +00:00
|
|
|
def verifyJsonSignature(doc: {}, publicKeyPem: str) -> bool:
|
|
|
|
"""Returns True if the given ActivityPub post was sent
|
|
|
|
by an actor having the given public key
|
|
|
|
"""
|
2021-01-05 20:11:16 +00:00
|
|
|
if not hasValidContext(doc):
|
|
|
|
return False
|
2021-02-04 18:18:31 +00:00
|
|
|
pubkey = load_pem_public_key(publicKeyPem.encode('utf-8'),
|
|
|
|
backend=default_backend())
|
2021-01-04 19:02:24 +00:00
|
|
|
to_be_signed = _options_hash(doc) + _doc_hash(doc)
|
|
|
|
signature = doc["signature"]["signatureValue"]
|
2021-02-04 18:18:31 +00:00
|
|
|
|
|
|
|
digest = getSHA256(to_be_signed.encode("utf-8"))
|
2021-01-05 17:27:11 +00:00
|
|
|
base64sig = base64.b64decode(signature)
|
2021-02-04 18:18:31 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
pubkey.verify(
|
|
|
|
base64sig,
|
|
|
|
digest,
|
|
|
|
padding.PKCS1v15(),
|
|
|
|
hazutils.Prehashed(hashes.SHA256()))
|
|
|
|
return True
|
|
|
|
except BaseException:
|
|
|
|
return False
|
2021-01-04 19:02:24 +00:00
|
|
|
|
|
|
|
|
2021-01-05 17:27:11 +00:00
|
|
|
def generateJsonSignature(doc: {}, privateKeyPem: str) -> None:
|
|
|
|
"""Adds a json signature to the given ActivityPub post
|
|
|
|
"""
|
2021-01-04 19:02:24 +00:00
|
|
|
if not doc.get('actor'):
|
|
|
|
return
|
2021-01-05 20:11:16 +00:00
|
|
|
if not hasValidContext(doc):
|
|
|
|
return
|
2021-01-04 19:02:24 +00:00
|
|
|
options = {
|
|
|
|
"type": "RsaSignature2017",
|
|
|
|
"creator": doc["actor"] + "#main-key",
|
|
|
|
"created": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
|
|
|
}
|
|
|
|
doc["signature"] = options
|
|
|
|
to_be_signed = _options_hash(doc) + _doc_hash(doc)
|
|
|
|
|
2021-02-04 18:18:31 +00:00
|
|
|
key = load_pem_private_key(privateKeyPem.encode('utf-8'),
|
|
|
|
None, backend=default_backend())
|
|
|
|
digest = getSHA256(to_be_signed.encode("utf-8"))
|
|
|
|
signature = key.sign(digest,
|
|
|
|
padding.PKCS1v15(),
|
|
|
|
hazutils.Prehashed(hashes.SHA256()))
|
|
|
|
sig = base64.b64encode(signature)
|
2021-01-04 19:02:24 +00:00
|
|
|
options["signatureValue"] = sig.decode("utf-8")
|