refactor sign VCs
This commit is contained in:
parent
9690b606e0
commit
5bc11525f0
|
@ -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
|
179
sign_vc.py
179
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()
|
||||
|
|
|
@ -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": []
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue