epicyon/linked_data_sig.py

119 lines
3.6 KiB
Python
Raw Normal View History

__filename__ = "linked_data_sig.py"
__author__ = "Bob Mottram"
__credits__ = ['Based on ' +
'https://github.com/tsileo/little-boxes']
__license__ = "AGPL3+"
2024-01-21 19:01:20 +00:00
__version__ = "1.5.0"
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
__status__ = "Production"
2021-06-15 15:08:12 +00:00
__module_group__ = "Security"
import random
import base64
import hashlib
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
from pyjsonld import normalize
2021-12-29 21:55:09 +00:00
from context import has_valid_context
2021-12-26 12:13:46 +00:00
from utils import get_sha_256
2023-11-20 22:27:58 +00:00
from utils import date_utcnow
2021-01-05 17:27:11 +00:00
def _options_hash(doc: {}) -> str:
"""Returns a hash of the signature, with a few fields removed
"""
2022-01-02 21:48:09 +00:00
doc_sig = dict(doc["signature"])
2021-01-05 17:27:11 +00:00
# remove fields from signature
2022-01-02 21:48:09 +00:00
for key in ["type", "id", "signatureValue"]:
if key in doc_sig:
del doc_sig[key]
2021-01-05 17:27:11 +00:00
2022-01-02 21:48:09 +00:00
doc_sig["@context"] = "https://w3id.org/identity/v1"
options = {
"algorithm": "URDNA2015",
"format": "application/nquads"
}
2021-01-05 17:27:11 +00:00
2022-01-02 21:48:09 +00:00
normalized = normalize(doc_sig, options)
hsh = hashlib.new("sha256")
hsh.update(normalized.encode("utf-8"))
return hsh.hexdigest()
2021-01-05 17:27:11 +00:00
def _doc_hash(doc: {}) -> str:
"""Returns a hash of the ActivityPub post
"""
doc = dict(doc)
2021-01-05 17:27:11 +00:00
# remove the signature
if "signature" in doc:
del doc["signature"]
2021-01-05 17:27:11 +00:00
options = {
"algorithm": "URDNA2015",
"format": "application/nquads"
}
2021-01-05 17:27:11 +00:00
normalized = normalize(doc, options)
2022-01-02 21:48:09 +00:00
hsh = hashlib.new("sha256")
hsh.update(normalized.encode("utf-8"))
return hsh.hexdigest()
2022-01-02 21:48:09 +00:00
def verify_json_signature(doc: {}, public_key_pem: str) -> bool:
2021-01-05 17:27:11 +00:00
"""Returns True if the given ActivityPub post was sent
by an actor having the given public key
"""
2021-12-29 21:55:09 +00:00
if not has_valid_context(doc):
return False
2022-01-02 21:48:09 +00:00
pubkey = load_pem_public_key(public_key_pem.encode('utf-8'),
backend=default_backend())
to_be_signed = _options_hash(doc) + _doc_hash(doc)
signature = doc["signature"]["signatureValue"]
2021-12-26 12:13:46 +00:00
digest = get_sha_256(to_be_signed.encode("utf-8"))
2021-01-05 17:27:11 +00:00
base64sig = base64.b64decode(signature)
try:
pubkey.verify(
base64sig,
digest,
padding.PKCS1v15(),
hazutils.Prehashed(hashes.SHA256()))
return True
2022-05-30 18:33:51 +00:00
except BaseException as ex:
print('EX: verify_json_signature unable to verify ' + str(ex))
return False
2022-01-02 21:48:09 +00:00
def generate_json_signature(doc: {}, private_key_pem: str) -> None:
2021-01-05 17:27:11 +00:00
"""Adds a json signature to the given ActivityPub post
"""
if not doc.get('actor'):
return
2021-12-29 21:55:09 +00:00
if not has_valid_context(doc):
return
options = {
"type": "RsaSignature2017",
"nonce": '%030x' % random.randrange(16**64),
"creator": doc["actor"] + "#main-key",
2023-11-20 22:27:58 +00:00
"created": date_utcnow().replace(microsecond=0).isoformat() + "Z",
}
doc["signature"] = options
to_be_signed = _options_hash(doc) + _doc_hash(doc)
2022-01-02 21:48:09 +00:00
key = load_pem_private_key(private_key_pem.encode('utf-8'),
None, backend=default_backend())
2021-12-26 12:13:46 +00:00
digest = get_sha_256(to_be_signed.encode("utf-8"))
signature = key.sign(digest,
padding.PKCS1v15(),
hazutils.Prehashed(hashes.SHA256()))
sig = base64.b64encode(signature)
options["signatureValue"] = sig.decode("utf-8")