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 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()

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 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()

View File