From bef62d19e9b72add3cc227396aa74163786d2728 Mon Sep 17 00:00:00 2001 From: Harald Roessler Date: Wed, 1 Apr 2026 15:35:31 +0700 Subject: [PATCH] fix: use RFC 8785 JCS canonicalization for VC signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace json.dumps(sort_keys=True) with jcs.canonicalize() (RFC 8785) for credential signing in credentials.py and endorsement.py. json.dumps(sort_keys=True) is not a proper canonicalization — it does not guarantee consistent output for unicode normalization, floating point representation, or key ordering in nested structures. JCS (JSON Canonicalization Scheme, RFC 8785) provides deterministic serialization. New credentials include canonicalizationAlgorithm: "JCS" in the proof. Verification checks this field and falls back to sort_keys for legacy credentials, ensuring backward compatibility. Note: hash computation functions (sports, signals, fantasy, interaction proofs) are intentionally unchanged — migrating those would invalidate existing commitment hashes stored in the database. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/credentials.py | 14 +++++++++++--- app/swarm/endorsement.py | 5 +++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/credentials.py b/app/credentials.py index b59818e..fe35005 100644 --- a/app/credentials.py +++ b/app/credentials.py @@ -1,5 +1,6 @@ """MolTrust Verifiable Credentials - W3C VC Data Model""" import os, json, datetime, hashlib +import jcs from nacl.signing import SigningKey from app.crypto.kms_signer import get_decrypted_signing_key_hex @@ -27,7 +28,7 @@ def issue_credential(subject_did: str, credential_type: str, claims: dict) -> di } signing_key = get_signing_key() - payload = json.dumps(credential, sort_keys=True).encode() + payload = jcs.canonicalize(credential) signed = signing_key.sign(payload) credential["proof"] = { @@ -35,6 +36,7 @@ def issue_credential(subject_did: str, credential_type: str, claims: dict) -> di "created": now.isoformat() + "Z", "verificationMethod": f"{ISSUER_DID}#key-1", "proofPurpose": "assertionMethod", + "canonicalizationAlgorithm": "JCS", "proofValue": signed.signature.hex() } return credential @@ -48,12 +50,18 @@ def verify_credential(credential: dict) -> dict: try: cred_copy = {k: v for k, v in credential.items() if k != "proof"} - payload = json.dumps(cred_copy, sort_keys=True).encode() signature = bytes.fromhex(proof["proofValue"]) signing_key = get_signing_key() verify_key = signing_key.verify_key - verify_key.verify(payload, signature) + + # Try JCS first (new credentials), fall back to sort_keys (legacy) + if proof.get("canonicalizationAlgorithm") == "JCS": + payload = jcs.canonicalize(cred_copy) + verify_key.verify(payload, signature) + else: + payload = json.dumps(cred_copy, sort_keys=True).encode() + verify_key.verify(payload, signature) exp = credential.get("expirationDate", "") if exp: diff --git a/app/swarm/endorsement.py b/app/swarm/endorsement.py index 5407b79..8cb6f16 100644 --- a/app/swarm/endorsement.py +++ b/app/swarm/endorsement.py @@ -153,15 +153,16 @@ async def issue_endorsement( # 11. Ed25519 Signatur (HIGH-5: real signing, no more sandbox_unsigned) from app.credentials import get_signing_key - import json as _json + import jcs as _jcs signing_key = get_signing_key() - payload = _json.dumps(vc, sort_keys=True).encode() + payload = _jcs.canonicalize(vc) signed = signing_key.sign(payload) vc["proof"] = { "type": "Ed25519Signature2020", "created": now.isoformat(), "verificationMethod": "did:web:api.moltrust.ch#key-1", "proofPurpose": "assertionMethod", + "canonicalizationAlgorithm": "JCS", "proofValue": signed.signature.hex() }