refactor sign VCs

This commit is contained in:
Cayo Puigdefabregas 2024-05-24 13:39:50 +02:00
parent 9690b606e0
commit 5bc11525f0
6 changed files with 130 additions and 168 deletions

64
sign.py Normal file
View File

@ -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

View File

@ -1,174 +1,39 @@
import json import json
import hashlib from utils import now
import multicodec from did import generate_keys, generate_did, get_signing_key
import multiformats from templates import credential_tmpl, proof_tmpl
import nacl.signing from sign import sign_proof
import nacl.encoding
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 # 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] _did = issuer_did + "#" + issuer_did.split("did:key:")[1]
proof = { proof = proof_tmpl.copy()
'@context':'https://w3id.org/security/v2', proof['verificationMethod'] = _did
'type': 'Ed25519Signature2018', proof['created'] = now()
'proofPurpose': 'assertionMethod',
'verificationMethod': _did,
'created': now()
}
sign_proof(document, proof, key) sign_proof(document, proof, key)
del proof['@context'] del proof['@context']
document['proof'] = proof document['proof'] = proof
return document return document
if __name__ == "__main__": def main():
# Generate an Ed25519 key pair key = generate_keys()
key = jwk.JWK.generate(kty='OKP', crv='Ed25519') did = generate_did(key)
key['kid'] = 'Generated' signing_key = get_signing_key(key)
# key = key_read()
jwk_pr = key.export_private(True) credential = credential_tmpl.copy()
private_key_material_str = jwk_pr['d'] credential["issuer"] = did
missing_padding = len(private_key_material_str) % 4 credential["issuanceDate"] = now()
if missing_padding: cred = json.dumps(credential)
private_key_material_str += '=' * (4 - missing_padding)
private_key_material = nacl.encoding.URLSafeBase64Encoder.decode(private_key_material_str) vc = sign(cred, signing_key, did)
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)
print(json.dumps(vc, separators=(',', ':'))) print(json.dumps(vc, separators=(',', ':')))
if __name__ == "__main__":
main()

29
templates.py Normal file
View File

@ -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": []
}

View File

@ -3,17 +3,12 @@ import multicodec
import multiformats import multiformats
import nacl.encoding import nacl.encoding
from datetime import datetime, timezone from did import generate_keys, generate_did, get_signing_key
from did_generate import generate_keys, generate_did, get_signing_key
from sign_vc import sign from sign_vc import sign
from sign_vp import sign_vp from sign_vp import sign_vp
from verify_vc import verify_vc from verify_vc import verify_vc
from verify_vp import verify_vp from verify_vp import verify_vp
from utils import now
def now():
timestamp = datetime.now(timezone.utc).replace(microsecond=0)
formatted_timestamp = timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
return formatted_timestamp
def test_generated_did_key(): def test_generated_did_key():
@ -37,6 +32,7 @@ def test_generated_did_key():
def test_credential(): def test_credential():
# import pdb; pdb.set_trace()
key = generate_keys() key = generate_keys()
did = generate_did(key) did = generate_did(key)
signing_key = get_signing_key(key) signing_key = get_signing_key(key)
@ -52,7 +48,9 @@ def test_credential():
"issuanceDate": now() "issuanceDate": now()
} }
vc = sign(credential, signing_key, did) cred = json.dumps(credential)
vc = sign(cred, signing_key, did)
header = 'eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9' header = 'eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9'
assert vc.get('proof', {}).get('jws') is not None assert vc.get('proof', {}).get('jws') is not None
assert header in vc.get('proof', {}).get('jws') assert header in vc.get('proof', {}).get('jws')
@ -75,7 +73,9 @@ def test_presentation():
"issuanceDate": now() "issuanceDate": now()
} }
vc = sign(credential, signing_key, did) cred = json.dumps(credential)
vc = sign(cred, signing_key, did)
vc_json = json.dumps(vc) vc_json = json.dumps(vc)
holder_key = generate_keys() holder_key = generate_keys()
@ -104,7 +104,9 @@ def test_verifiable_credential():
"issuanceDate": now() "issuanceDate": now()
} }
vc = sign(credential, signing_key, did) cred = json.dumps(credential)
vc = sign(cred, signing_key, did)
verified = verify_vc(vc) verified = verify_vc(vc)
assert verified assert verified
@ -125,7 +127,9 @@ def test_verifiable_presentation():
"issuanceDate": now() "issuanceDate": now()
} }
vc = sign(credential, signing_key, did) cred = json.dumps(credential)
vc = sign(cred, signing_key, did)
vc_json = json.dumps(vc) vc_json = json.dumps(vc)
holder_key = generate_keys() holder_key = generate_keys()

View File