| 
									
										
										
										
											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+" | 
					
						
							| 
									
										
										
										
											2024-01-21 19:01:20 +00:00
										 |  |  | __version__ = "1.5.0" | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  | __maintainer__ = "Bob Mottram" | 
					
						
							| 
									
										
										
										
											2021-09-10 16:14:50 +00:00
										 |  |  | __email__ = "bob@libreserver.org" | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  | __status__ = "Production" | 
					
						
							| 
									
										
										
										
											2021-06-15 15:08:12 +00:00
										 |  |  | __module_group__ = "Security" | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-10 16:02:36 +00:00
										 |  |  | import random | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  | import base64 | 
					
						
							|  |  |  | import hashlib | 
					
						
							| 
									
										
										
										
											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-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-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
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											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" | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  |     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-04 19:02:24 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							| 
									
										
										
										
											2022-01-02 21:48:09 +00:00
										 |  |  |     hsh = hashlib.new("sha256") | 
					
						
							|  |  |  |     hsh.update(normalized.encode("utf-8")) | 
					
						
							|  |  |  |     return hsh.hexdigest() | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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): | 
					
						
							| 
									
										
										
										
											2021-01-05 20:11:16 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2022-01-02 21:48:09 +00:00
										 |  |  |     pubkey = load_pem_public_key(public_key_pem.encode('utf-8'), | 
					
						
							| 
									
										
										
										
											2021-02-04 18:18:31 +00:00
										 |  |  |                                  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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							| 
									
										
										
										
											2021-02-04 18:18:31 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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)) | 
					
						
							| 
									
										
										
										
											2021-02-04 18:18:31 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  |     if not doc.get('actor'): | 
					
						
							|  |  |  |         return | 
					
						
							| 
									
										
										
										
											2021-12-29 21:55:09 +00:00
										 |  |  |     if not has_valid_context(doc): | 
					
						
							| 
									
										
										
										
											2021-01-05 20:11:16 +00:00
										 |  |  |         return | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  |     options = { | 
					
						
							|  |  |  |         "type": "RsaSignature2017", | 
					
						
							| 
									
										
										
										
											2022-05-10 16:02:36 +00:00
										 |  |  |         "nonce": '%030x' % random.randrange(16**64), | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  |         "creator": doc["actor"] + "#main-key", | 
					
						
							| 
									
										
										
										
											2023-11-20 22:27:58 +00:00
										 |  |  |         "created": date_utcnow().replace(microsecond=0).isoformat() + "Z", | 
					
						
							| 
									
										
										
										
											2021-01-04 19:02:24 +00:00
										 |  |  |     } | 
					
						
							|  |  |  |     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'), | 
					
						
							| 
									
										
										
										
											2021-02-04 18:18:31 +00:00
										 |  |  |                                None, backend=default_backend()) | 
					
						
							| 
									
										
										
										
											2021-12-26 12:13:46 +00:00
										 |  |  |     digest = get_sha_256(to_be_signed.encode("utf-8")) | 
					
						
							| 
									
										
										
										
											2021-02-04 18:18:31 +00:00
										 |  |  |     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") |