From 5bc11525f0aeff8f517648ca6874960a5e7adff3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 24 May 2024 13:39:50 +0200 Subject: [PATCH] refactor sign VCs --- did_generate.py => did.py | 0 sign.py | 64 ++++++++++++++ sign_vc.py | 179 +++++--------------------------------- templates.py | 29 ++++++ tests/test_certificate.py | 26 +++--- util.py => utils.py | 0 6 files changed, 130 insertions(+), 168 deletions(-) rename did_generate.py => did.py (100%) create mode 100644 sign.py create mode 100644 templates.py rename util.py => utils.py (100%) diff --git a/did_generate.py b/did.py similarity index 100% rename from did_generate.py rename to did.py diff --git a/sign.py b/sign.py new file mode 100644 index 0000000..bd94eac --- /dev/null +++ b/sign.py @@ -0,0 +1,64 @@ +import hashlib +import nacl.signing +import nacl.encoding +from pyld import jsonld + + +# https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L75 +def sign_bytes(data, secret): + # https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L125 + return secret.sign(data)[:-len(data)] + + +# https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L248 +def sign_bytes_b64(data, key): + signature = sign_bytes(data, key) + sig_b64 = nacl.encoding.URLSafeBase64Encoder.encode(signature) + return sig_b64 + + + # https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L581 +def detached_sign_unencoded_payload(payload, key): + header = b'{"alg":"EdDSA","crit":["b64"],"b64":false}' + header_b64 = nacl.encoding.URLSafeBase64Encoder.encode(header) + signing_input = header_b64 + b"." + payload + sig_b64 = sign_bytes_b64(signing_input, key) + jws = header_b64 + b".." + sig_b64 + return jws + + +# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L423 +def urdna2015_normalize(document, proof): + doc_dataset = jsonld.compact(document, "https://www.w3.org/2018/credentials/v1") + sigopts_dataset = jsonld.compact(proof, "https://w3id.org/security/v2") + doc_normalized = jsonld.normalize( + doc_dataset, + {'algorithm': 'URDNA2015', 'format': 'application/n-quads'} + ) + sigopts_normalized = jsonld.normalize( + sigopts_dataset, + {'algorithm': 'URDNA2015', 'format': 'application/n-quads'} + ) + return doc_normalized, sigopts_normalized + + +# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L456 +def sha256_normalized(doc_normalized, sigopts_normalized): + doc_digest = hashlib.sha256(doc_normalized.encode('utf-8')).digest() + sigopts_digest = hashlib.sha256(sigopts_normalized.encode('utf-8')).digest() + message = sigopts_digest + doc_digest + return message + + +# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L413 +def to_jws_payload(document, proof): + doc_normalized, sigopts_normalized = urdna2015_normalize(document, proof) + return sha256_normalized(doc_normalized, sigopts_normalized) + + +# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L498 +def sign_proof(document, proof, key): + message = to_jws_payload(document, proof) + jws = detached_sign_unencoded_payload(message, key) + proof["jws"] = jws.decode('utf-8')[:-2] + return proof diff --git a/sign_vc.py b/sign_vc.py index 719fe37..9ac79b2 100644 --- a/sign_vc.py +++ b/sign_vc.py @@ -1,174 +1,39 @@ import json -import hashlib -import multicodec -import multiformats -import nacl.signing -import nacl.encoding +from utils import now +from did import generate_keys, generate_did, get_signing_key +from templates import credential_tmpl, proof_tmpl +from sign import sign_proof -from pyld import jsonld -from jwcrypto import jwk -from nacl.public import PublicKey -from nacl.signing import SigningKey -from collections import OrderedDict -from nacl.encoding import RawEncoder -from datetime import datetime, timezone - -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -# For signature -from pyld.jsonld import JsonLdProcessor - - -_debug = False - - -def now(): - timestamp = datetime.now(timezone.utc).replace(microsecond=0) - formatted_timestamp = timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") - return formatted_timestamp - - -def key_to_did(public_key_bytes): - """did-key-format := - did:key:MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))""" - - #public_key_bytes = public_key.encode() - mc = multicodec.add_prefix('ed25519-pub', public_key_bytes) - - # Multibase encode the hashed bytes - did = multiformats.multibase.encode(mc, 'base58btc') - - return f"did:key:{did}" - - -def key_save(key): - # Save the private JWK to a file - private_jwk = key.export() - with open('keypairs.jwk', 'w') as f: - f.write(private_jwk) - - -def key_read(): - # Save the private JWK to a file - with open('keypairs.jwk', 'r') as f: - private_jwk = f.read() - - return jwk.JWK.from_json(private_jwk) - - -# https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L75 -def sign_bytes(data, secret): - # https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L125 - return secret.sign(data)[:-len(data)] - - -# https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L248 -def sign_bytes_b64(data, key): - signature = sign_bytes(data, key) - sig_b64 = nacl.encoding.URLSafeBase64Encoder.encode(signature) - return sig_b64 - - - # https://github.com/spruceid/ssi/blob/main/ssi-jws/src/lib.rs#L581 -def detached_sign_unencoded_payload(payload, key): - header = b'{"alg":"EdDSA","crit":["b64"],"b64":false}' - header_b64 = nacl.encoding.URLSafeBase64Encoder.encode(header) - signing_input = header_b64 + b"." + payload - sig_b64 = sign_bytes_b64(signing_input, key) - jws = header_b64 + b".." + sig_b64 - return jws - - -# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L423 -def urdna2015_normalize(document, proof): - doc_dataset = jsonld.compact(document, "https://www.w3.org/2018/credentials/v1") - sigopts_dataset = jsonld.compact(proof, "https://w3id.org/security/v2") - doc_normalized = jsonld.normalize( - doc_dataset, - {'algorithm': 'URDNA2015', 'format': 'application/n-quads'} - ) - sigopts_normalized = jsonld.normalize( - sigopts_dataset, - {'algorithm': 'URDNA2015', 'format': 'application/n-quads'} - ) - return doc_normalized, sigopts_normalized - - -# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L456 -def sha256_normalized(doc_normalized, sigopts_normalized): - doc_digest = hashlib.sha256(doc_normalized.encode('utf-8')).digest() - sigopts_digest = hashlib.sha256(sigopts_normalized.encode('utf-8')).digest() - message = sigopts_digest + doc_digest - return message - - -# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L413 -def to_jws_payload(document, proof): - doc_normalized, sigopts_normalized = urdna2015_normalize(document, proof) - return sha256_normalized(doc_normalized, sigopts_normalized) - - -# https://github.com/spruceid/ssi/blob/main/ssi-ldp/src/lib.rs#L498 -def sign_proof(document, proof, key): - message = to_jws_payload(document, proof) - jws = detached_sign_unencoded_payload(message, key) - proof["jws"] = jws.decode('utf-8')[:-2] - return proof # source: https://github.com/mmlab-aueb/PyEd25519Signature2018/blob/master/signer.py -def sign(document, key, issuer_did): +def sign(credential, key, issuer_did): + document = json.loads(credential) _did = issuer_did + "#" + issuer_did.split("did:key:")[1] - proof = { - '@context':'https://w3id.org/security/v2', - 'type': 'Ed25519Signature2018', - 'proofPurpose': 'assertionMethod', - 'verificationMethod': _did, - 'created': now() - } + proof = proof_tmpl.copy() + proof['verificationMethod'] = _did + proof['created'] = now() + sign_proof(document, proof, key) del proof['@context'] document['proof'] = proof return document -if __name__ == "__main__": - # Generate an Ed25519 key pair - key = jwk.JWK.generate(kty='OKP', crv='Ed25519') - key['kid'] = 'Generated' - # key = key_read() +def main(): + key = generate_keys() + did = generate_did(key) + signing_key = get_signing_key(key) - jwk_pr = key.export_private(True) - private_key_material_str = jwk_pr['d'] - missing_padding = len(private_key_material_str) % 4 - if missing_padding: - private_key_material_str += '=' * (4 - missing_padding) + credential = credential_tmpl.copy() + credential["issuer"] = did + credential["issuanceDate"] = now() + cred = json.dumps(credential) - private_key_material = nacl.encoding.URLSafeBase64Encoder.decode(private_key_material_str) - signing_key = SigningKey(private_key_material, encoder=RawEncoder) - verify_key = signing_key.verify_key - public_key_bytes = verify_key.encode() - - # Generate the DID - did = key_to_did(public_key_bytes) - # print(did) - - credential = { - "@context": "https://www.w3.org/2018/credentials/v1", - "id": "http://example.org/credentials/3731", - "type": ["VerifiableCredential"], - "credentialSubject": { - "id": "did:key:z6MkgGXSJoacuuNdwU1rGfPpFH72GACnzykKTxzCCTZs6Z2M", - }, - "issuer": did, - "issuanceDate": now() - } - - # vc = generate_vc(credential, signing_key, did) - vc = sign(credential, signing_key, did) + vc = sign(cred, signing_key, did) print(json.dumps(vc, separators=(',', ':'))) + +if __name__ == "__main__": + main() diff --git a/templates.py b/templates.py new file mode 100644 index 0000000..853c9e7 --- /dev/null +++ b/templates.py @@ -0,0 +1,29 @@ +# templates + + +credential_tmpl = { + "@context": "https://www.w3.org/2018/credentials/v1", + "id": "http://example.org/credentials/3731", + "type": ["VerifiableCredential"], + "credentialSubject": { + "id": "did:key:z6MkgGXSJoacuuNdwU1rGfPpFH72GACnzykKTxzCCTZs6Z2M", + }, + "issuer": None, + "issuanceDate": None +} + +proof_tmpl = { + '@context':'https://w3id.org/security/v2', + 'type': 'Ed25519Signature2018', + 'proofPurpose': 'assertionMethod', + 'verificationMethod': None, + 'created': None +} + +presentation_tmpl = { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "id": "http://example.org/presentations/3731", + "type": ["VerifiablePresentation"], + "holder": "", + "verifiableCredential": [] +} diff --git a/tests/test_certificate.py b/tests/test_certificate.py index 2a66f6a..99491de 100644 --- a/tests/test_certificate.py +++ b/tests/test_certificate.py @@ -3,17 +3,12 @@ import multicodec import multiformats import nacl.encoding -from datetime import datetime, timezone -from did_generate import generate_keys, generate_did, get_signing_key +from did import generate_keys, generate_did, get_signing_key from sign_vc import sign from sign_vp import sign_vp from verify_vc import verify_vc from verify_vp import verify_vp - -def now(): - timestamp = datetime.now(timezone.utc).replace(microsecond=0) - formatted_timestamp = timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") - return formatted_timestamp +from utils import now def test_generated_did_key(): @@ -37,6 +32,7 @@ def test_generated_did_key(): def test_credential(): + # import pdb; pdb.set_trace() key = generate_keys() did = generate_did(key) signing_key = get_signing_key(key) @@ -52,7 +48,9 @@ def test_credential(): "issuanceDate": now() } - vc = sign(credential, signing_key, did) + cred = json.dumps(credential) + + vc = sign(cred, signing_key, did) header = 'eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9' assert vc.get('proof', {}).get('jws') is not None assert header in vc.get('proof', {}).get('jws') @@ -75,7 +73,9 @@ def test_presentation(): "issuanceDate": now() } - vc = sign(credential, signing_key, did) + cred = json.dumps(credential) + + vc = sign(cred, signing_key, did) vc_json = json.dumps(vc) holder_key = generate_keys() @@ -104,7 +104,9 @@ def test_verifiable_credential(): "issuanceDate": now() } - vc = sign(credential, signing_key, did) + cred = json.dumps(credential) + + vc = sign(cred, signing_key, did) verified = verify_vc(vc) assert verified @@ -125,7 +127,9 @@ def test_verifiable_presentation(): "issuanceDate": now() } - vc = sign(credential, signing_key, did) + cred = json.dumps(credential) + + vc = sign(cred, signing_key, did) vc_json = json.dumps(vc) holder_key = generate_keys() diff --git a/util.py b/utils.py similarity index 100% rename from util.py rename to utils.py