pyvckit/sign_vc.py

175 lines
5.5 KiB
Python

import json
import hashlib
import multicodec
import multiformats
import nacl.signing
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
def sign(document, key, issuer_did):
_did = issuer_did + "#" + issuer_did.split("did:key:")[1]
proof = {
'@context':'https://w3id.org/security/v2',
'type': 'Ed25519Signature2018',
'proofPurpose': 'assertionMethod',
'verificationMethod': _did,
'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()
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)
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)
print(json.dumps(vc, separators=(',', ':')))