From 1054277beefa66203229c621fda378051a98ef16 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Mon, 22 Sep 2025 07:44:06 -0500 Subject: [PATCH 1/8] Refactor print statements for consistency and clarity in example scripts This commit updates the print statements across multiple example scripts, including `cddl_validation.py`, `cose_sign1_demo.py`, `cose_thumbprint.py`, `minimal_api_usage.py`, `sd_cwt_specification_example.py`, `validate_cbor.py`, and others. The changes focus on removing unnecessary whitespace and ensuring consistent formatting, enhancing the readability and maintainability of the code. Additionally, some imports are reorganized for better clarity. --- examples/cddl_validation.py | 69 +++++++++--------- examples/cose_sign1_demo.py | 8 ++- examples/cose_thumbprint.py | 47 ++++++------- examples/minimal_api_usage.py | 3 +- examples/sd_cwt_specification_example.py | 90 ++++++++++++------------ examples/validate_cbor.py | 51 +++++++------- 6 files changed, 133 insertions(+), 135 deletions(-) diff --git a/examples/cddl_validation.py b/examples/cddl_validation.py index d7a9e24..0b25c57 100644 --- a/examples/cddl_validation.py +++ b/examples/cddl_validation.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """Example demonstrating CDDL validation and CBOR EDN for SD-CWT and COSE Keys.""" -import base64 import hashlib from sd_cwt import cbor_utils, edn_utils @@ -14,7 +13,7 @@ def demonstrate_cbor_edn(): print("=" * 60) print("CBOR Extended Diagnostic Notation (EDN) Demo") print("=" * 60) - + # 1. SD-CWT Claims in EDN print("\n1. SD-CWT Claims in EDN:") sd_cwt_edn = ''' @@ -28,19 +27,19 @@ def demonstrate_cbor_edn(): ] / redacted_claim_keys (simple value 59) / } ''' - + # Remove comments for parsing import re clean_edn = re.sub(r'/[^/]*/|#[^\n]*', '', sd_cwt_edn) - + print(" EDN representation:") print(" " + clean_edn.strip().replace("\n", "\n ")) - + # Convert to CBOR cbor_data = edn_utils.diag_to_cbor(clean_edn) print(f"\n CBOR hex: {cbor_data.hex()[:60]}...") print(f" CBOR size: {len(cbor_data)} bytes") - + # 2. COSE Key in EDN print("\n2. COSE EC2 Key in EDN:") ec2_key_edn = ''' @@ -52,11 +51,11 @@ def demonstrate_cbor_edn(): -3: h'20138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e' } ''' - + clean_edn = re.sub(r'/[^/]*/', '', ec2_key_edn) print(" EDN representation:") print(" " + clean_edn.strip().replace("\n", "\n ")) - + # 3. Disclosure Array in EDN print("\n3. Disclosure Array in EDN:") disclosure_edn = '[h\'73616c74\', "BATCH-2024-001", "batch_id"]' # SD-CWT format: [salt, value, key] @@ -73,7 +72,7 @@ def demonstrate_cddl_validation(): print("\n" + "=" * 60) print("CDDL Schema Validation Demo") print("=" * 60) - + # Create a valid SD-CWT claims structure claims = { 1: "https://issuer.example.com", # iss @@ -84,21 +83,21 @@ def demonstrate_cddl_validation(): hashlib.sha256(b"disclosure2").digest(), ], } - + print("\n1. SD-CWT Claims Structure:") print(f" Issuer: {claims[1]}") print(f" Subject: {claims[2]}") print(f" Issued At: {claims[6]}") print(f" Redacted Claim Keys: {len(claims[59])} hashes") - + # Convert to CBOR cbor_data = cbor_utils.encode(claims) - + # Convert to EDN for display edn = edn_utils.cbor_to_diag(cbor_data) print("\n2. CBOR Diagnostic Notation:") print(f" {edn[:200]}...") - + # Try CDDL validation with zcbor print("\n3. CDDL Validation:") try: @@ -118,7 +117,7 @@ def demonstrate_cose_key_validation(): print("\n" + "=" * 60) print("COSE Key Structure Validation Demo") print("=" * 60) - + # Create different COSE key types keys = { "EC2": { @@ -146,18 +145,18 @@ def demonstrate_cose_key_validation(): -1: b"k" * 32, # key value }, } - + for key_type, key in keys.items(): print(f"\n{key_type} Key:") - + # Compute thumbprint (excludes optional fields) thumbprint = CoseKeyThumbprint.compute(key, "sha256") print(f" Thumbprint: {thumbprint.hex()[:32]}...") - + # Show canonical structure canonical = CoseKeyThumbprint.canonical_cbor(key) print(f" Canonical CBOR size: {len(canonical)} bytes") - + # Convert to EDN edn = edn_utils.cbor_to_diag(canonical) print(f" Canonical EDN: {edn[:100]}...") @@ -168,7 +167,7 @@ def demonstrate_test_vectors(): print("\n" + "=" * 60) print("Test Vector Structure Demo") print("=" * 60) - + # Create a test vector test_vector = { "description": "EC2 P-256 key thumbprint test", @@ -190,25 +189,25 @@ def demonstrate_test_vectors(): "thumbprint_uri": None, # Will be computed }, } - + # Compute actual values key = test_vector["input"]["key"] thumbprint = CoseKeyThumbprint.compute(key, "sha256") uri = CoseKeyThumbprint.uri(key, "sha256") - + test_vector["output"]["thumbprint"] = thumbprint test_vector["output"]["thumbprint_uri"] = uri - + print("\n1. Test Vector:") print(f" Description: {test_vector['description']}") print(f" Key Type: EC2 (kty={key[1]})") print(f" Curve: P-256 (crv={key[-1]})") print(f" Hash Algorithm: {test_vector['input']['hash_alg']}") - + print("\n2. Output:") print(f" Thumbprint: {thumbprint.hex()}") print(f" URI: {uri}") - + # Convert to CBOR and show size cbor_data = cbor_utils.encode(test_vector) print(f"\n3. Encoded size: {len(cbor_data)} bytes") @@ -238,29 +237,29 @@ def demonstrate_real_world_example(): # Select claims for selective disclosure (sensitive business info) sd_claims = ["inspector_license_number", "product_batch_id", "facility_location"] - + print("\n1. Original Claims:") for k, v in all_claims.items(): if isinstance(v, dict): print(f" {k}: ") else: print(f" {k}: {v}") - + # Create disclosures disclosures = [] sd_hashes = [] - + for claim_name in sd_claims: if claim_name in all_claims: salt = hashlib.sha256(f"salt_{claim_name}".encode()).digest()[:16] disclosure = [salt, all_claims[claim_name], claim_name] # SD-CWT format: [salt, value, key] disclosures.append(disclosure) - + # Hash the disclosure disclosure_cbor = cbor_utils.encode(disclosure) sd_hash = hashlib.sha256(disclosure_cbor).digest() sd_hashes.append(sd_hash) - + # Create SD-CWT claims (without disclosed claims) sd_cwt_claims = { 1: all_claims["iss"], # iss @@ -268,16 +267,16 @@ def demonstrate_real_world_example(): 6: all_claims["iat"], # iat 59: sd_hashes, # redacted_claim_keys (simple value 59) } - + print(f"\n2. SD-CWT Claims (after removing {len(sd_claims)} claims):") print(f" Issuer: {sd_cwt_claims[1]}") print(f" Subject: {sd_cwt_claims[2]}") print(f" Redacted Claim Keys: {len(sd_cwt_claims[59])} hashes") - + print("\n3. Disclosures Created:") for i, disclosure in enumerate(disclosures): print(f" #{i+1}: [{len(disclosure[0])} bytes salt, '{disclosure[1]}', ...]") - + # Entity selects claims to reveal to supply chain partner revealed = ["inspector_license_number", "inspection_date"] @@ -291,17 +290,17 @@ def main(): """Run all demonstrations.""" print("\nCDDL and CBOR EDN Validation Examples") print("=" * 60) - + demonstrate_cbor_edn() demonstrate_cddl_validation() demonstrate_cose_key_validation() demonstrate_test_vectors() demonstrate_real_world_example() - + print("\n" + "=" * 60) print("Demo completed!") print("=" * 60) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/cose_sign1_demo.py b/examples/cose_sign1_demo.py index ee0d272..c27bbff 100644 --- a/examples/cose_sign1_demo.py +++ b/examples/cose_sign1_demo.py @@ -2,14 +2,16 @@ """Demo script for COSE Sign1 functionality.""" import json + from sd_cwt import ( CoseAlgorithm, cose_key_generate, - cose_key_to_dict, cose_key_get_public, + cose_key_to_dict, cose_sign1_sign, cose_sign1_verify, ) + # Import internal implementations for demo purposes from sd_cwt.cose_sign1 import ( ES256Signer, @@ -161,7 +163,7 @@ def demo_external_aad(): payload = b"Sensitive data" external_aad = b"context-12345" - print(f"\n1. Signing with external AAD:") + print("\n1. Signing with external AAD:") print(f" Payload: {payload.decode()}") print(f" External AAD: {external_aad.decode()}") @@ -209,4 +211,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/cose_thumbprint.py b/examples/cose_thumbprint.py index 81096f9..ed279d1 100644 --- a/examples/cose_thumbprint.py +++ b/examples/cose_thumbprint.py @@ -2,7 +2,6 @@ """Example demonstrating COSE Key Thumbprint computation (RFC 9679).""" import base64 -import json from sd_cwt.thumbprint import CoseKeyThumbprint @@ -12,7 +11,7 @@ def demonstrate_ec2_thumbprint(): print("=" * 60) print("EC2 (P-256) Key Thumbprint Demo") print("=" * 60) - + # Sample EC2 key ec2_key = { 1: 2, # kty: EC2 @@ -22,24 +21,24 @@ def demonstrate_ec2_thumbprint(): -3: base64.b64decode("IBOL+C3BttVivg+lSreASjpkttcsz+1rb7btKLv8EX4="), # y "kid": "example-key-1", # Also excluded from thumbprint } - + print("\n1. Original COSE Key (with optional fields):") print(f" Key ID: {ec2_key.get('kid')}") print(f" Algorithm: {ec2_key.get(3)} (ES256)") print(f" Key Type: {ec2_key[1]} (EC2)") print(f" Curve: {ec2_key[-1]} (P-256)") - + # Compute thumbprint with SHA-256 thumbprint_sha256 = CoseKeyThumbprint.compute(ec2_key, "sha256") print("\n2. SHA-256 Thumbprint:") print(f" Hex: {thumbprint_sha256.hex()}") print(f" Base64url: {base64.urlsafe_b64encode(thumbprint_sha256).rstrip(b'=').decode()}") - + # Compute thumbprint URI uri = CoseKeyThumbprint.uri(ec2_key, "sha256") print("\n3. Thumbprint URI:") print(f" {uri}") - + # Show that optional fields don't affect thumbprint ec2_key_minimal = { 1: 2, # kty @@ -47,7 +46,7 @@ def demonstrate_ec2_thumbprint(): -2: ec2_key[-2], # x -3: ec2_key[-3], # y } - + thumbprint_minimal = CoseKeyThumbprint.compute(ec2_key_minimal, "sha256") print("\n4. Verification - Same thumbprint without optional fields:") print(f" Thumbprints match: {thumbprint_sha256 == thumbprint_minimal}") @@ -58,7 +57,7 @@ def demonstrate_okp_thumbprint(): print("\n" + "=" * 60) print("OKP (Ed25519) Key Thumbprint Demo") print("=" * 60) - + # Sample OKP key okp_key = { 1: 1, # kty: OKP @@ -66,16 +65,16 @@ def demonstrate_okp_thumbprint(): -1: 6, # crv: Ed25519 -2: base64.b64decode("11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="), # x } - + print("\n1. OKP Key:") print(f" Key Type: {okp_key[1]} (OKP)") print(f" Curve: {okp_key[-1]} (Ed25519)") - + # Compute thumbprint thumbprint = CoseKeyThumbprint.compute(okp_key, "sha256") print("\n2. SHA-256 Thumbprint:") print(f" Hex: {thumbprint.hex()}") - + # URI format uri = CoseKeyThumbprint.uri(okp_key, "sha256") print("\n3. Thumbprint URI:") @@ -87,15 +86,15 @@ def demonstrate_multiple_hash_algorithms(): print("\n" + "=" * 60) print("Multiple Hash Algorithms Demo") print("=" * 60) - + # Simple symmetric key symmetric_key = { 1: 4, # kty: Symmetric -1: base64.b64decode("hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG+Onbc6mxCcYg="), # k } - + print("\n1. Symmetric Key Thumbprints:") - + for hash_alg in ["sha256", "sha384", "sha512"]: thumbprint = CoseKeyThumbprint.compute(symmetric_key, hash_alg) uri = CoseKeyThumbprint.uri(symmetric_key, hash_alg) @@ -110,7 +109,7 @@ def demonstrate_canonical_cbor(): print("\n" + "=" * 60) print("Canonical CBOR Encoding Demo") print("=" * 60) - + # EC2 key with fields in non-canonical order ec2_key = { -3: b"y_coordinate", # y @@ -120,24 +119,24 @@ def demonstrate_canonical_cbor(): -1: 1, # crv "extra": "ignored", # Extra field (will be excluded) } - + print("\n1. Original key order:") print(f" Fields: {list(ec2_key.keys())}") - + # Get canonical CBOR canonical = CoseKeyThumbprint.canonical_cbor(ec2_key) - + print("\n2. Canonical encoding:") print(f" CBOR hex: {canonical.hex()}") print(f" Length: {len(canonical)} bytes") - + # Compute thumbprint import hashlib thumbprint = hashlib.sha256(canonical).digest() - + print("\n3. Resulting thumbprint:") print(f" SHA-256: {thumbprint.hex()}") - + # Show it's deterministic canonical2 = CoseKeyThumbprint.canonical_cbor(ec2_key) print("\n4. Deterministic result:") @@ -148,16 +147,16 @@ def main(): """Run all demonstrations.""" print("\nCOSE Key Thumbprint (RFC 9679) Examples") print("=" * 60) - + demonstrate_ec2_thumbprint() demonstrate_okp_thumbprint() demonstrate_multiple_hash_algorithms() demonstrate_canonical_cbor() - + print("\n" + "=" * 60) print("Demo completed!") print("=" * 60) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/minimal_api_usage.py b/examples/minimal_api_usage.py index 506d3bc..626ad38 100644 --- a/examples/minimal_api_usage.py +++ b/examples/minimal_api_usage.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """Example of using the minimal public API for COSE Sign1.""" -from typing import Any import sd_cwt @@ -87,4 +86,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/sd_cwt_specification_example.py b/examples/sd_cwt_specification_example.py index d5c4547..a3695b7 100644 --- a/examples/sd_cwt_specification_example.py +++ b/examples/sd_cwt_specification_example.py @@ -1,4 +1,5 @@ from sd_cwt import edn_utils + #!/usr/bin/env python3 """SD-CWT example matching the latest specification exactly. @@ -8,12 +9,11 @@ import hashlib import secrets -from typing import Dict, Any import cbor2 from fido2.cose import CoseKey -from sd_cwt.issuer import SDCWTIssuer, create_example_edn_claims +from sd_cwt.issuer import SDCWTIssuer def demonstrate_specification_example(): @@ -21,7 +21,7 @@ def demonstrate_specification_example(): print("=" * 80) print("SD-CWT Specification Example (Latest Draft)") print("=" * 80) - + # 1. Show the specification example in EDN print("\n1. Specification CWT Claims in EDN:") spec_edn = ''' @@ -49,16 +49,16 @@ def demonstrate_specification_example(): } } ''' - + print(spec_edn) - + # 2. Parse and display the claims print("\n2. Parsed Claims Structure:") # Remove comments properly import re clean_edn = re.sub(r'/[^/]*/', '', spec_edn) clean_edn = re.sub(r'#[^\n]*', '', clean_edn).strip() - + # Create clean version without comments for parsing clean_spec = ''' { @@ -85,10 +85,10 @@ def demonstrate_specification_example(): } } ''' - + cbor_data = edn_utils.diag_to_cbor(clean_spec) claims = cbor2.loads(cbor_data) - + print(f" Issuer (1): {claims[1]}") print(f" Subject (2): {claims[2]}") print(f" Expiration (4): {claims[4]}") @@ -98,8 +98,8 @@ def demonstrate_specification_example(): print(f" Device ID (501): {claims[501]}") print(f" Timestamps (502): {len(claims[502])} timestamps") print(f" Address (503): {claims[503]['country']}, {claims[503]['region']}") - print(f" Confirmation Key (8): EC2 P-256") - + print(" Confirmation Key (8): EC2 P-256") + return claims @@ -108,7 +108,7 @@ def demonstrate_sd_cwt_with_redaction(): print("\n" + "=" * 80) print("SD-CWT with Selective Disclosure") print("=" * 80) - + # 1. Create EDN with redaction tags print("\n1. EDN Claims with Redaction Tags:") edn_with_redaction = ''' @@ -126,9 +126,9 @@ def demonstrate_sd_cwt_with_redaction(): } } ''' - + print(edn_with_redaction) - + # 2. Create signing key print("\n2. Creating Signing Key...") signing_key_data = { @@ -140,11 +140,11 @@ def demonstrate_sd_cwt_with_redaction(): } signing_key = CoseKey(signing_key_data) print(" ✓ EC2 P-256 signing key created") - + # 3. Create issuer issuer = SDCWTIssuer(signing_key, "https://issuer.example") print(" ✓ SD-CWT issuer initialized") - + # 4. Parse claims (without actual redaction tags for now) simple_edn = ''' { @@ -161,32 +161,30 @@ def demonstrate_sd_cwt_with_redaction(): } } ''' - + print("\n3. Creating SD-CWT...") try: result = issuer.create_sd_cwt(simple_edn) print(" ✓ SD-CWT created successfully") print(f" ✓ SD-CWT size: {len(result['sd_cwt'])} bytes") print(f" ✓ Disclosures: {len(result['disclosures'])}") - + # Show SD-CWT structure print("\n4. SD-CWT Structure Analysis:") sd_cwt_tag = cbor2.loads(result['sd_cwt']) print(f" CBOR Tag: {sd_cwt_tag.tag} (COSE_Sign1)") - + cose_sign1 = sd_cwt_tag.value print(f" COSE_Sign1 elements: {len(cose_sign1)}") - + # Show payload payload = cbor2.loads(cose_sign1[2]) print(f" Payload claims: {len(payload)}") - + for key, value in payload.items(): - if isinstance(key, int) and key < 100: + if isinstance(key, int) and key < 100 or isinstance(key, str): print(f" {key}: {str(value)[:50]}{'...' if len(str(value)) > 50 else ''}") - elif isinstance(key, str): - print(f" {key}: {str(value)[:50]}{'...' if len(str(value)) > 50 else ''}") - + except Exception as e: print(f" ⚠ Error creating SD-CWT: {e}") @@ -196,28 +194,28 @@ def demonstrate_disclosure_creation(): print("\n" + "=" * 80) print("Manual Disclosure Creation") print("=" * 80) - + # Create sample disclosures print("\n1. Creating Sample Disclosures:") - + disclosures = [] claims_to_disclose = [ ("device_enabled", True), ("timestamps", [1549560720, 1612498440, 1674004740]), ("address", {"country": "us", "region": "ca", "postal_code": "94188"}) ] - + for claim_name, claim_value in claims_to_disclose: # Generate 128-bit salt salt = secrets.token_bytes(16) - + # Create disclosure array disclosure_array = [salt, claim_name, claim_value] disclosure_cbor = cbor2.dumps(disclosure_array) - + # Hash the disclosure disclosure_hash = hashlib.sha256(disclosure_cbor).digest() - + disclosures.append({ "name": claim_name, "value": claim_value, @@ -225,17 +223,17 @@ def demonstrate_disclosure_creation(): "disclosure": disclosure_cbor, "hash": disclosure_hash }) - + print(f" ✓ {claim_name}: {len(disclosure_cbor)} bytes CBOR") print(f" Salt: {salt.hex()[:16]}...") print(f" Hash: {disclosure_hash.hex()[:16]}...") - + # Show EDN representation of disclosures print("\n2. Disclosure Arrays in EDN:") for disc in disclosures: edn = edn_utils.cbor_to_diag(disc["disclosure"]) print(f" {disc['name']}: {edn}") - + # Create SD-CWT claims with hashes print("\n3. SD-CWT Claims with Disclosure Hashes:") sd_cwt_claims = { @@ -245,7 +243,7 @@ def demonstrate_disclosure_creation(): 501: "ABCD-123456", # This claim remains visible 59: [disc["hash"] for disc in disclosures], # redacted_claim_keys } - + edn = edn_utils.cbor_to_diag(cbor2.dumps(sd_cwt_claims)) print(f" {edn[:200]}...") @@ -255,12 +253,12 @@ def demonstrate_presentation(): print("\n" + "=" * 80) print("SD-CWT Presentation (Holder -> Verifier)") print("=" * 80) - + print("\n1. Holder's Decision:") print(" Available disclosures: device_enabled, timestamps, address") print(" Holder chooses to reveal: device_enabled, address") print(" Holder keeps private: timestamps") - + print("\n2. Presentation Structure:") presentation = { "sd_cwt": "base64url-encoded-sd-cwt-token", @@ -269,13 +267,13 @@ def demonstrate_presentation(): "base64url-encoded-address-disclosure" ] } - + for key, value in presentation.items(): print(f" {key}: {value}") - + print("\n3. Verifier Process:") print(" ✓ Verify SD-CWT signature") - print(" ✓ Check timestamp claims (exp, nbf, iat)") + print(" ✓ Check timestamp claims (exp, nbf, iat)") print(" ✓ Decode provided disclosures") print(" ✓ Hash disclosures and match against redacted_claim_keys") print(" ✓ Construct verified claims") @@ -286,24 +284,24 @@ def main(): print("SD-CWT Implementation Examples") print("Using latest draft-ietf-spice-sd-cwt specification") print("=" * 80) - + try: # Show specification example demonstrate_specification_example() - + # Show SD-CWT with redaction demonstrate_sd_cwt_with_redaction() - + # Show manual disclosure creation demonstrate_disclosure_creation() - + # Show presentation flow demonstrate_presentation() - + print("\n" + "=" * 80) print("✓ All demonstrations completed successfully!") print("=" * 80) - + except Exception as e: print(f"\n❌ Error during demonstration: {e}") import traceback @@ -311,4 +309,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/validate_cbor.py b/examples/validate_cbor.py index 8fef1db..5e03781 100644 --- a/examples/validate_cbor.py +++ b/examples/validate_cbor.py @@ -1,4 +1,5 @@ from sd_cwt import edn_utils + #!/usr/bin/env python3 """Example script demonstrating CBOR and CDDL validation for SD-CWT.""" @@ -8,7 +9,7 @@ import cbor2 -from sd_cwt.validation import CBORValidator, CDDLValidator, SDCWTValidator +from sd_cwt.validation import CBORValidator, SDCWTValidator def create_sample_sd_cwt(): @@ -16,16 +17,16 @@ def create_sample_sd_cwt(): # Create disclosure for selective disclosure disclosure1 = [b"salt1234567890", "given_name", "Alice"] disclosure2 = [b"salt0987654321", "family_name", "Smith"] - + # Hash disclosures hash1 = base64.urlsafe_b64encode( hashlib.sha256(cbor2.dumps(disclosure1)).digest() ).rstrip(b"=").decode() - + hash2 = base64.urlsafe_b64encode( hashlib.sha256(cbor2.dumps(disclosure2)).digest() ).rstrip(b"=").decode() - + # Create SD-CWT claims claims = { "iss": "https://issuer.example.com", @@ -34,7 +35,7 @@ def create_sample_sd_cwt(): "email": "alice@example.com", # Not selectively disclosed 59: [hash1, hash2] # redacted_claim_keys (simple value 59) } - + return cbor2.dumps(claims), [disclosure1, disclosure2] @@ -43,9 +44,9 @@ def demonstrate_cbor_validation(): print("=" * 60) print("CBOR Validation Demo") print("=" * 60) - + validator = CBORValidator() - + # Create sample data sample_data = { "string": "hello", @@ -53,19 +54,19 @@ def demonstrate_cbor_validation(): "array": [1, 2, 3], "nested": {"key": "value"} } - + cbor_data = cbor2.dumps(sample_data) - + # Validate structure print("\n1. Validating CBOR structure:") is_valid = validator.validate_structure(cbor_data) print(f" Valid: {is_valid}") - + # Convert to diagnostic notation print("\n2. CBOR Diagnostic notation:") diag = validator.to_diagnostic(cbor_data) print(f" {diag}") - + # Test invalid CBOR print("\n3. Testing invalid CBOR:") invalid_cbor = b"not valid cbor" @@ -78,12 +79,12 @@ def demonstrate_sd_cwt_validation(): print("\n" + "=" * 60) print("SD-CWT Validation Demo") print("=" * 60) - + validator = SDCWTValidator() - + # Create sample SD-CWT token, disclosures = create_sample_sd_cwt() - + print("\n1. Validating SD-CWT token:") results = validator.validate_token(token) print(f" Overall valid: {results['valid']}") @@ -92,10 +93,10 @@ def demonstrate_sd_cwt_validation(): print(f" Has sd_alg header: {results['has_sd_alg_header']}") if results['errors']: print(f" Errors: {results['errors']}") - + print("\n2. Token diagnostic notation:") validator.print_diagnostic(token) - + print("\n3. Validating disclosures:") for i, disclosure in enumerate(disclosures, 1): disclosure_cbor = cbor2.dumps(disclosure) @@ -110,7 +111,7 @@ def demonstrate_cbor_diag(): print("\n" + "=" * 60) print("CBOR Diagnostic Notation Demo") print("=" * 60) - + # Create complex CBOR structure complex_data = { 1: "issuer", # Using integer keys like in CWT @@ -125,9 +126,9 @@ def demonstrate_cbor_diag(): "binary": b"\x01\x02\x03\x04" } } - + cbor_data = cbor2.dumps(complex_data) - + print("\n1. Original Python data:") # Convert to JSON-serializable format json_safe = {} @@ -141,14 +142,14 @@ def demonstrate_cbor_diag(): else: json_safe[k] = v print(f" {json.dumps(json_safe, indent=2)}") - + print("\n2. CBOR hex representation:") print(f" {cbor_data.hex()}") - + print("\n3. CBOR diagnostic notation:") diag = edn_utils.cbor_to_diag(cbor_data) print(f" {diag}") - + print("\n4. Round-trip test:") cbor_from_diag = edn_utils.diag_to_cbor(diag) round_trip_data = cbor2.loads(cbor_from_diag) @@ -159,15 +160,15 @@ def main(): """Run all demonstrations.""" print("\nSD-CWT CBOR/CDDL Validation Examples") print("=" * 60) - + demonstrate_cbor_validation() demonstrate_sd_cwt_validation() demonstrate_cbor_diag() - + print("\n" + "=" * 60) print("Demo completed!") print("=" * 60) if __name__ == "__main__": - main() \ No newline at end of file + main() From 6a7da6a479a570cd86e77edd6ab1a3c68c2fb070 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Mon, 22 Sep 2025 07:45:00 -0500 Subject: [PATCH 2/8] update editors draft for context --- .../draft-ietf-spice-sd-cwt-latest.txt | 130 +++++++++--------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt b/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt index 25f4770..fe5dd5a 100644 --- a/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt +++ b/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt @@ -5,12 +5,12 @@ Secure Patterns for Internet CrEdentials M. Prorock Internet-Draft mesur.io Intended status: Standards Track O. Steele -Expires: 12 March 2026 Transmute +Expires: 21 March 2026 Tradeverifyd H. Birkholz Fraunhofer SIT R. Mahy Rohan Mahy Consulting Services - 8 September 2025 + 17 September 2025 Selective Disclosure CBOR Web Tokens (SD-CWT) @@ -56,7 +56,7 @@ Status of This Memo time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." - This Internet-Draft will expire on 12 March 2026. + This Internet-Draft will expire on 21 March 2026. Copyright Notice @@ -530,12 +530,12 @@ Issuer Holder Verifier bb05c9d8fb61cfc230ddfdfb4616a693' ] } >>, - / CWT signature / h'9c9022e57adb33c853f30b6e8a590f40 - 6ca55849d7b8cd2a2519d3aec03e61b9 - ef0ecd85fe96103f916f58d73cd2f775 - 4c390401945f0683b144d3504e500f94 - d30433c3445417dc3c920f7a155548e9 - 1994601827d0a46ead66ff450485e85f' + / CWT signature / h'ed7ff84b27e746199698a94cc19292e4 + b72dc4c3eb551f0ef2b9da07980c648c + 2bb033c337c6ed13e1bc7c5b7b7c9df9 + 49a70239f51eca1f6d8e058b8b70bcb3 + b5746812a932ffb37a2e6e984957e3f6 + b003eb3319fbe21e97f6a3a273307424' ]) Figure 1: Issued SD-CWT with all disclosures @@ -588,8 +588,8 @@ Issuer Holder Verifier / redacted_claim_keys / simple(59) : [ / redacted inspector_license_number / - h'af375dc3fba1d082448642c00be7b2f7 - bb05c9d8fb61cfc230ddfdfb4616a693' + h'd9df03da474fcb3c65771748e2e0608c + f437504ecc24f450aaeacd40dd552b3f', / ... next redacted claim at the same level would go here / ], Figure 5: redacted inspector_license_number claim in the issued @@ -653,10 +653,10 @@ Issuer Holder Verifier / iat / 6 : 1725244237, / 2024-09-02T02:30:37+00:00Z / / cnonce / 39 : h'8c0f5f523b95bea44a9a48c649240803' } >>, / end of KBT payload / - / KBT signature / h'd895729e72a3a7c801d5a20e9daf5103 - 27858aecbb39b8b2e4bc11cbbd625ea8 - c60b78da31fc9762c46b7cd61094d047 - 5ff1f19a7496cde53ab11600a5859d10' + / KBT signature / h'dd49379434b25b03cd8756787ab49731 + 580a04505439ca78ee53300dd49a00b7 + 0e8715d015a2a6e8d88455f5850e3d93 + eade1366c0040c2cee1cc568322a6b93' ]) / end of kbt / The digests in protected parts of the issued SD-CWT and the @@ -732,7 +732,11 @@ Issuer Holder Verifier salt, the disclosed value, and the name of the redacted element. For Salted Disclosed Claims of items in an array, the name is omitted. - salted = salted-claim / salted-element / decoy + ; an array of bstr-encoded Salted Disclosed Claims + salted-array = [ *bstr-encoded-salted ] + + bstr-encoded-salted = bstr .cbor salted-entry + salted-entry = salted-claim / salted-element / decoy salted-claim = [ bstr .size 16, ; 128-bit salt any, ; Claim Value @@ -746,9 +750,6 @@ Issuer Holder Verifier bstr .size 16 ; 128-bit salt ] - ; a collection of Salted Disclosed Claims - salted-array = [ +bstr .cbor salted ] - When a blinded claim is a key in a map, its blinded claim hash is added to a redacted_claim_keys array claim in the CWT payload that is at the same level of hierarchy as the key being blinded. The @@ -868,8 +869,9 @@ Issuer Holder Verifier sd-protected = { &(typ: 16) ^ => "application/sd-cwt" / TBD11, &(alg: 1) ^ => int, - &(sd_alg: TBD2) ^ => int, ; -16 for sha-256 - ? &(sd_aead: TBD7) ^ => uint .size 2 + ? &(kid: 4) ^ => bstr, + ? &(sd_alg: TBD2) ^ => int, ; -16 for sha-256 + ? &(sd_aead: TBD7) ^ => uint .size 2, * key => any } @@ -884,9 +886,9 @@ Issuer Holder Verifier &(iss: 1) ^ => tstr, ; "https://issuer.example" ? &(sub: 2) ^ => tstr, ; "https://device.example" ? &(aud: 3) ^ => tstr, ; "https://verifier.example/app" - ? &(exp: 4) ^ => int, ; 1883000000 - ? &(nbf: 5) ^ => int, ; 1683000000 - ? &(iat: 6) ^ => int, ; 1683000000 + ? &(exp: 4) ^ => num, ; 1883000000 + ? &(nbf: 5) ^ => num, ; 1683000000 + ? &(iat: 6) ^ => num, ; 1683000000 ? &(cti: 7) ^ => bstr, &(cnf: 8) ^ => { * key => any }, ; key confirmation ? &(cnonce: 39) ^ => bstr, @@ -982,9 +984,9 @@ Issuer Holder Verifier kbt-payload = { &(aud: 3) ^ => tstr, ; "https://verifier.example/app" - ? &(exp: 4) ^ => int, ; 1883000000 - ? &(nbf: 5) ^ => int, ; 1683000000 - &(iat: 6) ^ => int, ; 1683000000 + ? &(exp: 4) ^ => num, ; 1883000000 + ? &(nbf: 5) ^ => num, ; 1683000000 + &(iat: 6) ^ => num, ; 1683000000 ? &(cnonce: 39) ^ => bstr, * key => any } @@ -1145,14 +1147,13 @@ Issuer Holder Verifier The CDDL for AEAD encrypted disclosures is below. - aead-encrypted-array = [ +aead-encrypted ] + aead-encrypted-array = [ *aead-encrypted ] aead-encrypted = [ - bstr, ; nonce value - bstr, ; the ciphertext output of a bstr-encoded-salted - ; with a matching salt + bstr .size 16, ; 128-bit nonce + bstr, ; the encryption ciphertext output of a + ; bstr-encoded-salted bstr ; the corresponding authentication tag ] - ;bstr-encoded-salted = bstr .cbor salted Note: Because the encryption algorithm is in a registry that contains only AEAD algorithms, an attacker cannot replace the @@ -1261,12 +1262,12 @@ Issuer Holder Verifier bb05c9d8fb61cfc230ddfdfb4616a693' ] } >>, - / CWT signature / h'9c9022e57adb33c853f30b6e8a590f40 - 6ca55849d7b8cd2a2519d3aec03e61b9 - ef0ecd85fe96103f916f58d73cd2f775 - 4c390401945f0683b144d3504e500f94 - d30433c3445417dc3c920f7a155548e9 - 1994601827d0a46ead66ff450485e85f' + / CWT signature / h'ed7ff84b27e746199698a94cc19292e4 + b72dc4c3eb551f0ef2b9da07980c648c + 2bb033c337c6ed13e1bc7c5b7b7c9df9 + 49a70239f51eca1f6d8e058b8b70bcb3 + b5746812a932ffb37a2e6e984957e3f6 + b003eb3319fbe21e97f6a3a273307424' ]), / end of issuer SD-CWT / / typ / 16: "application/kb+cwt", @@ -1277,10 +1278,10 @@ Issuer Holder Verifier / iat / 6 : 1725244237, / 2024-09-02T02:30:37+00:00Z / / cnonce / 39 : h'8c0f5f523b95bea44a9a48c649240803' } >>, / end of KBT payload / - / KBT signature / h'd895729e72a3a7c801d5a20e9daf5103 - 27858aecbb39b8b2e4bc11cbbd625ea8 - c60b78da31fc9762c46b7cd61094d047 - 5ff1f19a7496cde53ab11600a5859d10' + / KBT signature / h'dd49379434b25b03cd8756787ab49731 + 580a04505439ca78ee53300dd49a00b7 + 0e8715d015a2a6e8d88455f5850e3d93 + eade1366c0040c2cee1cc568322a6b93' ]) / end of kbt / Figure 6: An EDN Example @@ -2231,9 +2232,9 @@ Issuer Holder Verifier [I-D.draft-ietf-oauth-sd-jwt-vc] Terbu, O., Fett, D., and B. Campbell, "SD-JWT-based Verifiable Credentials (SD-JWT VC)", Work in Progress, - Internet-Draft, draft-ietf-oauth-sd-jwt-vc-10, 7 July - 2025, . + Internet-Draft, draft-ietf-oauth-sd-jwt-vc-11, 15 + September 2025, . [I-D.draft-ietf-oauth-selective-disclosure-jwt] Fett, D., Yasuda, K., and B. Campbell, "Selective @@ -2300,8 +2301,9 @@ Appendix A. Complete CDDL Schema sd-protected = { &(typ: 16) ^ => "application/sd-cwt" / TBD11, &(alg: 1) ^ => int, - &(sd_alg: TBD2) ^ => int, ; -16 for sha-256 - ? &(sd_aead: TBD7) ^ => uint .size 2 + ? &(kid: 4) ^ => bstr, + ? &(sd_alg: TBD2) ^ => int, ; -16 for sha-256 + ? &(sd_aead: TBD7) ^ => uint .size 2, * key => any } @@ -2327,9 +2329,9 @@ Appendix A. Complete CDDL Schema &(iss: 1) ^ => tstr, ; "https://issuer.example" ? &(sub: 2) ^ => tstr, ; "https://device.example" ? &(aud: 3) ^ => tstr, ; "https://verifier.example/app" - ? &(exp: 4) ^ => int, ; 1883000000 - ? &(nbf: 5) ^ => int, ; 1683000000 - ? &(iat: 6) ^ => int, ; 1683000000 + ? &(exp: 4) ^ => num, ; 1883000000 + ? &(nbf: 5) ^ => num, ; 1683000000 + ? &(iat: 6) ^ => num, ; 1683000000 ? &(cti: 7) ^ => bstr, &(cnf: 8) ^ => { * key => any }, ; key confirmation ? &(cnonce: 39) ^ => bstr, @@ -2340,15 +2342,16 @@ Appendix A. Complete CDDL Schema kbt-payload = { &(aud: 3) ^ => tstr, ; "https://verifier.example/app" - ? &(exp: 4) ^ => int, ; 1883000000 - ? &(nbf: 5) ^ => int, ; 1683000000 - &(iat: 6) ^ => int, ; 1683000000 + ? &(exp: 4) ^ => num, ; 1883000000 + ? &(nbf: 5) ^ => num, ; 1683000000 + &(iat: 6) ^ => num, ; 1683000000 ? &(cnonce: 39) ^ => bstr, * key => any } - salted-array = [ +bstr .cbor salted ] - salted = salted-claim / salted-element / decoy + salted-array = [ *bstr-encoded-salted ] + bstr-encoded-salted = bstr .cbor salted-entry + salted-entry = salted-claim / salted-element / decoy salted-claim = [ bstr .size 16, ; 128-bit salt any, ; claim value @@ -2361,9 +2364,8 @@ Appendix A. Complete CDDL Schema decoy = [ bstr .size 16 ; 128-bit salt ] - ;bstr-encoded-salted = bstr .cbor salted - aead-encrypted-array = [ +aead-encrypted ] + aead-encrypted-array = [ *aead-encrypted ] aead-encrypted = [ bstr .size 16, ; 128-bit nonce bstr, ; the encryption ciphertext output of a @@ -2371,11 +2373,7 @@ Appendix A. Complete CDDL Schema bstr ; the corresponding authentication tag ] - header_map = { - * key => any - } - empty_or_serialized_map = bstr .cbor header_map / bstr .size 0 - + num = int / float key = int / text TBD1 = 17 TBD2 = 18 @@ -2394,7 +2392,7 @@ Appendix A. Complete CDDL Schema ; redacted_claim_element is to be used in CDDL payloads that contain ; array elements that are meant to be redacted. - redacted_claim_element = #6.60( bstr .size 16 ) ; #6.(bstr) + ;redacted_claim_element = #6.60( bstr .size 16 ) ; #6.(bstr) ;TBD5 = 60; CBOR tag wrapping redacted_claim_element Figure 7: A complete CDDL description of SD-CWT @@ -2455,7 +2453,7 @@ C.1. Subject / Holder { /kty/ 1 : 2, /EC/ - /alg/ 3 : -9, /ESP256/ + /alg/ 3 : -7, /ES256/ /crv/ -1 : 1, /P-256/ /x/ -2 : h'8554eb275dcd6fbd1c7ac641aa2c90d9 2022fd0d3024b5af18c7cc61ad527a2d', @@ -2853,8 +2851,8 @@ Authors' Addresses Orie Steele - Transmute - Email: orie@transmute.industries + Tradeverifyd + Email: orie@or13.io Henk Birkholz From b7f047714c1a4b1f006eaccf517d8e0319f07641 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 1 Nov 2025 10:50:22 -0400 Subject: [PATCH 3/8] update draft --- .../draft-ietf-spice-sd-cwt-latest.txt | 1334 +++++++++++++++-- tests/test_appendix_c_keys.py | 328 ++++ 2 files changed, 1521 insertions(+), 141 deletions(-) create mode 100644 tests/test_appendix_c_keys.py diff --git a/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt b/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt index fe5dd5a..4858031 100644 --- a/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt +++ b/docs/specifications/draft-ietf-spice-sd-cwt-latest.txt @@ -5,16 +5,15 @@ Secure Patterns for Internet CrEdentials M. Prorock Internet-Draft mesur.io Intended status: Standards Track O. Steele -Expires: 21 March 2026 Tradeverifyd +Expires: 23 April 2026 Tradeverifyd H. Birkholz Fraunhofer SIT R. Mahy - Rohan Mahy Consulting Services - 17 September 2025 + 20 October 2025 Selective Disclosure CBOR Web Tokens (SD-CWT) - draft-ietf-spice-sd-cwt-latest + draft-ietf-spice-sd-cwt-05 Abstract @@ -51,12 +50,20 @@ Status of This Memo working documents as Internet-Drafts. The list of current Internet- Drafts is at https://datatracker.ietf.org/drafts/current/. + + + +Prorock, et al. Expires 23 April 2026 [Page 1] + +Internet-Draft SD-CWT October 2025 + + Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." - This Internet-Draft will expire on 21 March 2026. + This Internet-Draft will expire on 23 April 2026. Copyright Notice @@ -74,88 +81,120 @@ Copyright Notice Table of Contents - 1. Introduction - 1.1. High-Level Flow - 2. Terminology - 3. Overview of Selective Disclosure CWT - 3.1. A CWT without Selective Disclosure - 3.2. Holder gets an SD-CWT from the Issuer - 4. Holder prepares an SD-CWT for a Verifier - 5. Differences from the CBOR Web Token Specification - 6. SD-CWT Definition - 6.1. Types of Blinded Claims - 7. SD-CWT Issuance - 7.1. Issuer Generation - 7.2. Holder Validation - 8. SD-CWT Presentation - 8.1. Creating a Key Binding Token - 9. SD-KBT and SD-CWT Verifier Validation - 10. Decoy Digests - 11. Encrypted Disclosures - 11.1. AEAD Encrypted Disclosures Mechanism - 12. Credential Types - 13. Examples - 13.1. Minimal Spanning Example - 13.2. Nested Example - 14. To Be Redacted Tag Definition - 15. Privacy Considerations - 15.1. Correlation - 15.2. Determinism - 15.3. Audience - 15.4. Credential Types - 16. Security Considerations - 16.1. Issuer Key Compromise - 16.2. Disclosure Coercion and Over-identification - 16.3. Threat Model Development Guidance - 16.4. Random Numbers - 16.5. Binding the KBT and the CWT - 16.6. Covert Channels - 16.7. Nested Disclosure Ordering - 17. IANA Considerations - 17.1. COSE Header Parameters - 17.1.1. sd_claims - 17.1.2. sd_alg - 17.1.3. sd_aead_encrypted_claims - 17.1.4. sd_aead - 17.2. CBOR Simple Values - 17.3. CBOR Tags - 17.3.1. To Be Redacted Tag - 17.3.2. Redacted Claim Element Tag - 17.4. CBOR Web Token (CWT) Claims - 17.4.1. vct - 17.5. Media Types - 17.5.1. application/sd-cwt - 17.5.2. application/kb+cwt - 17.6. Structured Syntax Suffix - 17.7. Content-Formats - 17.8. Verifiable Credential Type Identifiers - 17.8.1. Registration Template - 17.8.2. Initial Registry Contents - 18. References - 18.1. Normative References - 18.2. Informative References - Appendix A. Complete CDDL Schema - Appendix B. Comparison to SD-JWT - B.1. Media Types - B.2. Redaction Claims - B.3. Issuance - B.4. Presentation - B.5. Validation - Appendix C. Keys Used in the Examples - C.1. Subject / Holder - C.2. Issuer - Appendix D. Implementation Status - D.1. Transmute Prototype - D.2. Rust Prototype - Appendix E. Document History - E.1. draft-ietf-spice-sd-cwt-04 - E.2. draft-ietf-spice-sd-cwt-03 - E.3. draft-ietf-spice-sd-cwt-02 - E.4. draft-ietf-spice-sd-cwt-01 - E.5. draft-ietf-spice-sd-cwt-00 - Acknowledgments - Contributors - Authors' Addresses + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 + 1.1. High-Level Flow . . . . . . . . . . . . . . . . . . . . . 5 + 2. Terminology . . . . . . . . . . . . . . . . . . . . . . . . . 6 + 3. Overview of Selective Disclosure CWT . . . . . . . . . . . . 10 + 3.1. A CWT without Selective Disclosure . . . . . . . . . . . 10 + 3.2. Holder gets an SD-CWT from the Issuer . . . . . . . . . . 11 + 4. Holder prepares an SD-CWT for a Verifier . . . . . . . . . . 15 + 5. Differences from the CBOR Web Token Specification . . . . . . 16 + 6. SD-CWT Definition . . . . . . . . . . . . . . . . . . . . . . 17 + 6.1. Types of Blinded Claims . . . . . . . . . . . . . . . . . 17 + 7. SD-CWT Issuance . . . . . . . . . . . . . . . . . . . . . . . 18 + 7.1. Issuer Generation . . . . . . . . . . . . . . . . . . . . 19 + 7.2. Holder Validation . . . . . . . . . . . . . . . . . . . . 19 + 8. SD-CWT Presentation . . . . . . . . . . . . . . . . . . . . . 21 + 8.1. Creating a Key Binding Token . . . . . . . . . . . . . . 22 + 9. SD-KBT and SD-CWT Verifier Validation . . . . . . . . . . . . 23 + 10. Decoy Digests . . . . . . . . . . . . . . . . . . . . . . . . 24 + 11. Encrypted Disclosures . . . . . . . . . . . . . . . . . . . . 24 + 11.1. AEAD Encrypted Disclosures Mechanism . . . . . . . . . . 25 + 12. Credential Types . . . . . . . . . . . . . . . . . . . . . . 27 + 13. Examples . . . . . . . . . . . . . . . . . . . . . . . . . . 27 + 13.1. Minimal Spanning Example . . . . . . . . . . . . . . . . 27 + 13.2. Nested Example . . . . . . . . . . . . . . . . . . . . . 29 + 14. To Be Redacted Tag Definition . . . . . . . . . . . . . . . . 33 + 15. Privacy Considerations . . . . . . . . . . . . . . . . . . . 33 + + + +Prorock, et al. Expires 23 April 2026 [Page 2] + +Internet-Draft SD-CWT October 2025 + + + 15.1. Correlation . . . . . . . . . . . . . . . . . . . . . . 34 + 15.2. Determinism . . . . . . . . . . . . . . . . . . . . . . 34 + 15.3. Audience . . . . . . . . . . . . . . . . . . . . . . . . 34 + 15.4. Credential Types . . . . . . . . . . . . . . . . . . . . 34 + 16. Security Considerations . . . . . . . . . . . . . . . . . . . 35 + 16.1. Issuer Key Compromise . . . . . . . . . . . . . . . . . 35 + 16.2. Disclosure Coercion and Over-identification . . . . . . 36 + 16.3. Threat Model Development Guidance . . . . . . . . . . . 37 + 16.4. Random Numbers . . . . . . . . . . . . . . . . . . . . . 38 + 16.5. Binding the KBT and the CWT . . . . . . . . . . . . . . 39 + 16.6. Covert Channels . . . . . . . . . . . . . . . . . . . . 39 + 16.7. Nested Disclosure Ordering . . . . . . . . . . . . . . . 39 + 16.8. Choice of AEAD algorithms . . . . . . . . . . . . . . . 40 + 17. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 40 + 17.1. COSE Header Parameters . . . . . . . . . . . . . . . . . 40 + 17.1.1. sd_claims . . . . . . . . . . . . . . . . . . . . . 40 + 17.1.2. sd_alg . . . . . . . . . . . . . . . . . . . . . . . 41 + 17.1.3. sd_aead_encrypted_claims . . . . . . . . . . . . . . 41 + 17.1.4. sd_aead . . . . . . . . . . . . . . . . . . . . . . 41 + 17.2. CBOR Simple Values . . . . . . . . . . . . . . . . . . . 42 + 17.3. CBOR Tags . . . . . . . . . . . . . . . . . . . . . . . 42 + 17.3.1. To Be Redacted Tag . . . . . . . . . . . . . . . . . 42 + 17.3.2. Redacted Claim Element Tag . . . . . . . . . . . . . 42 + 17.4. CBOR Web Token (CWT) Claims . . . . . . . . . . . . . . 43 + 17.4.1. vct . . . . . . . . . . . . . . . . . . . . . . . . 43 + 17.5. Media Types . . . . . . . . . . . . . . . . . . . . . . 43 + 17.5.1. application/sd-cwt . . . . . . . . . . . . . . . . . 43 + 17.5.2. application/kb+cwt . . . . . . . . . . . . . . . . . 44 + 17.6. Structured Syntax Suffix . . . . . . . . . . . . . . . . 45 + 17.7. Content-Formats . . . . . . . . . . . . . . . . . . . . 46 + 17.8. Verifiable Credential Type Identifiers . . . . . . . . . 46 + 17.8.1. Registration Template . . . . . . . . . . . . . . . 48 + 17.8.2. Initial Registry Contents . . . . . . . . . . . . . 48 + 18. References . . . . . . . . . . . . . . . . . . . . . . . . . 48 + 18.1. Normative References . . . . . . . . . . . . . . . . . . 48 + 18.2. Informative References . . . . . . . . . . . . . . . . . 50 + Appendix A. Complete CDDL Schema . . . . . . . . . . . . . . . . 51 + Appendix B. Comparison to SD-JWT . . . . . . . . . . . . . . . . 54 + B.1. Media Types . . . . . . . . . . . . . . . . . . . . . . . 54 + B.2. Redaction Claims . . . . . . . . . . . . . . . . . . . . 54 + B.3. Issuance . . . . . . . . . . . . . . . . . . . . . . . . 54 + B.4. Presentation . . . . . . . . . . . . . . . . . . . . . . 55 + B.5. Validation . . . . . . . . . . . . . . . . . . . . . . . 55 + Appendix C. Keys Used in the Examples . . . . . . . . . . . . . 55 + C.1. Subject / Holder . . . . . . . . . . . . . . . . . . . . 55 + C.2. Issuer . . . . . . . . . . . . . . . . . . . . . . . . . 57 + Appendix D. Implementation Status . . . . . . . . . . . . . . . 59 + D.1. Transmute Prototype . . . . . . . . . . . . . . . . . . . 59 + + + +Prorock, et al. Expires 23 April 2026 [Page 3] + +Internet-Draft SD-CWT October 2025 + + + D.2. Rust Prototype . . . . . . . . . . . . . . . . . . . . . 60 + Appendix E. Relationship between RATS Architecture and Verifiable + Credentials . . . . . . . . . . . . . . . . . . . . . . . 60 + E.1. Three-Party Verifiable Credentials Model . . . . . . . . 60 + E.2. RATS Architecture Roles . . . . . . . . . . . . . . . . . 61 + E.3. Role Mappings in the Three-Party Model . . . . . . . . . 61 + E.3.1. Verifiable Credential Issuer as RATS Endorser . . . . 61 + E.3.2. Verifiable Credential Holder as RATS Verifier . . . . 62 + E.3.3. Verifiable Credential Verifier as RATS Relying + Party . . . . . . . . . . . . . . . . . . . . . . . . 62 + E.3.4. All Parties Can Be Attesters . . . . . . . . . . . . 63 + E.4. Comparison with RATS Interaction Models . . . . . . . . . 63 + E.5. Roles That Don't Map to the Three-Party Model . . . . . . 64 + E.6. Application to SD-CWT . . . . . . . . . . . . . . . . . . 64 + Appendix F. Sample Disclosure Matching Algorithm for Verifier . 65 + Appendix G. Document History . . . . . . . . . . . . . . . . . . 66 + G.1. draft-ietf-spice-sd-cwt-05 . . . . . . . . . . . . . . . 66 + G.2. draft-ietf-spice-sd-cwt-04 . . . . . . . . . . . . . . . 66 + G.3. draft-ietf-spice-sd-cwt-03 . . . . . . . . . . . . . . . 67 + G.4. draft-ietf-spice-sd-cwt-02 . . . . . . . . . . . . . . . 68 + G.5. draft-ietf-spice-sd-cwt-01 . . . . . . . . . . . . . . . 68 + G.6. draft-ietf-spice-sd-cwt-00 . . . . . . . . . . . . . . . 69 + Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 69 + Contributors . . . . . . . . . . . . . . . . . . . . . . . . . . 69 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 69 1. Introduction @@ -169,6 +208,24 @@ Table of Contents credentials to prove the integrity and authenticity of selected attributes asserted by an Issuer about a Subject to a Verifier. + + + + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 4] + +Internet-Draft SD-CWT October 2025 + + Although techniques such as one time use and batch issuance can improve the confidentiality and security characteristics of CWT-based credential protocols, SD-CWTs remain traceable. Selective Disclosure @@ -185,21 +242,52 @@ Table of Contents Claims Sets contain Claim Keys and Claim Values. SD-CWT enables Issuers to mark certain Claim Keys or Claim Values mandatory or optional for a Holder of a CWT to disclose. A Verifier that does not - understand selective disclosure at all cannot process redacted Claim - Keys sent by the Holder. However, Claim Keys and Claim Values that - are not understood remain ignored, as described in Section 3 of + understand selective disclosure at all can only act on unblinded + claims sent by the Holder; it will ignore Blinded Claims representing + array items, and will fail to process any SD-CWT containing Blinded + Claims that represent map keys. optional Claim Keys, whether they are + disclosed or not, can only be processed by a Verifier that + understands this specification. However, Claim Keys and Claim Values + that are not understood remain ignored, as described in Section 3 of [RFC8392]. 1.1. High-Level Flow Figure 1: High-level SD-CWT Issuance and Presentation Flow + + + + + + + + + + + + + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 5] + +Internet-Draft SD-CWT October 2025 + + Issuer Holder Verifier | | | | +---+ | | | | Key Gen | | Request SD-CWT |<--+ | - |<-------------------------------| | + |<-------------------------------+ | | | | +------------------------------->| Request Nonce | | Receive SD-CWT +-------------------------------->| @@ -243,6 +331,13 @@ Issuer Holder Verifier This specification uses terms from CWT [RFC8392], COSE [RFC9052] [RFC9053] and JWT [RFC7519]. + + +Prorock, et al. Expires 23 April 2026 [Page 6] + +Internet-Draft SD-CWT October 2025 + + The terms Claim Name, Claim Key, and Claim Value are defined in [RFC8392]. @@ -292,6 +387,13 @@ Issuer Holder Verifier Redacted Claim Element: The hash of an element redacted from an array data structure. + + +Prorock, et al. Expires 23 April 2026 [Page 7] + +Internet-Draft SD-CWT October 2025 + + Presented Disclosed Claims Set: The CBOR map containing zero or more Redacted Claim Keys or Redacted Claim Elements. @@ -339,6 +441,15 @@ Issuer Holder Verifier | Validated Disclosed Claim Set | +------------------------------------------+ + + + + +Prorock, et al. Expires 23 April 2026 [Page 8] + +Internet-Draft SD-CWT October 2025 + + This diagram relates the terminology specific to selective disclosure and redaction. @@ -387,6 +498,14 @@ Issuer Holder Verifier v +------------------------------------------+ | Blinded Claim Hash (computed) | + + + +Prorock, et al. Expires 23 April 2026 [Page 9] + +Internet-Draft SD-CWT October 2025 + + +-----+------------------------------------+ | | 6. Matches with hash in payload @@ -408,6 +527,41 @@ Issuer Holder Verifier keys shown in the examples have been invented for this example and do not have registered integer keys. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 10] + +Internet-Draft SD-CWT October 2025 + + { / iss / 1 : "https://issuer.example", / sub / 2 : "https://device.example", @@ -453,6 +607,17 @@ Issuer Holder Verifier the SD-CWT. After the Holder requests an SD-CWT from the Issuer, the Issuer generates the following SD-CWT: + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 11] + +Internet-Draft SD-CWT October 2025 + + / cose-sign1 / 18([ / issuer SD-CWT / / CWT protected / << { / alg / 1 : -35, / ES384 / @@ -501,6 +666,14 @@ Issuer Holder Verifier 2022fd0d3024b5af18c7cc61ad527a2d', / y / -3: h'4dc7ae2c677e96d0cc82597655ce92d5 503f54293d87875d1e79ce4770194343' + + + +Prorock, et al. Expires 23 April 2026 [Page 12] + +Internet-Draft SD-CWT October 2025 + + } }, /most_recent_inspection_passed/ 500: true, @@ -547,6 +720,16 @@ Issuer Holder Verifier consisting of a per-disclosure random salt, the Claim Key, and Claim Value. + + + + + +Prorock, et al. Expires 23 April 2026 [Page 13] + +Internet-Draft SD-CWT October 2025 + + <<[ /salt/ h'bae611067bb823486797da1ebbb52f83', /value/ "ABCD-123456", @@ -595,6 +778,14 @@ Issuer Holder Verifier Figure 5: redacted inspector_license_number claim in the issued CWT payload + + + +Prorock, et al. Expires 23 April 2026 [Page 14] + +Internet-Draft SD-CWT October 2025 + + 4. Holder prepares an SD-CWT for a Verifier When the Holder wants to send an SD-CWT and disclose none, some, or @@ -637,6 +828,20 @@ Issuer Holder Verifier The issued SD-CWT is placed in the kcwt (Confirmation Key CWT) protected header field (defined in [RFC9528]). + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 15] + +Internet-Draft SD-CWT October 2025 + + / cose-sign1 / 18( / sd_kbt / [ / KBT protected / << { / alg / 1: -7, / ES256 / @@ -684,6 +889,15 @@ Issuer Holder Verifier hex as 21, and a tag of 60 wrapping a 3 is represented in hex as D8 3C 03 + + + + +Prorock, et al. Expires 23 April 2026 [Page 16] + +Internet-Draft SD-CWT October 2025 + + Note that Holders presenting to a Verifier that does not support this specification would need to present a CWT without tagged map keys or simple value map keys. @@ -732,6 +946,14 @@ Issuer Holder Verifier salt, the disclosed value, and the name of the redacted element. For Salted Disclosed Claims of items in an array, the name is omitted. + + + +Prorock, et al. Expires 23 April 2026 [Page 17] + +Internet-Draft SD-CWT October 2025 + + ; an array of bstr-encoded Salted Disclosed Claims salted-array = [ *bstr-encoded-salted ] @@ -780,6 +1002,14 @@ Issuer Holder Verifier matter, which is not discussed in this specification. This specification defines the format of an SD-CWT communicated between an Issuer and a Holder in this section, and describes the format of a + + + +Prorock, et al. Expires 23 April 2026 [Page 18] + +Internet-Draft SD-CWT October 2025 + + Key Binding Token containing that SD-CWT communicated between a Holder and a Verifier in Section 8. @@ -828,6 +1058,14 @@ Issuer Holder Verifier * the issuer (iss) and subject (sub) are correct; + + + +Prorock, et al. Expires 23 April 2026 [Page 19] + +Internet-Draft SD-CWT October 2025 + + * if an audience (aud) is present, it is acceptable; * the CWT is valid according to the nbf and exp claims, if present; @@ -859,6 +1097,31 @@ Issuer Holder Verifier The following informative CDDL is provided to describe the syntax for SD-CWT issuance. A complete CDDL schema is in Appendix A. + + + + + + + + + + + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 20] + +Internet-Draft SD-CWT October 2025 + + sd-cwt-issued = #6.18([ protected: bstr .cbor sd-protected, sd-unprotected, @@ -908,6 +1171,13 @@ Issuer Holder Verifier Salted Disclosed Claims in the sd_claims header parameter of the unprotected header. + + +Prorock, et al. Expires 23 April 2026 [Page 21] + +Internet-Draft SD-CWT October 2025 + + An SD-CWT presentation to a Verifier has the same syntax as an SD-CWT issued to a Holder, except the Holder chooses the subset of disclosures included in the sd_claims header parameter. @@ -956,6 +1226,14 @@ Issuer Holder Verifier * the Holder's disclosure is linked to the creation time (iat) of the key binding. + + + +Prorock, et al. Expires 23 April 2026 [Page 22] + +Internet-Draft SD-CWT October 2025 + + The SD-KBT prevents an attacker from copying and pasting disclosures, or from adding or removing disclosures without detection. Confirmation is established according to [RFC8747], using the cnf @@ -1003,6 +1281,15 @@ Issuer Holder Verifier 1. First the Verifier must open the protected headers of the SD-KBT and find the Issuer SD-CWT present in the kcwt field. + + + + +Prorock, et al. Expires 23 April 2026 [Page 23] + +Internet-Draft SD-CWT October 2025 + + 2. Next, the Verifier must validate the SD-CWT as described in Section 7.2 of [RFC8392]. @@ -1012,35 +1299,14 @@ Issuer Holder Verifier 4. Using the confirmation key, the Verifier validates the SD-KBT as described in Section 7.2 of [RFC8392]. - 5. Finally, the Verifier MUST extract and decode the disclosed - claims from the sd_claims header parameter in the unprotected - header of the SD-CWT. The decoded sd_claims are converted to an - intermediate data structure called a Digest To Disclosed Claim - Map that is used to transform the Presented Disclosed Claims Set - into a Validated Disclosed Claims Set. The Verifier MUST compute - the hash of each Salted Disclosed Claim (salted), in order to - match each disclosed value to each entry of the Presented - Disclosed Claims Set. One possible concrete representation of - the intermediate data structure for the Digest To Disclosed Claim - Map could be: { &(digested-salted-disclosed-claim) => salted } - - a. The Verifier constructs an empty cbor map called the - Validated Disclosed Claims Set, and initializes it with all - mandatory to disclose claims from the verified Presented - Disclosed Claims Set. - - b. Next, the Verifier performs a breadth first or depth first - traversal of the Presented Disclosed Claims Set and Validated - Disclosed Claims Set, using the Digest To Disclosed Claim Map - to insert claims into the Validated Disclosed Claims Set when - they appear in the Presented Disclosed Claims Set. By - performing these steps, the recipient can cryptographically - verify the integrity of the protected claims and verify they - have not been tampered with. - - c. If there remain unused claims in the Digest To Disclosed - Claim Map at the end of this procedure the SD-CWT MUST be - considered invalid. + 5. The Verifier MUST extract and decode the disclosed claims from + the sd_claims header parameter in the unprotected header of the + SD-CWT. Each decoded disclosure is treated as if it is a claim + key or claim element at the location corresponding to its Blinded + Claim Hash in the payload. If there are any disclosures that do + not have a corresponding Blinded Claim Hash, the entire SD-CWT is + invalid. If any decoded Redacted Claim Key duplicates another + claim key in the same position, the entire SD-CWT is invalid. Note: A Verifier MUST be prepared to process disclosures in any order. When disclosures are nested, a disclosed value @@ -1058,6 +1324,10 @@ Issuer Holder Verifier Disclosed Claims Set, just as it might be applied to a validated CWT Claims Set. + By performing these steps, the recipient can cryptographically verify + the integrity of the protected claims and verify they have not been + tampered with. + 10. Decoy Digests *TODO* @@ -1069,6 +1339,13 @@ Issuer Holder Verifier Attester, and a target entity called a Relying Party. Other protocols have a similar type of internal structure for the Verifier. + + +Prorock, et al. Expires 23 April 2026 [Page 24] + +Internet-Draft SD-CWT October 2025 + + In some of these use cases, there is existing usage of AES-128 GCM and other Authenticated Encryption with Additional Data (AEAD) [RFC5116] algorithms. @@ -1099,7 +1376,8 @@ Issuer Holder Verifier an Authenticated Encryption with Additional Data (AEAD) algorithm [RFC5116] registered in the IANA AEAD Algorithms registry (https://www.iana.org/assignments/aead-parameters/aead- - parameters.xhtml) . The second header parameter + parameters.xhtml) . (Guidance on specific algorithms is discussed in + Section 16.8.) The second header parameter (sd_aead_encrypted_claims) contains a list of AEAD encrypted disclosures. Taking the first example disclosure from above: @@ -1116,6 +1394,14 @@ Issuer Holder Verifier consists of its encryption algorithm's ciphertext and its authentication tag. (For example, in AEAD_AES_128_GCM the authentication tag is 16 octets.) The nonce (nonce), the encryption + + + +Prorock, et al. Expires 23 April 2026 [Page 25] + +Internet-Draft SD-CWT October 2025 + + algorithm's ciphertext (ciphertext) and authentication tag (tag) are put in an array. The resulting array is placed in the sd_aead_encrypted_claims header parameter in the unprotected headers @@ -1160,6 +1446,18 @@ Issuer Holder Verifier algorithm or the message, without a decryption verification failure. + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 26] + +Internet-Draft SD-CWT October 2025 + + 12. Credential Types This specification defines the CWT claim vct (for Verifiable @@ -1208,6 +1506,14 @@ Issuer Holder Verifier /value/ "ABCD-123456", /claim/ 501 / inspector_license_number / ]>>, + + + +Prorock, et al. Expires 23 April 2026 [Page 27] + +Internet-Draft SD-CWT October 2025 + + <<[ /salt/ h'8de86a012b3043ae6e4457b9e1aaab80', /value/ 1549560720 / inspected 7-Feb-2019 / @@ -1256,6 +1562,14 @@ Issuer Holder Verifier b78e3ab768ce941863dc8914e8f5815f' ] }, + + + +Prorock, et al. Expires 23 April 2026 [Page 28] + +Internet-Draft SD-CWT October 2025 + + / redacted_claim_keys / simple(59) : [ / redacted inspector_license_number / h'af375dc3fba1d082448642c00be7b2f7 @@ -1293,6 +1607,25 @@ Issuer Holder Verifier structure. It could be blinded at multiple levels of the claims set hierarchy. + + + + + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 29] + +Internet-Draft SD-CWT October 2025 + + { / iss / 1 : "https://issuer.example", / sub / 2 : "https://device.example", @@ -1334,6 +1667,21 @@ Issuer Holder Verifier ] } + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 30] + +Internet-Draft SD-CWT October 2025 + + For example, looking at the nested disclosures below, the first disclosure unblinds the entire January 2023 inspection record. However, when the record is disclosed, the inspector license number @@ -1382,6 +1730,14 @@ Issuer Holder Verifier c891cbc3329b7fea70a3aa636c87a0a4' ] }, + + + +Prorock, et al. Expires 23 April 2026 [Page 31] + +Internet-Draft SD-CWT October 2025 + + /claim/ 503 / San Francisco location / ]>>, <<[ @@ -1426,11 +1782,23 @@ Issuer Holder Verifier disclosed Claims Set visible to the Verifier would look like the following: - { - / iss / 1 : "https://issuer.example", - / sub / 2 : "https://device.example", - / exp / 4 : 1725330600, /2024-09-02T19:30:00Z/ - / nbf / 5 : 1725243840, /2024-09-01T19:25:00Z/ + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 32] + +Internet-Draft SD-CWT October 2025 + + + { + / iss / 1 : "https://issuer.example", + / sub / 2 : "https://device.example", + / exp / 4 : 1725330600, /2024-09-02T19:30:00Z/ + / nbf / 5 : 1725243840, /2024-09-01T19:25:00Z/ / iat / 6 : 1725244200, /2024-09-01T19:30:00Z/ / cnf / 8 : { ... }, 504: [ / inspection history log / @@ -1470,6 +1838,18 @@ Issuer Holder Verifier the recommendations from [RFC6973]. Many of the topics discussed in [RFC6973] apply to SD-CWT, but are not repeated here. + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 33] + +Internet-Draft SD-CWT October 2025 + + 15.1. Correlation Presentations of the same SD-CWT to multiple Verifiers can be @@ -1511,6 +1891,21 @@ Issuer Holder Verifier to-disclose data elements in an SD-CWT must be carefully chosen based on the specific privacy risks associated with each credential type. + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 34] + +Internet-Draft SD-CWT October 2025 + + For example, a passport credential contains highly sensitive personal information where even partial disclosure can have significant privacy implications: - Revealing citizenship status may expose an @@ -1559,6 +1954,14 @@ Issuer Holder Verifier targeting the provisioning and binding between issuer names and their cryptographic key material pose significant risks. An attacker who can manipulate these bindings could substitute their own keys for + + + +Prorock, et al. Expires 23 April 2026 [Page 35] + +Internet-Draft SD-CWT October 2025 + + legitimate issuer keys, enabling credential forgery while appearing to be a trusted issuer. @@ -1604,6 +2007,17 @@ Issuer Holder Verifier remain vulnerable to over-identification and long-term misuse of their disclosed information. + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 36] + +Internet-Draft SD-CWT October 2025 + + 16.3. Threat Model Development Guidance This section provides guidance for developing threat models when @@ -1652,6 +2066,14 @@ Issuer Holder Verifier b. How can the Holder be convinced the Verifier that received presentations is legitimate? + + + +Prorock, et al. Expires 23 April 2026 [Page 37] + +Internet-Draft SD-CWT October 2025 + + c. How can the Holder be convinced the Verifier will not share, sell, leak, or otherwise disclose the Holder's presentations or Issuer or Holder signed material? @@ -1699,6 +2121,15 @@ Issuer Holder Verifier Poor choice of salts can lead to brute force attacks that can reveal redacted claims. + + + + +Prorock, et al. Expires 23 April 2026 [Page 38] + +Internet-Draft SD-CWT October 2025 + + 16.5. Binding the KBT and the CWT The "iss" claim in the SD-CWT is self-asserted by the Issuer. @@ -1744,6 +2175,36 @@ Issuer Holder Verifier However, the order can affect the runtime of the verification process. + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 39] + +Internet-Draft SD-CWT October 2025 + + +16.8. Choice of AEAD algorithms + + The AEAD encrypted disclosures mechanism discussed in Section 11.1 + can refer to any AEAD alogithm in the IANA AEAD Algorithms registry + (https://www.iana.org/assignments/aead-parameters/aead- + parameters.xhtml) . + + When choosing an AEAD algorithm, the tag length is critical for the + integrity of encrypted disclosures in SD-CWT. As such, + implementations MUST NOT use any AEAD algorithm with a tag length + less than 16 octets. + + Algorithms using AES-CCM are NOT RECOMMENDED. + + As of this writing, implementations MUST NOT use algorithms 3 through + 14, 18, 19, 21, 22, 24, 25, 27, or 28. Implementations using the + AEGIS algorithms containing an X MUST only use the 256-bit tag + variant. + 17. IANA Considerations 17.1. COSE Header Parameters @@ -1771,6 +2232,16 @@ Issuer Holder Verifier * Reference: Section 4 of this specification + + + + + +Prorock, et al. Expires 23 April 2026 [Page 40] + +Internet-Draft SD-CWT October 2025 + + 17.1.2. sd_alg The following completed registration template per RFC8152 is @@ -1820,6 +2291,13 @@ Issuer Holder Verifier * Value Registry: IANA AEAD Algorithm number + + +Prorock, et al. Expires 23 April 2026 [Page 41] + +Internet-Draft SD-CWT October 2025 + + * Description: The AEAD algorithm used for encrypting disclosures. * Reference: Section 11.1 of this specification @@ -1869,6 +2347,13 @@ Issuer Holder Verifier * Semantics: A selective disclosure redacted (array) claim element. + + +Prorock, et al. Expires 23 April 2026 [Page 42] + +Internet-Draft SD-CWT October 2025 + + * Specification Document(s): Section 6.1 of this specification 17.4. CBOR Web Token (CWT) Claims @@ -1916,6 +2401,15 @@ Issuer Holder Verifier * Encoding considerations: binary + + + + +Prorock, et al. Expires 23 April 2026 [Page 43] + +Internet-Draft SD-CWT October 2025 + + * Security considerations: Section 16 of this specification and [RFC8392] @@ -1963,6 +2457,15 @@ Issuer Holder Verifier * Encoding considerations: binary + + + + +Prorock, et al. Expires 23 April 2026 [Page 44] + +Internet-Draft SD-CWT October 2025 + + * Security considerations: Section 16 of this specification and [RFC8392] @@ -2011,6 +2514,14 @@ Issuer Holder Verifier * Encoding considerations: binary + + + +Prorock, et al. Expires 23 April 2026 [Page 45] + +Internet-Draft SD-CWT October 2025 + + * Interoperability considerations: n/a * Fragment identifier considerations: n/a @@ -2058,6 +2569,15 @@ Issuer Holder Verifier The registration procedures for numbers in specific ranges are as described below: + + + + +Prorock, et al. Expires 23 April 2026 [Page 46] + +Internet-Draft SD-CWT October 2025 + + +=============+=======================================+ | Range | Registration Procedure | +=============+=======================================+ @@ -2101,6 +2621,19 @@ Issuer Holder Verifier and should direct all requests for registration in the Specification Required range to the review mailing list. + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 47] + +Internet-Draft SD-CWT October 2025 + + It is suggested that multiple Designated Experts be appointed who are able to represent the perspectives of different applications using this specification, in order to enable broadly-informed review of @@ -2148,12 +2681,21 @@ Issuer Holder Verifier RFC 7942, DOI 10.17487/RFC7942, July 2016, . + + + + +Prorock, et al. Expires 23 April 2026 [Page 48] + +Internet-Draft SD-CWT October 2025 + + [I-D.ietf-cbor-edn-literals] Bormann, C., "CBOR Extended Diagnostic Notation (EDN)", Work in Progress, Internet-Draft, draft-ietf-cbor-edn- - literals-18, 7 July 2025, + literals-19, 16 October 2025, . + edn-literals-19>. [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, @@ -2195,6 +2737,15 @@ Issuer Holder Verifier DOI 10.17487/RFC9052, August 2022, . + + + + +Prorock, et al. Expires 23 April 2026 [Page 49] + +Internet-Draft SD-CWT October 2025 + + [RFC9053] Schaad, J., "CBOR Object Signing and Encryption (COSE): Initial Algorithms", RFC 9053, DOI 10.17487/RFC9053, August 2022, . @@ -2218,16 +2769,16 @@ Issuer Holder Verifier [I-D.draft-ietf-cbor-cde] Bormann, C., "CBOR Common Deterministic Encoding (CDE)", - Work in Progress, Internet-Draft, draft-ietf-cbor-cde-12, - 7 July 2025, . + Work in Progress, Internet-Draft, draft-ietf-cbor-cde-13, + 13 October 2025, . [I-D.draft-ietf-keytrans-protocol] McMillion, B. and F. Linker, "Key Transparency Protocol", Work in Progress, Internet-Draft, draft-ietf-keytrans- - protocol-02, 6 July 2025, + protocol-03, 19 October 2025, . + keytrans-protocol-03>. [I-D.draft-ietf-oauth-sd-jwt-vc] Terbu, O., Fett, D., and B. Campbell, "SD-JWT-based @@ -2243,6 +2794,14 @@ Issuer Holder Verifier May 2025, . + + + +Prorock, et al. Expires 23 April 2026 [Page 50] + +Internet-Draft SD-CWT October 2025 + + [I-D.ietf-httpbis-unprompted-auth] Schinazi, D., Oliver, D., and J. Hoyland, "The Concealed HTTP Authentication Scheme", Work in Progress, Internet- @@ -2291,6 +2850,14 @@ Appendix A. Complete CDDL Schema signature: bstr ]) + + + +Prorock, et al. Expires 23 April 2026 [Page 51] + +Internet-Draft SD-CWT October 2025 + + kbt-cwt = #6.18([ protected: bstr .cbor kbt-protected, kbt-unprotected, @@ -2340,6 +2907,13 @@ Appendix A. Complete CDDL Schema * key => any } + + +Prorock, et al. Expires 23 April 2026 [Page 52] + +Internet-Draft SD-CWT October 2025 + + kbt-payload = { &(aud: 3) ^ => tstr, ; "https://verifier.example/app" ? &(exp: 4) ^ => num, ; 1883000000 @@ -2388,6 +2962,14 @@ Appendix A. Complete CDDL Schema ; REDACTED_KEYS is to be used in CDDL payloads that are meant to ; convey that a map key is redacted. REDACTED_KEYS = #7.59 ; #7. + + + +Prorock, et al. Expires 23 April 2026 [Page 53] + +Internet-Draft SD-CWT October 2025 + + ;TBD4 = 59 ; for CBOR simple value 59 ; redacted_claim_element is to be used in CDDL payloads that contain @@ -2430,6 +3012,20 @@ B.3. Issuance The issuance process for SD-CWT is similar to SD-JWT, with the exception that a confirmation claim is REQUIRED. + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 54] + +Internet-Draft SD-CWT October 2025 + + B.4. Presentation The presentation process for SD-CWT is similar to SD-JWT, except that @@ -2477,6 +3073,15 @@ C.1. Subject / Holder The same map in CBOR pretty printing + + + + +Prorock, et al. Expires 23 April 2026 [Page 55] + +Internet-Draft SD-CWT October 2025 + + A4 # map(4) 01 # unsigned(1) 02 # unsigned(2) @@ -2526,6 +3131,13 @@ C.1. Subject / Holder JLWvGMfMYa1Sei1Nx64sZ36W0MyCWXZVzpLVUD9UKT2Hh10eec5HcBlDQw== -----END PUBLIC KEY----- + + +Prorock, et al. Expires 23 April 2026 [Page 56] + +Internet-Draft SD-CWT October 2025 + + Holder private key in PEM format -----BEGIN PRIVATE KEY----- @@ -2565,6 +3177,23 @@ C.2. Issuer The same map in CBOR pretty printing + + + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 57] + +Internet-Draft SD-CWT October 2025 + + A4 # map(5) 01 # unsigned(1) 02 # unsigned(2) @@ -2612,6 +3241,15 @@ C.2. Issuer Issuer public key in PEM format + + + + +Prorock, et al. Expires 23 April 2026 [Page 58] + +Internet-Draft SD-CWT October 2025 + + -----BEGIN PUBLIC KEY----- MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEwxeYsMeIX6NSj7+HfltMOm3GelpdxrMH tyjDclkm8qvl+0lkzZHjlIpUk/brtsu/j2x+x2FpHK03TE2qk4dFPxgFjs5Y6wqO @@ -2661,6 +3299,13 @@ D.1. Transmute Prototype Description: An open-source implementation of this specification. + + +Prorock, et al. Expires 23 April 2026 [Page 59] + +Internet-Draft SD-CWT October 2025 + + Maturity: Prototype Coverage: The current version ('main') implements functionality @@ -2698,11 +3343,346 @@ D.2. Rust Prototype Contact: Beltram Maldant (beltram.ietf.spice@pm.me) -Appendix E. Document History +Appendix E. Relationship between RATS Architecture and Verifiable + Credentials + + This appendix describes the relationship between the Remote + ATtestation procedureS (RATS) architecture defined in [RFC9334] and + the three-party model used in verifiable credentials. + +E.1. Three-Party Verifiable Credentials Model + + The verifiable credentials model involves three distinct parties: + + + + +Prorock, et al. Expires 23 April 2026 [Page 60] + +Internet-Draft SD-CWT October 2025 + + + * *Issuer*: Creates and signs credentials containing claims about a + subject + + * *Holder*: Controls the credential and presents it to verifiers + (the holder is typically the subject of the credential) + + * *Verifier*: Receives and validates presented credentials to make + authorization or access decisions. In this appendix we refer to + this role as a *Credential Verifier* + + In SD-CWT, these roles are explicitly represented: the Issuer signs + claims using an Assertion Key (Section 2), the Holder controls the + credential and creates presentations using a Confirmation Key, and + the Verifier validates both the Issuer's signature over the + credential and the Holder's signature over the presentation (key + binding token). + +E.2. RATS Architecture Roles + + The RATS architecture defines the following key roles: + + * *Attester*: Produces Evidence about its own trustworthiness and + operational state + + * *Endorser*: Provides Endorsements about an Attester (typically a + manufacturer) + + * *Reference Value Provider*: Supplies Reference Values used by + Verifiers to evaluate Evidence + + * *Verifier*: Appraises Evidence and produces Attestation Results. + In this appendix we refer to this role as a *RATS Verifier* + + * *Relying Party*: Consumes Attestation Results to make + authorization decisions + +E.3. Role Mappings in the Three-Party Model + + The mapping between RATS roles and verifiable credential roles can be + understood as follows: + +E.3.1. Verifiable Credential Issuer as RATS Endorser + + A verifiable credential Issuer functions as a RATS Endorser. The + Endorser role in RATS produces Endorsements - secure statements about + an Attester's capabilities, identity, or trustworthiness. Similarly, + a credential Issuer produces signed credentials containing claims + about a subject (the Holder). Both roles: + + + +Prorock, et al. Expires 23 April 2026 [Page 61] + +Internet-Draft SD-CWT October 2025 + + + * Make authoritative statements about another party's attributes or + capabilities + + * Use cryptographic signatures to ensure integrity and authenticity + + * Are typically trusted third parties in their respective ecosystems + + * Provide information that enables downstream authorization + decisions + + The credential issued by the Issuer serves the same function as an + Endorsement in RATS: it is a signed assertion about the Holder's + attributes that can be used by Credential Verifiers to make trust + decisions. + +E.3.2. Verifiable Credential Holder as RATS Verifier + + A verifiable credential Holder functions as a RATS Verifier. The + RATS Verifier appraises Evidence and Endorsements and produces + Attestation Results. In the credentials model, the Holder: + + * Receives credentials (analogous to Endorsements) from Issuers + + * Evaluates which credentials to present and which claims to + disclose + + * Produces presentations (analogous to Attestation Results) that are + sent to Credential Verifiers + + * Uses their Confirmation Key to create key binding tokens that + prove control + + The Holder's presentation, which includes the Issuer's credential + plus the Holder's signature over selected disclosures, functions as + an Attestation Result - a processed, signed assertion derived from + the original credential (Endorsement). + +E.3.3. Verifiable Credential Verifier as RATS Relying Party + + A verifiable credential Credential Verifier functions as a RATS + Relying Party. The Relying Party: + + * Consumes Attestation Results (credential presentations) to make + authorization decisions + + * Validates the cryptographic integrity of received assertions + + + + + +Prorock, et al. Expires 23 April 2026 [Page 62] + +Internet-Draft SD-CWT October 2025 + + + * Makes access control or authorization decisions based on the + claims received + + * Does not directly interact with the original Endorsement source + (the Issuer) + + The Credential Verifier appraises the Holder's presentation in the + same way a Relying Party appraises Attestation Results from a RATS + Verifier. + +E.3.4. All Parties Can Be Attesters + + Importantly, any of these parties - Issuer, Holder, or Credential + Verifier - can simultaneously function as a RATS Attester. The + Attester role in RATS is about producing Evidence about one's own + trustworthiness: + + * An *Issuer* may be an Attester when it needs to prove its own + integrity, platform state, or authorization to issue certain + credential types. For example, an Issuer might provide Evidence + about its secure enclave or certified infrastructure when + establishing trust with Holders or during credential issuance. + + * A *Holder* may be an Attester when presenting credentials, + particularly when the presentation itself requires proof of the + Holder's platform integrity. For example, a Holder might provide + Evidence about their device's secure boot state, firmware version, + or trusted execution environment alongside their credential + presentation. + + * A *Credential Verifier* may be an Attester when it needs to prove + its own trustworthiness to Holders or to upstream systems. For + example, a Credential Verifier might provide Evidence about its + data protection capabilities, compliance certifications, or secure + processing environment before Holders agree to disclose sensitive + claims. + + The Attester role is orthogonal to the three primary roles - it + represents the ability to produce evidence about one's own state, + while the Issuer/Holder/Credential Verifier roles represent the flow + of credentials and claims about subjects. + +E.4. Comparison with RATS Interaction Models + + RATS defines two interaction models: + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 63] + +Internet-Draft SD-CWT October 2025 + + + *Passport Model*: The Attester sends Evidence to a RATS Verifier, + receives Attestation Results, and presents these results to Relying + Parties. This maps to the three-party credentials model where the + Holder obtains credentials from Issuers and presents them to + Credential Verifiers. + + *Background-Check Model*: The Attester sends Evidence to a Relying + Party, which forwards it to a RATS Verifier. The RATS Verifier + returns results directly to the Relying Party. This is a two-party + model from the Attester's perspective and does not map well to the + three-party credentials model, as it lacks Holder mediation and + control over presentations. + +E.5. Roles That Don't Map to the Three-Party Model + + The *Reference Value Provider* role from RATS does not have a direct + equivalent in the three-party verifiable credentials model. This + role supplies reference values (known-good measurements or + configurations) that RATS Verifiers use to appraise Endorsements and + Evidence. In credentials systems, equivalent functionality might be + provided through: + + * Trust registries that list authorized Issuers + + * Schema registries, or lists of valid claims that define credential + formats + + * Governance frameworks that specify validation rules + + * Revocation registries + + However, these are typically considered part of the trust + infrastructure rather than a distinct party in the presentation + protocol. The Reference Value Provider role is primarily relevant in + scenarios where raw Evidence must be evaluated against known-good + values - a pattern more common in the two-party background-check + model than in the three-party credentials model where Issuers have + already performed evaluation and produced credentials. + +E.6. Application to SD-CWT + + When applying RATS concepts to SD-CWT: + + * SD-CWT credentials function as Endorsements about the Holder + (subject) + + * The Holder's key binding token and selective disclosure act as the + RATS Verifier's appraisal and production of Attestation Results + + + +Prorock, et al. Expires 23 April 2026 [Page 64] + +Internet-Draft SD-CWT October 2025 + + + * The Credential Verifier consumes these presentations as a Relying + Party consumes Attestation Results + + * Any party can additionally provide Evidence about their own + platform or operational state (act as an Attester) + + * The three-party model with selective disclosure maps naturally to + the RATS passport model + + * Reference Value Provider functionality is addressed through trust + infrastructure and out-of-band mechanisms rather than protocol- + level roles + +Appendix F. Sample Disclosure Matching Algorithm for Verifier + + The Verifier of an SD-CWT needs to decode disclosed claims match them + with their redacted versions. The following example algorithm + describes a way to accomplish this. + + 1. The decoded sd_claims are converted to an intermediate data + structure called a Digest To Disclosed Claim Map that is used to + transform the Presented Disclosed Claims Set into a Validated + Disclosed Claims Set. + + 2. The Verifier MUST compute the hash of each Salted Disclosed Claim + (salted), in order to match each disclosed value to each entry of + the Presented Disclosed Claims Set. + + One possible concrete representation of the intermediate data + structure for the Digest To Disclosed Claim Map is a CBOR map with + the hash of the bstr-encoded-salted data structure (from the CDDL) + as the map key and its value as the contents of the corresponding + salted-entry data structure. + + 3. The Verifier constructs an empty CBOR map called the Validated + Disclosed Claims Set, and initializes it with all mandatory to + disclose claims from the verified Presented Disclosed Claims Set. + + 4. Next, the Verifier performs a depth-first traversal of the + Presented Disclosed Claims Set and Validated Disclosed Claims + Set, using the Digest To Disclosed Claim Map to insert claims + into the Validated Disclosed Claims Set when they appear in the + Presented Disclosed Claims Set. + + 5. The Verifier repeats the fourth step if the previous iteration + resulted in any new Presented Disclosed Claims. + + + + + +Prorock, et al. Expires 23 April 2026 [Page 65] + +Internet-Draft SD-CWT October 2025 + + + 6. If there remain unused claims in the Digest To Disclosed Claim + Map at the end of this procedure the SD-CWT MUST be considered + invalid. Likewise, if this algorithm results in any duplicate + CBOR map keys, the entire SD-CWT MUST be considered invalid. + + Note: If there are remaining digests without corresponding + disclosures, this means that either the holder intentionally did + not disclose a claim, or that the digest is a decoy digest + Section 10. + +Appendix G. Document History Note: RFC Editor, please remove this entire section on publication. -E.1. draft-ietf-spice-sd-cwt-04 +G.1. draft-ietf-spice-sd-cwt-05 + + * Added this change log (PR#150) + + * Moved non-normative validation algorithm to an appendix (PR#149) + + * Added appendix describing mapping to RATS concepts (#147) + + * Provided guidance on choice of AEAD algorithm (#148) + + * Fixed algorithm in COSE key examples (#145) + + * Updated contact information (PR#142, PR #150) + + * Removed SPICE from the title of the document (PR#139) + + * Made clear extent to which verifiers cannot process unknown claims + (PR#138) + + * Sorted CBOR map keys in examples to facilitate use as test vectors + (PR#135) + + * Consistently use term "tag" in context of AEAD algorithms (PR#134) + + * Improved AASVG diagram in Terminology section (PR#129) + +G.2. draft-ietf-spice-sd-cwt-04 * Place value before claim name in disclosures @@ -2710,6 +3690,14 @@ E.1. draft-ietf-spice-sd-cwt-04 * Greatly improved text around AEAD encrypted disclosures + + + +Prorock, et al. Expires 23 April 2026 [Page 66] + +Internet-Draft SD-CWT October 2025 + + * Applied clarifications and corrections suggested by Mike Jones. * Do not update CWT [RFC8392]. @@ -2743,7 +3731,7 @@ E.1. draft-ietf-spice-sd-cwt-04 * Fixed some references -E.2. draft-ietf-spice-sd-cwt-03 +G.3. draft-ietf-spice-sd-cwt-03 * remove bstr encoding from sd_claims array (but not the individual disclosures) @@ -2757,6 +3745,15 @@ E.2. draft-ietf-spice-sd-cwt-03 * clarify that duplicate map keys are not allowed, and how tagged keys are represented. + + + + +Prorock, et al. Expires 23 April 2026 [Page 67] + +Internet-Draft SD-CWT October 2025 + + * added security considerations section (#42) and text about privacy and linkability risks (#43) @@ -2775,7 +3772,7 @@ E.2. draft-ietf-spice-sd-cwt-03 * add optional encrypted disclosures -E.3. draft-ietf-spice-sd-cwt-02 +G.4. draft-ietf-spice-sd-cwt-02 * KBT now includes the entire SD-CWT in the Confirmation Key CWT (kcwt) existing COSE protected header. Has algorithm now @@ -2789,7 +3786,7 @@ E.3. draft-ietf-spice-sd-cwt-02 * Add section saying SD-CWT updates the CWT spec (RFC8392). (PR#29) -E.4. draft-ietf-spice-sd-cwt-01 +G.5. draft-ietf-spice-sd-cwt-01 * Added Overview section @@ -2805,6 +3802,14 @@ E.4. draft-ietf-spice-sd-cwt-01 * Consistently avoid use of bare term "key" - to make crypto keys and map keys clear + + + +Prorock, et al. Expires 23 April 2026 [Page 68] + +Internet-Draft SD-CWT October 2025 + + * Make clear issued SD-CWT can contain zero or more redactions; presented SD-CWT can disclose zero, some, or all redacted claims. @@ -2818,7 +3823,7 @@ E.4. draft-ietf-spice-sd-cwt-01 * Updated draft metadata -E.5. draft-ietf-spice-sd-cwt-00 +G.6. draft-ietf-spice-sd-cwt-00 * Initial working group version based on draft-prorock-spice-cose- sd-cwt-01. @@ -2855,6 +3860,12 @@ Authors' Addresses Email: orie@or13.io + +Prorock, et al. Expires 23 April 2026 [Page 69] + +Internet-Draft SD-CWT October 2025 + + Henk Birkholz Fraunhofer SIT Rheinstrasse 75 @@ -2864,5 +3875,46 @@ Authors' Addresses Rohan Mahy - Rohan Mahy Consulting Services - Email: rohan.ietf@gmail.com \ No newline at end of file + Email: rohan.ietf@gmail.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Prorock, et al. Expires 23 April 2026 [Page 70] \ No newline at end of file diff --git a/tests/test_appendix_c_keys.py b/tests/test_appendix_c_keys.py new file mode 100644 index 0000000..8c52c7a --- /dev/null +++ b/tests/test_appendix_c_keys.py @@ -0,0 +1,328 @@ +"""Comprehensive unit tests for keys from Appendix C of draft-ietf-spice-sd-cwt-latest.txt""" + +import pytest + +from sd_cwt import cbor_utils, edn_utils +from sd_cwt.thumbprint import CoseKeyThumbprint + + +class TestAppendixCKeys: + """Test key examples from Appendix C: Keys Used in the Examples.""" + + @pytest.fixture + def es256_holder_private_key_edn(self): + """ES256 Holder/Subject private key from C.1 (with corrected algorithm).""" + return """ + { + 1: 2, + 3: -7, + -1: 1, + -2: h'8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d', + -3: h'4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343', + -4: h'5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5' + } + """ + + @pytest.fixture + def es256_holder_public_key_edn(self): + """ES256 Holder public key for thumbprint computation (without private component).""" + return """ + { + 1: 2, + -1: 1, + -2: h'8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d', + -3: h'4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343' + } + """ + + @pytest.fixture + def es384_issuer_private_key_edn(self): + """ES384 Issuer private key from C.2 (with corrected algorithm).""" + return """ + { + 1: 2, + 2: "https://issuer.example/cwk3.cbor", + 3: -35, + -1: 2, + -2: h'c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf', + -3: h'8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554', + -4: h'71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c' + } + """ + + @pytest.fixture + def es384_issuer_public_key_edn(self): + """ES384 Issuer public key for thumbprint computation (without private component and kid).""" + return """ + { + 1: 2, + -1: 2, + -2: h'c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf', + -3: h'8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554' + } + """ + + def test_es256_holder_private_key_structure(self, es256_holder_private_key_edn): + """Test ES256 holder private key structure and values from specification C.1.""" + cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + key = cbor_utils.decode(cbor_data) + + assert isinstance(key, dict) + assert key[1] == 2 # kty: EC2 + assert key[3] == -7 # alg: ES256 (corrected from spec's erroneous -9) + assert key[-1] == 1 # crv: P-256 + + assert len(key[-2]) == 32 # X coordinate: 32 bytes for P-256 + assert len(key[-3]) == 32 # Y coordinate: 32 bytes for P-256 + assert len(key[-4]) == 32 # Private key: 32 bytes for P-256 + + expected_x = bytes.fromhex('8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d') + expected_y = bytes.fromhex('4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343') + expected_d = bytes.fromhex('5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5') + + assert key[-2] == expected_x + assert key[-3] == expected_y + assert key[-4] == expected_d + + def test_es256_holder_public_key_structure(self, es256_holder_public_key_edn): + """Test ES256 holder public key structure for thumbprint computation.""" + cbor_data = edn_utils.diag_to_cbor(es256_holder_public_key_edn) + key = cbor_utils.decode(cbor_data) + + assert isinstance(key, dict) + assert len(key) == 4 # Should only have 4 fields (no private key, no alg) + assert -4 not in key # No private key component + assert 3 not in key # No alg in thumbprint format + assert key[1] == 2 # kty: EC2 + assert key[-1] == 1 # crv: P-256 + + def test_es384_issuer_private_key_structure(self, es384_issuer_private_key_edn): + """Test ES384 issuer private key structure and values from specification C.2.""" + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + key = cbor_utils.decode(cbor_data) + + assert isinstance(key, dict) + assert key[1] == 2 # kty: EC2 + assert key[2] == "https://issuer.example/cwk3.cbor" # kid + assert key[3] == -35 # alg: ES384 (corrected from spec's erroneous -51) + assert key[-1] == 2 # crv: P-384 + + assert len(key[-2]) == 48 # X coordinate: 48 bytes for P-384 + assert len(key[-3]) == 48 # Y coordinate: 48 bytes for P-384 + assert len(key[-4]) == 48 # Private key: 48 bytes for P-384 + + expected_x = bytes.fromhex('c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf') + expected_y = bytes.fromhex('8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554') + expected_d = bytes.fromhex('71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c') + + assert key[-2] == expected_x + assert key[-3] == expected_y + assert key[-4] == expected_d + + def test_es384_issuer_public_key_structure(self, es384_issuer_public_key_edn): + """Test ES384 issuer public key structure for thumbprint computation.""" + cbor_data = edn_utils.diag_to_cbor(es384_issuer_public_key_edn) + key = cbor_utils.decode(cbor_data) + + assert isinstance(key, dict) + assert len(key) == 4 # Should only have 4 fields (no kid, no alg, no private key) + assert -4 not in key # No private key component + assert 2 not in key # No kid in thumbprint format + assert 3 not in key # No alg in thumbprint format + assert key[1] == 2 # kty: EC2 + assert key[-1] == 2 # crv: P-384 + + def test_thumbprint_computation_es256_holder(self, es256_holder_public_key_edn): + """Test COSE key thumbprint computation for ES256 holder key matches specification.""" + cbor_data = edn_utils.diag_to_cbor(es256_holder_public_key_edn) + key = cbor_utils.decode(cbor_data) + + thumbprint = CoseKeyThumbprint.compute(key, "sha256") + expected_thumbprint = bytes.fromhex('8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0') + + assert thumbprint == expected_thumbprint, f"Expected {expected_thumbprint.hex()}, got {thumbprint.hex()}" + + def test_thumbprint_computation_es384_issuer(self, es384_issuer_public_key_edn): + """Test COSE key thumbprint computation for ES384 issuer key matches specification.""" + cbor_data = edn_utils.diag_to_cbor(es384_issuer_public_key_edn) + key = cbor_utils.decode(cbor_data) + + thumbprint = CoseKeyThumbprint.compute(key, "sha256") + expected_thumbprint = bytes.fromhex('554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0') + + assert thumbprint == expected_thumbprint, f"Expected {expected_thumbprint.hex()}, got {thumbprint.hex()}" + + def test_cbor_roundtrip_all_keys(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test CBOR encoding/decoding roundtrip for all keys from specification.""" + test_keys = [es256_holder_private_key_edn, es384_issuer_private_key_edn] + + for key_edn in test_keys: + cbor_data = edn_utils.diag_to_cbor(key_edn) + key = cbor_utils.decode(cbor_data) + + re_encoded = cbor_utils.encode(key) + re_decoded = cbor_utils.decode(re_encoded) + + assert key == re_decoded, "CBOR roundtrip should preserve key structure" + + def test_key_validation_required_fields(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test that all keys have required fields according to specification.""" + # ES256 holder key + cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + holder_key = cbor_utils.decode(cbor_data) + + required_private_fields = {1, 3, -1, -2, -3, -4} # kty, alg, crv, x, y, d + assert all(field in holder_key for field in required_private_fields) + + # ES384 issuer key (includes kid) + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + issuer_key = cbor_utils.decode(cbor_data) + + required_issuer_fields = {1, 2, 3, -1, -2, -3, -4} # kty, kid, alg, crv, x, y, d + assert all(field in issuer_key for field in required_issuer_fields) + + def test_extract_public_keys_from_private(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test extracting public keys from private keys per specification.""" + # Extract ES256 holder public key + cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + private_key = cbor_utils.decode(cbor_data) + public_key = {k: v for k, v in private_key.items() if k != -4} + + assert -4 not in public_key + assert len(public_key) == 5 # kty, alg, crv, x, y + + # Extract ES384 issuer public key + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + private_key = cbor_utils.decode(cbor_data) + public_key = {k: v for k, v in private_key.items() if k != -4} + + assert -4 not in public_key + assert len(public_key) == 6 # kty, kid, alg, crv, x, y + + def test_algorithm_curve_consistency(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test that algorithm and curve parameters are consistent per specification.""" + # ES256 with P-256 + cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + key = cbor_utils.decode(cbor_data) + assert key[3] == -7 # ES256 + assert key[-1] == 1 # P-256 + assert len(key[-2]) == 32 # P-256 coordinates are 32 bytes + + # ES384 with P-384 + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + key = cbor_utils.decode(cbor_data) + assert key[3] == -35 # ES384 + assert key[-1] == 2 # P-384 + assert len(key[-2]) == 48 # P-384 coordinates are 48 bytes + + def test_private_key_ranges(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test that private key values are in valid ranges.""" + # ES256 (P-256) + cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + key = cbor_utils.decode(cbor_data) + d_int = int.from_bytes(key[-4], 'big') + assert 0 < d_int < 2**256 + + # ES384 (P-384) + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + key = cbor_utils.decode(cbor_data) + d_int = int.from_bytes(key[-4], 'big') + assert 0 < d_int < 2**384 + + def test_coordinate_non_zero(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test that public key coordinates are not zero.""" + # ES256 + cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + key = cbor_utils.decode(cbor_data) + assert key[-2] != b'\x00' * 32 # X coordinate not zero + assert key[-3] != b'\x00' * 32 # Y coordinate not zero + + # ES384 + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + key = cbor_utils.decode(cbor_data) + assert key[-2] != b'\x00' * 48 # X coordinate not zero + assert key[-3] != b'\x00' * 48 # Y coordinate not zero + + def test_specification_algorithm_corrections(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test corrections of specification algorithm errors.""" + # The specification shows incorrect algorithm values that we've corrected + + # ES256 holder key: spec shows -9 but should be -7 for ES256 + cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + holder_key = cbor_utils.decode(cbor_data) + assert holder_key[3] == -7, "ES256 algorithm should be -7, not -9 as shown in spec" + + # ES384 issuer key: spec shows -51 but should be -35 for ES384 + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + issuer_key = cbor_utils.decode(cbor_data) + assert issuer_key[3] == -35, "ES384 algorithm should be -35, not -51 as shown in spec" + + def test_cbor_pretty_print_structure(self, es256_holder_public_key_edn): + """Test that CBOR structure matches specification pretty printing format.""" + cbor_data = edn_utils.diag_to_cbor(es256_holder_public_key_edn) + key = cbor_utils.decode(cbor_data) + + # Re-encode and verify structure matches spec's CBOR pretty printing + re_encoded = cbor_utils.encode(key) + re_decoded = cbor_utils.decode(re_encoded) + + # Should match expected structure from specification lines 2478-2490 + assert len(re_decoded) == 4 # map(4) + assert re_decoded[1] == 2 # kty: EC2 + assert re_decoded[-1] == 1 # crv: P-256 + assert len(re_decoded[-2]) == 32 # X coordinate: 32 bytes + assert len(re_decoded[-3]) == 32 # Y coordinate: 32 bytes + + def test_issuer_key_id_specification_compliance(self, es384_issuer_private_key_edn): + """Test that issuer key ID exactly matches specification.""" + cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + key = cbor_utils.decode(cbor_data) + + # Must match specification line 2543 + expected_kid = "https://issuer.example/cwk3.cbor" + assert key[2] == expected_kid, "Key ID must exactly match specification" + assert isinstance(key[2], str), "Key ID must be string type" + assert key[2].startswith("https://"), "Key ID must be HTTPS URL" + + def test_interoperability_export_formats(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test exporting keys in formats suitable for interoperability testing.""" + keys = [es256_holder_private_key_edn, es384_issuer_private_key_edn] + + for key_edn in keys: + cbor_data = edn_utils.diag_to_cbor(key_edn) + key = cbor_utils.decode(cbor_data) + public_key = {k: v for k, v in key.items() if k != -4} + + # Test hex export + hex_export = cbor_utils.encode(public_key).hex() + assert isinstance(hex_export, str) + assert len(hex_export) > 0 + assert all(c in '0123456789abcdef' for c in hex_export.lower()) + + # Test EDN export roundtrip + edn_export = edn_utils.cbor_to_diag(cbor_utils.encode(public_key)) + assert "1:2" in edn_export or "1: 2" in edn_export # kty: EC2 + + def test_edn_format_specification_compliance(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + """Test that EDN format parsing works correctly for specification examples.""" + # Test holder key EDN + holder_cbor = edn_utils.diag_to_cbor(es256_holder_private_key_edn) + holder_decoded = cbor_utils.decode(holder_cbor) + + # Convert back to EDN and verify roundtrip + holder_roundtrip_edn = edn_utils.cbor_to_diag(holder_cbor) + holder_roundtrip_cbor = edn_utils.diag_to_cbor(holder_roundtrip_edn) + holder_roundtrip_decoded = cbor_utils.decode(holder_roundtrip_cbor) + + assert holder_decoded == holder_roundtrip_decoded, "Holder EDN roundtrip should preserve key" + + # Test issuer key EDN + issuer_cbor = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) + issuer_decoded = cbor_utils.decode(issuer_cbor) + + # Convert back to EDN and verify roundtrip + issuer_roundtrip_edn = edn_utils.cbor_to_diag(issuer_cbor) + issuer_roundtrip_cbor = edn_utils.diag_to_cbor(issuer_roundtrip_edn) + issuer_roundtrip_decoded = cbor_utils.decode(issuer_roundtrip_cbor) + + assert issuer_decoded == issuer_roundtrip_decoded, "Issuer EDN roundtrip should preserve key" \ No newline at end of file From 1b6864a960b386b90eabfbdd7cd748cfbe79b3bf Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 1 Nov 2025 12:29:42 -0400 Subject: [PATCH 4/8] thumbprints are good --- tests/test_ietf_124.py | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/test_ietf_124.py diff --git a/tests/test_ietf_124.py b/tests/test_ietf_124.py new file mode 100644 index 0000000..173c74f --- /dev/null +++ b/tests/test_ietf_124.py @@ -0,0 +1,72 @@ +from sd_cwt import cbor_utils, edn_utils +from sd_cwt.cose_keys import cose_key_thumbprint + + +class TestIETF124: + """Test IETF 124 compliance.""" + + def test_holder_key_thumbprint(self): + """Test key import and thumbprint generation for the holder key.""" + holder_key_edn = """ +{ + /kty/ 1 : 2, /EC/ + /alg/ 3 : -7, /ES256/ + /crv/ -1 : 1, /P-256/ + /x/ -2 : h'8554eb275dcd6fbd1c7ac641aa2c90d9 + 2022fd0d3024b5af18c7cc61ad527a2d', + /y/ -3 : h'4dc7ae2c677e96d0cc82597655ce92d5 + 503f54293d87875d1e79ce4770194343', + /d/ -4 : h'5759a86e59bb3b002dde467da4b52f3d + 06e6c2cd439456cf0485b9b864294ce5' +} + """ + + + cbor_data = edn_utils.diag_to_cbor(holder_key_edn) + holder_key_dict = cbor_utils.decode(cbor_data) + assert holder_key_dict is not None + assert holder_key_dict[1] == 2 + assert holder_key_dict[3] == -7 + assert holder_key_dict[-1] == 1 + expected_x = bytes.fromhex('8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d') + expected_y = bytes.fromhex('4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343') + expected_d = bytes.fromhex('5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5') + assert holder_key_dict[-2] == expected_x + assert holder_key_dict[-3] == expected_y + assert holder_key_dict[-4] == expected_d + + thumbprint = cose_key_thumbprint(cbor_data) + assert thumbprint is not None + assert thumbprint == bytes.fromhex('8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0') + + def test_issuer_key_thumbprint(self): + """Test key import and thumbprint generation for the issuer key.""" + issuer_key_edn = """ +{ + /kty/ 1 : 2, /EC/ + /kid/ 2 : "https://issuer.example/cwk3.cbor", + /alg/ 3 : -51, /ESP384/ + /crv/ -1 : 2, /P-384/ + /x/ -2 : h'c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307 + b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf', + /y/ -3 : h'8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e + 84a055a31fb7f9214b27509522c159e764f8711e11609554', + /d/ -4 : h'71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d + 5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c' +} + """ + + cbor_data = edn_utils.diag_to_cbor(issuer_key_edn) + issuer_key_dict = cbor_utils.decode(cbor_data) + assert issuer_key_dict is not None + assert issuer_key_dict[1] == 2 + assert issuer_key_dict[3] == -51 + assert issuer_key_dict[-1] == 2 + expected_x = bytes.fromhex('c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf') + expected_y = bytes.fromhex('8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554') + expected_d = bytes.fromhex('71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c') + assert issuer_key_dict[-2] == expected_x + + thumbprint = cose_key_thumbprint(cbor_data) + assert thumbprint is not None + assert thumbprint == bytes.fromhex('554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0') \ No newline at end of file From c1c8d59d37fbded0eebfd3e79060ff29412a5a4b Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 1 Nov 2025 12:48:08 -0400 Subject: [PATCH 5/8] signature verification done --- src/sd_cwt/cose_sign1.py | 39 ++++++++ tests/test_ietf_124.py | 197 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 235 insertions(+), 1 deletion(-) diff --git a/src/sd_cwt/cose_sign1.py b/src/sd_cwt/cose_sign1.py index 08351b7..6417864 100644 --- a/src/sd_cwt/cose_sign1.py +++ b/src/sd_cwt/cose_sign1.py @@ -243,6 +243,45 @@ def verify(self, message: bytes, signature: bytes) -> bool: return False +class ES384Verifier: + """ECDSA P-384 SHA-384 verifier implementation.""" + + def __init__(self, public_key_x: bytes, public_key_y: bytes): + """Initialize ES384 verifier with public key coordinates. + + Args: + public_key_x: X coordinate of public key (48 bytes) + public_key_y: Y coordinate of public key (48 bytes) + """ + x = int.from_bytes(public_key_x, byteorder='big') + y = int.from_bytes(public_key_y, byteorder='big') + + public_numbers = ec.EllipticCurvePublicNumbers(x, y, ec.SECP384R1()) + self.public_key = public_numbers.public_key(default_backend()) + + def verify(self, message: bytes, signature: bytes) -> bool: + """Verify a signature with ES384.""" + try: + if len(signature) != 96: + return False + + r = int.from_bytes(signature[:48], byteorder='big') + s = int.from_bytes(signature[48:], byteorder='big') + signature_der = utils.encode_dss_signature(r, s) + + self.public_key.verify( + signature_der, + message, + ec.ECDSA(hashes.SHA384()) + ) + return True + + except InvalidSignature: + return False + except (ValueError, TypeError): + return False + + def generate_es256_key_pair() -> tuple[bytes, bytes, bytes]: """Generate an ES256 (ECDSA P-256) key pair. diff --git a/tests/test_ietf_124.py b/tests/test_ietf_124.py index 173c74f..68faf42 100644 --- a/tests/test_ietf_124.py +++ b/tests/test_ietf_124.py @@ -69,4 +69,199 @@ def test_issuer_key_thumbprint(self): thumbprint = cose_key_thumbprint(cbor_data) assert thumbprint is not None - assert thumbprint == bytes.fromhex('554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0') \ No newline at end of file + assert thumbprint == bytes.fromhex('554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0') + + + def test_minimal_spanning_example(self): + """Test the minimal spanning example from the specification.""" + holder_key_edn = """ +{ + /kty/ 1 : 2, + /alg/ 3 : -7, + /crv/ -1 : 1, + /x/ -2 : h'8554eb275dcd6fbd1c7ac641aa2c90d9 + 2022fd0d3024b5af18c7cc61ad527a2d', + /y/ -3 : h'4dc7ae2c677e96d0cc82597655ce92d5 + 503f54293d87875d1e79ce4770194343', + /d/ -4 : h'5759a86e59bb3b002dde467da4b52f3d + 06e6c2cd439456cf0485b9b864294ce5' +} + """ + + issuer_key_edn = """ +{ + /kty/ 1 : 2, + /kid/ 2 : "https://issuer.example/cwk3.cbor", + /alg/ 3 : -51, + /crv/ -1 : 2, + /x/ -2 : h'c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307 + b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf', + /y/ -3 : h'8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e + 84a055a31fb7f9214b27509522c159e764f8711e11609554', + /d/ -4 : h'71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d + 5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c' +} + """ + + minimal_spanning_example_edn = """ + + / cose-sign1 / 18( / sd_kbt / [ + / KBT protected / << { + / alg / 1: -7, / ES256 / + / kcwt / 13: 18([ / issuer SD-CWT / + / CWT protected / << { + / alg / 1 : -35, / ES384 / + / kid / 4 : 'https://issuer.example/cose-key3', + / typ / 16 : "application/sd-cwt", + / sd_alg / 18 : -16 / SHA256 / + } >>, + / CWT unprotected / { + / sd_claims / 17 : [ / these are the disclosures / + <<[ + /salt/ h'bae611067bb823486797da1ebbb52f83', + /value/ "ABCD-123456", + /claim/ 501 / inspector_license_number / + ]>>, + <<[ + /salt/ h'8de86a012b3043ae6e4457b9e1aaab80', + /value/ 1549560720 / inspected 7-Feb-2019 / + ]>>, + <<[ + /salt/ h'ec615c3035d5a4ff2f5ae29ded683c8e', + /value/ "ca", + /claim/ "region" / region=California / + ]>>, + ] + } + / CWT payload / << { + / iss / 1 : "https://issuer.example", + / sub / 2 : "https://device.example", + / exp / 4 : 1725330600, /2024-09-03T02:30:00+00:00Z/ + / nbf / 5 : 1725243900, /2024-09-02T02:25:00+00:00Z/ + / iat / 6 : 1725244200, /2024-09-02T02:30:00+00:00Z/ + / cnf / 8 : { + / cose key / 1 : { + / kty / 1: 2, / EC2 / + / crv / -1: 1, / P-256 / + / x / -2: h'8554eb275dcd6fbd1c7ac641aa2c90d9 + 2022fd0d3024b5af18c7cc61ad527a2d', + / y / -3: h'4dc7ae2c677e96d0cc82597655ce92d5 + 503f54293d87875d1e79ce4770194343' + } + }, + /most_recent_inspection_passed/ 500: true, + /inspection_dates/ 502 : [ + / redacted inspection date 7-Feb-2019 / + 60(h'1b7fc8ecf4b1290712497d226c04b503 + b4aa126c603c83b75d2679c3c613f3fd'), + / redacted inspection date 4-Feb-2021 / + 60(h'64afccd3ad52da405329ad935de1fb36 + 814ec48fdfd79e3a108ef858e291e146'), + 1674004740, / 2023-01-17T17:19:00 / + ], + / inspection_location / 503 : { + "country" : "us", / United States / + / redacted_claim_keys / simple(59) : [ + / redacted region / + h'0d4b8c6123f287a1698ff2db15764564 + a976fb742606e8fd00e2140656ba0df3' + / redacted postal_code / + h'c0b7747f960fc2e201c4d47c64fee141 + b78e3ab768ce941863dc8914e8f5815f' + ] + }, + / redacted_claim_keys / simple(59) : [ + / redacted inspector_license_number / + h'af375dc3fba1d082448642c00be7b2f7 + bb05c9d8fb61cfc230ddfdfb4616a693' + ] + } >>, + / CWT signature / h'ed7ff84b27e746199698a94cc19292e4 + b72dc4c3eb551f0ef2b9da07980c648c + 2bb033c337c6ed13e1bc7c5b7b7c9df9 + 49a70239f51eca1f6d8e058b8b70bcb3 + b5746812a932ffb37a2e6e984957e3f6 + b003eb3319fbe21e97f6a3a273307424' + ]), + / end of issuer SD-CWT / + / typ / 16: "application/kb+cwt", + } >>, / end of KBT protected header / + / KBT unprotected / {}, + / KBT payload / << { + / aud / 3 : "https://verifier.example/app", + / iat / 6 : 1725244237, / 2024-09-02T02:30:37+00:00Z / + / cnonce / 39 : h'8c0f5f523b95bea44a9a48c649240803' + } >>, / end of KBT payload / + / KBT signature / h'dd49379434b25b03cd8756787ab49731 + 580a04505439ca78ee53300dd49a00b7 + 0e8715d015a2a6e8d88455f5850e3d93 + eade1366c0040c2cee1cc568322a6b93' + ]) / end of kbt / + """ + + holder_key_cbor = edn_utils.diag_to_cbor(holder_key_edn) + holder_key_dict = cbor_utils.decode(holder_key_cbor) + holder_x = holder_key_dict[-2] + holder_y = holder_key_dict[-3] + + issuer_key_cbor = edn_utils.diag_to_cbor(issuer_key_edn) + issuer_key_dict = cbor_utils.decode(issuer_key_cbor) + issuer_x = issuer_key_dict[-2] + issuer_y = issuer_key_dict[-3] + + kbt_cbor = edn_utils.diag_to_cbor(minimal_spanning_example_edn) + kbt_decoded = cbor_utils.decode(kbt_cbor) + + assert cbor_utils.is_tag(kbt_decoded) + assert cbor_utils.get_tag_number(kbt_decoded) == 18 + + kbt_array = cbor_utils.get_tag_value(kbt_decoded) + assert isinstance(kbt_array, list) + assert len(kbt_array) == 4 + + kbt_protected_bytes = kbt_array[0] + kbt_protected = cbor_utils.decode(kbt_protected_bytes) + + assert 13 in kbt_protected + issuer_sd_cwt_cbor_or_data = kbt_protected[13] + + if isinstance(issuer_sd_cwt_cbor_or_data, bytes): + issuer_sd_cwt_cbor = issuer_sd_cwt_cbor_or_data + else: + issuer_sd_cwt_cbor = cbor_utils.encode(issuer_sd_cwt_cbor_or_data) + + from sd_cwt.cose_sign1 import ES256Verifier, ES384Verifier, cose_sign1_verify + + issuer_verifier = ES384Verifier(issuer_x, issuer_y) + issuer_valid, issuer_payload_bytes = cose_sign1_verify(issuer_sd_cwt_cbor, issuer_verifier) + assert issuer_valid, "Issuer SD-CWT signature verification failed" + assert issuer_payload_bytes is not None + + issuer_payload = cbor_utils.decode(issuer_payload_bytes) + assert issuer_payload is not None + assert issuer_payload[1] == "https://issuer.example" + assert issuer_payload[2] == "https://device.example" + + assert 8 in issuer_payload + cnf_claim = issuer_payload[8] + assert 1 in cnf_claim + holder_key_in_cnf = cnf_claim[1] + assert holder_key_in_cnf[-2] == holder_x + assert holder_key_in_cnf[-3] == holder_y + + issuer_sd_cwt_decoded = cbor_utils.decode(issuer_sd_cwt_cbor) + issuer_sd_cwt_array = cbor_utils.get_tag_value(issuer_sd_cwt_decoded) + issuer_unprotected = issuer_sd_cwt_array[1] + assert 17 in issuer_unprotected + disclosures = issuer_unprotected[17] + assert len(disclosures) == 3 + + holder_verifier = ES256Verifier(holder_x, holder_y) + holder_valid, holder_payload_bytes = cose_sign1_verify(kbt_cbor, holder_verifier) + assert holder_valid, "Holder KBT signature verification failed" + assert holder_payload_bytes is not None + + holder_payload = cbor_utils.decode(holder_payload_bytes) + assert holder_payload is not None + assert holder_payload[3] == "https://verifier.example/app" + assert holder_payload[6] == 1725244237 From 4c425f41fdb8b60dda71c94e2f2037bda6949e02 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 1 Nov 2025 12:54:43 -0400 Subject: [PATCH 6/8] redaction verfied --- tests/test_ietf_124.py | 82 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/tests/test_ietf_124.py b/tests/test_ietf_124.py index 68faf42..331b250 100644 --- a/tests/test_ietf_124.py +++ b/tests/test_ietf_124.py @@ -73,7 +73,26 @@ def test_issuer_key_thumbprint(self): def test_minimal_spanning_example(self): - """Test the minimal spanning example from the specification.""" + """Test the minimal spanning example from the specification. + + This test verifies PARTIAL DISCLOSURE where the holder selectively reveals + only some claims while keeping others redacted. + + DISCLOSED (3 disclosures present): + 1. inspector_license_number = "ABCD-123456" (top-level claim 501) + 2. inspection_dates[0] = 1549560720 (7-Feb-2019, array element) + 3. region = "ca" (nested in inspection_location) + + OMITTED (2 disclosures not included): + 1. inspection_dates[1] = 4-Feb-2021 (remains redacted with tag 60) + 2. postal_code in inspection_location (remains redacted) + + ALWAYS VISIBLE (never redacted): + - Standard claims: iss, sub, exp, nbf, iat, cnf + - most_recent_inspection_passed = true + - inspection_dates[2] = 1674004740 (2023-01-17) + - inspection_location.country = "us" + """ holder_key_edn = """ { /kty/ 1 : 2, @@ -265,3 +284,64 @@ def test_minimal_spanning_example(self): assert holder_payload is not None assert holder_payload[3] == "https://verifier.example/app" assert holder_payload[6] == 1725244237 + + disclosure_0 = cbor_utils.decode(disclosures[0]) + assert disclosure_0[0] == bytes.fromhex('bae611067bb823486797da1ebbb52f83') + assert disclosure_0[1] == "ABCD-123456" + assert disclosure_0[2] == 501 + + disclosure_1 = cbor_utils.decode(disclosures[1]) + assert disclosure_1[0] == bytes.fromhex('8de86a012b3043ae6e4457b9e1aaab80') + assert disclosure_1[1] == 1549560720 + assert len(disclosure_1) == 2 + + disclosure_2 = cbor_utils.decode(disclosures[2]) + assert disclosure_2[0] == bytes.fromhex('ec615c3035d5a4ff2f5ae29ded683c8e') + assert disclosure_2[1] == "ca" + assert disclosure_2[2] == "region" + + simple_59 = cbor_utils.create_simple_value(59) + assert simple_59 in issuer_payload + redacted_top_level = issuer_payload[simple_59] + assert len(redacted_top_level) == 1 + expected_inspector_hash = bytes.fromhex('af375dc3fba1d082448642c00be7b2f7bb05c9d8fb61cfc230ddfdfb4616a693') + assert redacted_top_level[0] == expected_inspector_hash + + assert 502 in issuer_payload + inspection_dates = issuer_payload[502] + assert len(inspection_dates) == 3 + + assert cbor_utils.is_tag(inspection_dates[0]) + assert cbor_utils.get_tag_number(inspection_dates[0]) == 60 + redacted_date_1_hash = cbor_utils.get_tag_value(inspection_dates[0]) + assert redacted_date_1_hash == bytes.fromhex('1b7fc8ecf4b1290712497d226c04b503b4aa126c603c83b75d2679c3c613f3fd') + + assert cbor_utils.is_tag(inspection_dates[1]) + assert cbor_utils.get_tag_number(inspection_dates[1]) == 60 + redacted_date_2_hash = cbor_utils.get_tag_value(inspection_dates[1]) + assert redacted_date_2_hash == bytes.fromhex('64afccd3ad52da405329ad935de1fb36814ec48fdfd79e3a108ef858e291e146') + + assert inspection_dates[2] == 1674004740 + + assert 503 in issuer_payload + inspection_location = issuer_payload[503] + assert inspection_location["country"] == "us" + assert simple_59 in inspection_location + redacted_location_keys = inspection_location[simple_59] + assert len(redacted_location_keys) == 2 + expected_region_hash = bytes.fromhex('0d4b8c6123f287a1698ff2db15764564a976fb742606e8fd00e2140656ba0df3') + expected_postal_hash = bytes.fromhex('c0b7747f960fc2e201c4d47c64fee141b78e3ab768ce941863dc8914e8f5815f') + assert redacted_location_keys[0] == expected_region_hash + assert redacted_location_keys[1] == expected_postal_hash + + print("\n✓ PARTIAL DISCLOSURE VERIFIED:") + print(f" • Issuer ES384 signature: VALID") + print(f" • Holder ES256 signature: VALID") + print(f" • Disclosures present: {len(disclosures)}") + print(f" 1. inspector_license_number = {disclosure_0[1]}") + print(f" 2. inspection_dates[0] = {disclosure_1[1]} (7-Feb-2019)") + print(f" 3. region = {disclosure_2[1]}") + print(f" • Omitted disclosures still redacted:") + print(f" 1. inspection_dates[1] with hash {redacted_date_2_hash.hex()[:16]}...") + print(f" 2. postal_code with hash {expected_postal_hash.hex()[:16]}...") + print(f" • Always visible claims: iss, sub, exp, nbf, iat, cnf, country=us") From 45502cd3d3fc4027046cdface66cc935ace2a716 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 1 Nov 2025 13:07:18 -0400 Subject: [PATCH 7/8] linting --- pyproject.toml | 5 +- src/sd_cwt/cddl_utils.py | 4 +- src/sd_cwt/cose_keys.py | 24 ++--- src/sd_cwt/cose_sign1.py | 49 +++------ src/sd_cwt/edn_utils.py | 1 - src/sd_cwt/holder_binding.py | 12 +-- src/sd_cwt/redaction.py | 26 +++-- src/sd_cwt/sd_cwt.py | 54 ++++------ src/sd_cwt/simple_api.py | 104 +++++++++--------- tests/test_appendix_c_keys.py | 124 ++++++++++++++-------- tests/test_edn_api_comprehensive.py | 49 ++++++--- tests/test_edn_redaction_deterministic.py | 29 ++--- tests/test_es256_key_generation.py | 20 ++-- tests/test_ietf_124.py | 70 ++++++++---- tests/test_key_binding_token.py | 12 +-- tests/test_redaction_tags.py | 51 ++++++--- tests/test_resolvers.py | 1 - tests/test_simple_api_workflow.py | 62 +++++------ tests/test_specification_private_keys.py | 64 +++++++---- 19 files changed, 416 insertions(+), 345 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 337da83..e6e1f7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,10 @@ select = [ "UP", # pyupgrade "SIM", # flake8-simplify ] -ignore = ["E501"] # line too long +ignore = [ + "E501", # line too long + "E402", # module level import not at top of file (conflicts with module docstrings) +] [tool.black] line-length = 100 diff --git a/src/sd_cwt/cddl_utils.py b/src/sd_cwt/cddl_utils.py index dc4ad3f..a9f4710 100644 --- a/src/sd_cwt/cddl_utils.py +++ b/src/sd_cwt/cddl_utils.py @@ -27,9 +27,7 @@ def __init__(self, schema: str): def _compile_schema(self) -> None: """Compile the CDDL schema.""" try: - self.validator = zcbor.DataTranslator.from_cddl( - self.schema, default_max_qty=100 - ) + self.validator = zcbor.DataTranslator.from_cddl(self.schema, default_max_qty=100) except Exception as e: print(f"Failed to compile CDDL schema with zcbor: {e}") self.validator = None diff --git a/src/sd_cwt/cose_keys.py b/src/sd_cwt/cose_keys.py index da144f7..15440e0 100644 --- a/src/sd_cwt/cose_keys.py +++ b/src/sd_cwt/cose_keys.py @@ -27,22 +27,22 @@ def cose_key_generate(key_id: Optional[bytes] = None) -> bytes: # Get private key value (32 bytes for P-256) private_value = private_key.private_numbers().private_value - d = private_value.to_bytes(32, byteorder='big') + d = private_value.to_bytes(32, byteorder="big") # Get public key coordinates (32 bytes each for P-256) public_key = private_key.public_key() public_numbers = public_key.public_numbers() - x = public_numbers.x.to_bytes(32, byteorder='big') - y = public_numbers.y.to_bytes(32, byteorder='big') + x = public_numbers.x.to_bytes(32, byteorder="big") + y = public_numbers.y.to_bytes(32, byteorder="big") # Build COSE_Key structure for ES256/P-256 cose_key = { - 1: COSE_KTY_EC2, # kty: EC2 + 1: COSE_KTY_EC2, # kty: EC2 3: COSE_ALG_ES256, # alg: ES256 -1: COSE_CRV_P256, # crv: P-256 - -2: x, # x: x-coordinate - -3: y, # y: y-coordinate - -4: d, # d: private key + -2: x, # x: x-coordinate + -3: y, # y: y-coordinate + -4: d, # d: private key } # Add key ID if provided @@ -52,8 +52,6 @@ def cose_key_generate(key_id: Optional[bytes] = None) -> bytes: return cbor_utils.encode(cose_key) - - def cose_key_from_dict(key_dict: dict[int, Any]) -> bytes: """Convert a COSE key dictionary to CBOR bytes. @@ -119,10 +117,10 @@ def cose_key_thumbprint(cose_key: bytes, hash_algorithm: str = "sha-256") -> byt # For EC2 (P-256): kty, crv, x, y canonical = { - 1: key_dict[1], # kty - -1: key_dict[-1], # crv - -2: key_dict[-2], # x - -3: key_dict[-3], # y + 1: key_dict[1], # kty + -1: key_dict[-1], # crv + -2: key_dict[-2], # x + -3: key_dict[-3], # y } # Encode canonically diff --git a/src/sd_cwt/cose_sign1.py b/src/sd_cwt/cose_sign1.py index 6417864..efd7929 100644 --- a/src/sd_cwt/cose_sign1.py +++ b/src/sd_cwt/cose_sign1.py @@ -174,24 +174,17 @@ def __init__(self, private_key_bytes: bytes): private_key_bytes: The private key bytes (32 bytes for P-256) """ # Create private key from bytes - private_value = int.from_bytes(private_key_bytes, byteorder='big') - self.private_key = ec.derive_private_key( - private_value, - ec.SECP256R1(), - default_backend() - ) + private_value = int.from_bytes(private_key_bytes, byteorder="big") + self.private_key = ec.derive_private_key(private_value, ec.SECP256R1(), default_backend()) def sign(self, message: bytes) -> bytes: """Sign a message with ES256.""" # Sign the message - signature_der = self.private_key.sign( - message, - ec.ECDSA(hashes.SHA256()) - ) + signature_der = self.private_key.sign(message, ec.ECDSA(hashes.SHA256())) # Convert DER to raw (r||s) format for COSE r, s = utils.decode_dss_signature(signature_der) - signature = r.to_bytes(32, byteorder='big') + s.to_bytes(32, byteorder='big') + signature = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") return signature @@ -212,8 +205,8 @@ def __init__(self, public_key_x: bytes, public_key_y: bytes): public_key_y: Y coordinate of public key (32 bytes) """ # Create public key from coordinates - x = int.from_bytes(public_key_x, byteorder='big') - y = int.from_bytes(public_key_y, byteorder='big') + x = int.from_bytes(public_key_x, byteorder="big") + y = int.from_bytes(public_key_y, byteorder="big") public_numbers = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()) self.public_key = public_numbers.public_key(default_backend()) @@ -225,16 +218,12 @@ def verify(self, message: bytes, signature: bytes) -> bool: if len(signature) != 64: return False - r = int.from_bytes(signature[:32], byteorder='big') - s = int.from_bytes(signature[32:], byteorder='big') + r = int.from_bytes(signature[:32], byteorder="big") + s = int.from_bytes(signature[32:], byteorder="big") signature_der = utils.encode_dss_signature(r, s) # Verify the signature - self.public_key.verify( - signature_der, - message, - ec.ECDSA(hashes.SHA256()) - ) + self.public_key.verify(signature_der, message, ec.ECDSA(hashes.SHA256())) return True except InvalidSignature: @@ -253,8 +242,8 @@ def __init__(self, public_key_x: bytes, public_key_y: bytes): public_key_x: X coordinate of public key (48 bytes) public_key_y: Y coordinate of public key (48 bytes) """ - x = int.from_bytes(public_key_x, byteorder='big') - y = int.from_bytes(public_key_y, byteorder='big') + x = int.from_bytes(public_key_x, byteorder="big") + y = int.from_bytes(public_key_y, byteorder="big") public_numbers = ec.EllipticCurvePublicNumbers(x, y, ec.SECP384R1()) self.public_key = public_numbers.public_key(default_backend()) @@ -265,15 +254,11 @@ def verify(self, message: bytes, signature: bytes) -> bool: if len(signature) != 96: return False - r = int.from_bytes(signature[:48], byteorder='big') - s = int.from_bytes(signature[48:], byteorder='big') + r = int.from_bytes(signature[:48], byteorder="big") + s = int.from_bytes(signature[48:], byteorder="big") signature_der = utils.encode_dss_signature(r, s) - self.public_key.verify( - signature_der, - message, - ec.ECDSA(hashes.SHA384()) - ) + self.public_key.verify(signature_der, message, ec.ECDSA(hashes.SHA384())) return True except InvalidSignature: @@ -293,13 +278,13 @@ def generate_es256_key_pair() -> tuple[bytes, bytes, bytes]: # Get private key bytes private_value = private_key.private_numbers().private_value - private_key_bytes = private_value.to_bytes(32, byteorder='big') + private_key_bytes = private_value.to_bytes(32, byteorder="big") # Get public key coordinates public_key = private_key.public_key() public_numbers = public_key.public_numbers() - public_key_x = public_numbers.x.to_bytes(32, byteorder='big') - public_key_y = public_numbers.y.to_bytes(32, byteorder='big') + public_key_x = public_numbers.x.to_bytes(32, byteorder="big") + public_key_y = public_numbers.y.to_bytes(32, byteorder="big") return private_key_bytes, public_key_x, public_key_y diff --git a/src/sd_cwt/edn_utils.py b/src/sd_cwt/edn_utils.py index 756913e..2a340a8 100644 --- a/src/sd_cwt/edn_utils.py +++ b/src/sd_cwt/edn_utils.py @@ -4,7 +4,6 @@ abstracting the underlying cbor-diag library implementation. """ - import cbor_diag # type: ignore[import-untyped] diff --git a/src/sd_cwt/holder_binding.py b/src/sd_cwt/holder_binding.py index 04798e6..dfcd87d 100644 --- a/src/sd_cwt/holder_binding.py +++ b/src/sd_cwt/holder_binding.py @@ -57,7 +57,7 @@ def create_sd_kbt( audience: str, issued_at: int, cnonce: Optional[bytes] = None, - key_id: Optional[bytes] = None + key_id: Optional[bytes] = None, ) -> bytes: """Create an SD-CWT Key Binding Token (SD-KBT). @@ -77,8 +77,8 @@ def create_sd_kbt( """ # SD-KBT payload (CWT Claims Set) kbt_payload = { - 3: audience, # aud - REQUIRED: corresponds to the Verifier - 6: issued_at, # iat - REQUIRED: issued at time + 3: audience, # aud - REQUIRED: corresponds to the Verifier + 6: issued_at, # iat - REQUIRED: issued at time } # Add optional cnonce if provided @@ -88,7 +88,7 @@ def create_sd_kbt( # Protected header for SD-KBT protected_header = { 1: holder_signer.algorithm, # Algorithm - 16: "application/kb+cwt", # typ header parameter (REQUIRED) + 16: "application/kb+cwt", # typ header parameter (REQUIRED) TBD_KCWT: sd_cwt_with_disclosures, # kcwt - contains the full SD-CWT } @@ -107,7 +107,7 @@ def create_sd_kbt( payload_bytes, holder_signer, protected_header=protected_header, - unprotected_header=unprotected_header + unprotected_header=unprotected_header, ) return sd_kbt @@ -117,7 +117,7 @@ def create_sd_cwt_with_mandatory_cnf( base_claims: dict[Any, Any], holder_key: bytes, sd_hashes: list[bytes], - use_thumbprint: bool = False + use_thumbprint: bool = False, ) -> dict[Any, Any]: """Create SD-CWT claims with mandatory cnf claim. diff --git a/src/sd_cwt/redaction.py b/src/sd_cwt/redaction.py index 2189907..a12f08a 100644 --- a/src/sd_cwt/redaction.py +++ b/src/sd_cwt/redaction.py @@ -54,6 +54,7 @@ def __init__(self, seed: int = 42): seed: Integer seed for deterministic salt generation """ import random + self._random = random.Random(seed) def generate_salt(self, length: int = 16) -> bytes: @@ -162,13 +163,13 @@ def find_redacted_claims(claims: dict[Any, Any]) -> list[tuple[list[Any], Any]]: """ # Claims that are mandatory to disclose (MUST NOT be redacted) per specification MANDATORY_TO_DISCLOSE_CLAIMS = { - 1, # iss - issuer - 3, # aud - audience - 4, # exp - expiration - 5, # nbf - not before - 6, # iat - issued at - 7, # cti - CWT ID - 8, # cnf - confirmation (holder binding) + 1, # iss - issuer + 3, # aud - audience + 4, # exp - expiration + 5, # nbf - not before + 6, # iat - issued at + 7, # cti - CWT ID + 8, # cnf - confirmation (holder binding) 39, # cnonce - client nonce } @@ -212,7 +213,7 @@ def _traverse(obj: Any, path: list[Any]) -> None: def process_redactions( claims: dict[Any, Any], redacted_paths: list[tuple[list[Any], Any]], - salt_generator: Optional[SaltGenerator] = None + salt_generator: Optional[SaltGenerator] = None, ) -> tuple[dict[Any, Any], list[bytes], list[bytes]]: """Process redactions and create disclosures. @@ -225,6 +226,7 @@ def process_redactions( Tuple of (redacted_claims, disclosures, map_key_hashes) map_key_hashes: Only the hashes for redacted map keys (for simple(59)) """ + # Manual deep copy to handle CBOR tags def deep_copy_claims(obj: Any) -> Any: if isinstance(obj, dict): @@ -300,10 +302,7 @@ def deep_copy_claims(obj: Any) -> Any: return redacted_claims, disclosures, map_key_hashes -def build_sd_cwt_claims( - claims: dict[Any, Any], - map_key_hashes: list[bytes] -) -> dict[Any, Any]: +def build_sd_cwt_claims(claims: dict[Any, Any], map_key_hashes: list[bytes]) -> dict[Any, Any]: """Build SD-CWT claims with redacted map key hashes. Args: @@ -323,8 +322,7 @@ def build_sd_cwt_claims( def edn_to_redacted_cbor( - edn_string: str, - salt_generator: Optional[SaltGenerator] = None + edn_string: str, salt_generator: Optional[SaltGenerator] = None ) -> tuple[bytes, list[bytes]]: """Convert EDN with redaction tags to redacted CBOR claims. diff --git a/src/sd_cwt/sd_cwt.py b/src/sd_cwt/sd_cwt.py index 6d8374a..c65dc41 100644 --- a/src/sd_cwt/sd_cwt.py +++ b/src/sd_cwt/sd_cwt.py @@ -27,7 +27,7 @@ def create_sd_cwt_with_holder_binding( holder_key: Optional[bytes] = None, use_thumbprint: bool = False, salt_generator: Optional[SaltGenerator] = None, - issuer_key_id: Optional[bytes] = None + issuer_key_id: Optional[bytes] = None, ) -> dict[str, Any]: """Create a complete SD-CWT with mandatory holder binding. @@ -81,17 +81,9 @@ def create_sd_cwt_with_holder_binding( payload = cbor_utils.encode(sd_cwt_claims) # Sign the SD-CWT - sd_cwt = cose_sign1_sign( - payload, - issuer_signer, - protected_header=protected_header - ) + sd_cwt = cose_sign1_sign(payload, issuer_signer, protected_header=protected_header) - return { - "sd_cwt": sd_cwt, - "disclosures": disclosures, - "holder_key": holder_key - } + return {"sd_cwt": sd_cwt, "disclosures": disclosures, "holder_key": holder_key} def create_sd_cwt_presentation( @@ -102,7 +94,7 @@ def create_sd_cwt_presentation( verifier_audience: str, issued_at: int, cnonce: Optional[bytes] = None, - holder_key_id: Optional[bytes] = None + holder_key_id: Optional[bytes] = None, ) -> bytes: """Create an SD-CWT presentation with selected disclosures and SD-KBT. @@ -123,20 +115,12 @@ def create_sd_cwt_presentation( selected_disclosures = [all_disclosures[i] for i in selected_disclosure_indices] # Create SD-CWT with selected disclosures structure - sd_cwt_with_disclosures_dict = { - "sd_cwt": sd_cwt, - "disclosures": selected_disclosures - } + sd_cwt_with_disclosures_dict = {"sd_cwt": sd_cwt, "disclosures": selected_disclosures} sd_cwt_with_disclosures = cbor_utils.encode(sd_cwt_with_disclosures_dict) # Create and return SD-KBT return create_sd_kbt( - sd_cwt_with_disclosures, - holder_signer, - verifier_audience, - issued_at, - cnonce, - holder_key_id + sd_cwt_with_disclosures, holder_signer, verifier_audience, issued_at, cnonce, holder_key_id ) @@ -165,7 +149,7 @@ def validate_sd_cwt_presentation(sd_kbt: bytes) -> dict[str, Any]: "audience": None, "issued_at": None, "cnonce": None, - "errors": [] + "errors": [], } # Validate SD-KBT structure @@ -203,14 +187,16 @@ def validate_sd_cwt_presentation(sd_kbt: bytes) -> dict[str, Any]: return result # Success - result.update({ - "valid": True, - "sd_cwt": sd_cwt, - "disclosures": disclosures, - "audience": extracted_info["aud"], - "issued_at": extracted_info["iat"], - "cnonce": extracted_info["cnonce"] - }) + result.update( + { + "valid": True, + "sd_cwt": sd_cwt, + "disclosures": disclosures, + "audience": extracted_info["aud"], + "issued_at": extracted_info["iat"], + "cnonce": extracted_info["cnonce"], + } + ) except Exception as e: result["errors"].append(f"Validation error: {str(e)}") @@ -233,11 +219,7 @@ def extract_verified_claims(sd_kbt: bytes) -> dict[str, Any]: - claims: Complete claims map with disclosed values (if valid) - errors: List of errors encountered """ - result = { - "valid": False, - "claims": {}, - "errors": [] - } + result = {"valid": False, "claims": {}, "errors": []} # First validate the presentation validation_result = validate_sd_cwt_presentation(sd_kbt) diff --git a/src/sd_cwt/simple_api.py b/src/sd_cwt/simple_api.py index a6dd6a9..df9d837 100644 --- a/src/sd_cwt/simple_api.py +++ b/src/sd_cwt/simple_api.py @@ -2,7 +2,7 @@ import re import time -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Optional from . import cbor_utils from .cose_sign1 import cose_sign1_sign @@ -13,9 +13,8 @@ def select_disclosures_by_claim_names( - disclosures: List[bytes], - claim_names: List[str] -) -> List[bytes]: + disclosures: list[bytes], claim_names: list[str] +) -> list[bytes]: """Select disclosures that match the specified claim names. Args: @@ -41,13 +40,13 @@ def select_disclosures_by_claim_names( def create_edn_with_annotations( - base_claims: Dict[str, Any], - optional_claims: Dict[str, Any], + base_claims: dict[str, Any], + optional_claims: dict[str, Any], issuer: str = "https://issuer.example", subject: str = "https://subject.example", holder_public_key: bytes = None, use_holder_thumbprint: bool = False, - issued_at: Optional[int] = None + issued_at: Optional[int] = None, ) -> str: """Create EDN string with selective disclosure annotations. @@ -115,7 +114,7 @@ def create_edn_with_annotations( return "\n".join(edn_parts) -def _format_cnf_for_edn(cnf_claim: Dict[int, Any]) -> str: +def _format_cnf_for_edn(cnf_claim: dict[int, Any]) -> str: """Format cnf claim for EDN representation.""" if 1 in cnf_claim: # Full COSE key key = cnf_claim[1] @@ -158,7 +157,7 @@ def _format_value_for_edn(value: Any) -> str: class SDCWTIssuer: """Simple API for issuing SD-CWTs with selective disclosure.""" - def __init__(self, issuer_private_key: Dict[int, Any]): + def __init__(self, issuer_private_key: dict[int, Any]): """Initialize with issuer's private key. Args: @@ -169,13 +168,13 @@ def __init__(self, issuer_private_key: Dict[int, Any]): def issue_credential( self, - base_claims: Dict[str, Any], - optional_claims: Dict[str, Any], + base_claims: dict[str, Any], + optional_claims: dict[str, Any], holder_public_key: bytes, issuer: str = "https://issuer.example", subject: str = "https://subject.example", use_holder_thumbprint: bool = False, - ) -> Tuple[bytes, str, List[bytes]]: + ) -> tuple[bytes, str, list[bytes]]: """Issue an SD-CWT credential with selective disclosure. Args: @@ -203,7 +202,7 @@ def issue_credential( issuer=issuer, subject=subject, holder_public_key=holder_public_key, - use_holder_thumbprint=use_holder_thumbprint + use_holder_thumbprint=use_holder_thumbprint, ) # Convert EDN to redacted CBOR @@ -214,7 +213,7 @@ def issue_credential( protected_header = { 1: -7, # ES256 16: "application/sd-cwt", # typ - 4: issuer_thumbprint # kid + 4: issuer_thumbprint, # kid } payload_cbor = cbor_utils.encode(redacted_claims) @@ -223,10 +222,7 @@ def issue_credential( return sd_cwt, edn_string, disclosures -def create_presentation_edn( - original_edn: str, - selected_claims: List[str] -) -> str: +def create_presentation_edn(original_edn: str, selected_claims: list[str]) -> str: """Create a presentation EDN with selected claims disclosed. Args: @@ -243,12 +239,12 @@ def create_presentation_edn( ) """ # Parse the original EDN and rebuild with selected disclosures - lines = original_edn.strip().split('\n') + lines = original_edn.strip().split("\n") presentation_lines = [] for line in lines: line = line.strip() - if not line or line in ['{', '}']: + if not line or line in ["{", "}"]: presentation_lines.append(line) continue @@ -262,21 +258,21 @@ def create_presentation_edn( if disclosed: # Remove tag 58 wrapper for disclosed claims # Transform: "claim": 58(value), -> "claim": value, - clean_line = re.sub(r'58\(([^)]+)\)', r'\1', line) + clean_line = re.sub(r"58\(([^)]+)\)", r"\1", line) presentation_lines.append(clean_line) else: # Keep standard claims (iss, sub, iat, cnf) and non-disclosed claims as-is - if any(std_claim in line for std_claim in ['1:', '2:', '6:', '8:']): + if any(std_claim in line for std_claim in ["1:", "2:", "6:", "8:"]): presentation_lines.append(line) # Skip optional claims that are not selected for disclosure - return '\n'.join(presentation_lines) + return "\n".join(presentation_lines) class SDCWTPresenter: """Simple API for creating SD-CWT presentations.""" - def __init__(self, holder_private_key: Dict[int, Any]): + def __init__(self, holder_private_key: dict[int, Any]): """Initialize with holder's private key. Args: @@ -288,10 +284,10 @@ def __init__(self, holder_private_key: Dict[int, Any]): def create_presentation( self, sd_cwt: bytes, - disclosures: List[bytes], - selected_disclosures: List[bytes], + disclosures: list[bytes], + selected_disclosures: list[bytes], audience: str, - nonce: Optional[str] = None + nonce: Optional[str] = None, ) -> bytes: """Create a presentation with selected claims disclosed. @@ -331,15 +327,13 @@ def create_presentation( audience=audience, issued_at=current_time, cnonce=cnonce, - key_id=holder_thumbprint + key_id=holder_thumbprint, ) return kbt def _create_sd_cwt_with_selected_disclosures( - self, - original_sd_cwt: bytes, - selected_disclosures: List[bytes] + self, original_sd_cwt: bytes, selected_disclosures: list[bytes] ) -> bytes: """Create SD-CWT with only selected disclosures in unprotected header. @@ -380,14 +374,13 @@ def _create_sd_cwt_with_selected_disclosures( protected_header_bytes, new_unprotected, payload_bytes, - signature_bytes + signature_bytes, ] # Re-wrap with tag if original was tagged if cbor_utils.is_tag(cose_sign1): new_cose_sign1 = cbor_utils.create_tag( - cbor_utils.get_tag_number(cose_sign1), - new_cose_sign1_value + cbor_utils.get_tag_number(cose_sign1), new_cose_sign1_value ) else: new_cose_sign1 = new_cose_sign1_value @@ -406,14 +399,12 @@ def __init__(self, public_key_resolver): public_key_resolver: Function that resolves key IDs to COSE keys """ from .verifiers import CredentialVerifier + self.credential_verifier = CredentialVerifier(public_key_resolver) def verify_presentation( - self, - kbt: bytes, - expected_audience: str, - holder_key_resolver=None - ) -> Tuple[bool, Optional[Dict[str, Any]], bool]: + self, kbt: bytes, expected_audience: str, holder_key_resolver=None + ) -> tuple[bool, Optional[dict[str, Any]], bool]: """Verify an SD-CWT presentation and extract claims. Args: @@ -464,9 +455,7 @@ def verify_presentation( # Get presentation verifier for KBT verification presentation_verifier = get_presentation_verifier( - sd_cwt, - self.credential_verifier, - holder_key_resolver + sd_cwt, self.credential_verifier, holder_key_resolver ) if presentation_verifier is None: @@ -486,11 +475,12 @@ def verify_presentation( except Exception as e: import traceback + print(f"Verification error: {e}") # For debugging traceback.print_exc() return False, None, False - def _extract_clean_claims(self, payload: Dict[int, Any]) -> Tuple[Dict[str, Any], bool]: + def _extract_clean_claims(self, payload: dict[int, Any]) -> tuple[dict[str, Any], bool]: """Extract claims and check if redaction tags are absent. Args: @@ -505,13 +495,13 @@ def _extract_clean_claims(self, payload: Dict[int, Any]) -> Tuple[Dict[str, Any] for key, value in payload.items(): # Handle standard JWT claims if key == 1: - clean_claims['iss'] = value + clean_claims["iss"] = value continue elif key == 2: - clean_claims['sub'] = value + clean_claims["sub"] = value continue elif key == 6: - clean_claims['iat'] = value + clean_claims["iat"] = value continue elif key == 8: # cnf claim - don't include in clean claims @@ -533,10 +523,8 @@ def _extract_clean_claims(self, payload: Dict[int, Any]) -> Tuple[Dict[str, Any] return clean_claims, tags_absent def _reconstruct_verified_claims( - self, - sd_cwt_with_disclosures: bytes, - payload: Dict[int, Any] - ) -> Tuple[Dict[str, Any], bool]: + self, sd_cwt_with_disclosures: bytes, payload: dict[int, Any] + ) -> tuple[dict[str, Any], bool]: """Reconstruct verified claims from SD-CWT payload and disclosures. Args: @@ -577,11 +565,11 @@ def _reconstruct_verified_claims( elif isinstance(key, int): # Handle numeric keys by converting to standard claim names if key == 1: - clean_claims['iss'] = value + clean_claims["iss"] = value elif key == 2: - clean_claims['sub'] = value + clean_claims["sub"] = value elif key == 6: - clean_claims['iat'] = value + clean_claims["iat"] = value # Add other numeric key mappings as needed except Exception: @@ -593,7 +581,7 @@ def _reconstruct_verified_claims( return clean_claims, tags_absent - def _clean_value_recursive(self, value: Any) -> Tuple[Any, bool]: + def _clean_value_recursive(self, value: Any) -> tuple[Any, bool]: """Recursively clean a value and check for redaction tags. Returns: @@ -607,11 +595,15 @@ def _clean_value_recursive(self, value: Any) -> Tuple[Any, bool]: if tag_number in [58, 59, 60]: is_clean = False # Return the tag value, cleaned recursively - inner_value, inner_clean = self._clean_value_recursive(cbor_utils.get_tag_value(value)) + inner_value, inner_clean = self._clean_value_recursive( + cbor_utils.get_tag_value(value) + ) return inner_value, inner_clean else: # Other tags - keep as is but check inner value - inner_value, inner_clean = self._clean_value_recursive(cbor_utils.get_tag_value(value)) + inner_value, inner_clean = self._clean_value_recursive( + cbor_utils.get_tag_value(value) + ) return value, inner_clean elif isinstance(value, dict): diff --git a/tests/test_appendix_c_keys.py b/tests/test_appendix_c_keys.py index 8c52c7a..a781532 100644 --- a/tests/test_appendix_c_keys.py +++ b/tests/test_appendix_c_keys.py @@ -68,17 +68,23 @@ def test_es256_holder_private_key_structure(self, es256_holder_private_key_edn): key = cbor_utils.decode(cbor_data) assert isinstance(key, dict) - assert key[1] == 2 # kty: EC2 - assert key[3] == -7 # alg: ES256 (corrected from spec's erroneous -9) - assert key[-1] == 1 # crv: P-256 + assert key[1] == 2 # kty: EC2 + assert key[3] == -7 # alg: ES256 (corrected from spec's erroneous -9) + assert key[-1] == 1 # crv: P-256 assert len(key[-2]) == 32 # X coordinate: 32 bytes for P-256 assert len(key[-3]) == 32 # Y coordinate: 32 bytes for P-256 assert len(key[-4]) == 32 # Private key: 32 bytes for P-256 - expected_x = bytes.fromhex('8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d') - expected_y = bytes.fromhex('4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343') - expected_d = bytes.fromhex('5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5') + expected_x = bytes.fromhex( + "8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d" + ) + expected_y = bytes.fromhex( + "4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343" + ) + expected_d = bytes.fromhex( + "5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5" + ) assert key[-2] == expected_x assert key[-3] == expected_y @@ -92,9 +98,9 @@ def test_es256_holder_public_key_structure(self, es256_holder_public_key_edn): assert isinstance(key, dict) assert len(key) == 4 # Should only have 4 fields (no private key, no alg) assert -4 not in key # No private key component - assert 3 not in key # No alg in thumbprint format - assert key[1] == 2 # kty: EC2 - assert key[-1] == 1 # crv: P-256 + assert 3 not in key # No alg in thumbprint format + assert key[1] == 2 # kty: EC2 + assert key[-1] == 1 # crv: P-256 def test_es384_issuer_private_key_structure(self, es384_issuer_private_key_edn): """Test ES384 issuer private key structure and values from specification C.2.""" @@ -102,18 +108,24 @@ def test_es384_issuer_private_key_structure(self, es384_issuer_private_key_edn): key = cbor_utils.decode(cbor_data) assert isinstance(key, dict) - assert key[1] == 2 # kty: EC2 + assert key[1] == 2 # kty: EC2 assert key[2] == "https://issuer.example/cwk3.cbor" # kid assert key[3] == -35 # alg: ES384 (corrected from spec's erroneous -51) - assert key[-1] == 2 # crv: P-384 + assert key[-1] == 2 # crv: P-384 assert len(key[-2]) == 48 # X coordinate: 48 bytes for P-384 assert len(key[-3]) == 48 # Y coordinate: 48 bytes for P-384 assert len(key[-4]) == 48 # Private key: 48 bytes for P-384 - expected_x = bytes.fromhex('c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf') - expected_y = bytes.fromhex('8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554') - expected_d = bytes.fromhex('71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c') + expected_x = bytes.fromhex( + "c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf" + ) + expected_y = bytes.fromhex( + "8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554" + ) + expected_d = bytes.fromhex( + "71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c" + ) assert key[-2] == expected_x assert key[-3] == expected_y @@ -127,10 +139,10 @@ def test_es384_issuer_public_key_structure(self, es384_issuer_public_key_edn): assert isinstance(key, dict) assert len(key) == 4 # Should only have 4 fields (no kid, no alg, no private key) assert -4 not in key # No private key component - assert 2 not in key # No kid in thumbprint format - assert 3 not in key # No alg in thumbprint format - assert key[1] == 2 # kty: EC2 - assert key[-1] == 2 # crv: P-384 + assert 2 not in key # No kid in thumbprint format + assert 3 not in key # No alg in thumbprint format + assert key[1] == 2 # kty: EC2 + assert key[-1] == 2 # crv: P-384 def test_thumbprint_computation_es256_holder(self, es256_holder_public_key_edn): """Test COSE key thumbprint computation for ES256 holder key matches specification.""" @@ -138,9 +150,13 @@ def test_thumbprint_computation_es256_holder(self, es256_holder_public_key_edn): key = cbor_utils.decode(cbor_data) thumbprint = CoseKeyThumbprint.compute(key, "sha256") - expected_thumbprint = bytes.fromhex('8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0') + expected_thumbprint = bytes.fromhex( + "8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0" + ) - assert thumbprint == expected_thumbprint, f"Expected {expected_thumbprint.hex()}, got {thumbprint.hex()}" + assert ( + thumbprint == expected_thumbprint + ), f"Expected {expected_thumbprint.hex()}, got {thumbprint.hex()}" def test_thumbprint_computation_es384_issuer(self, es384_issuer_public_key_edn): """Test COSE key thumbprint computation for ES384 issuer key matches specification.""" @@ -148,11 +164,17 @@ def test_thumbprint_computation_es384_issuer(self, es384_issuer_public_key_edn): key = cbor_utils.decode(cbor_data) thumbprint = CoseKeyThumbprint.compute(key, "sha256") - expected_thumbprint = bytes.fromhex('554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0') + expected_thumbprint = bytes.fromhex( + "554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0" + ) - assert thumbprint == expected_thumbprint, f"Expected {expected_thumbprint.hex()}, got {thumbprint.hex()}" + assert ( + thumbprint == expected_thumbprint + ), f"Expected {expected_thumbprint.hex()}, got {thumbprint.hex()}" - def test_cbor_roundtrip_all_keys(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + def test_cbor_roundtrip_all_keys( + self, es256_holder_private_key_edn, es384_issuer_private_key_edn + ): """Test CBOR encoding/decoding roundtrip for all keys from specification.""" test_keys = [es256_holder_private_key_edn, es384_issuer_private_key_edn] @@ -165,7 +187,9 @@ def test_cbor_roundtrip_all_keys(self, es256_holder_private_key_edn, es384_issue assert key == re_decoded, "CBOR roundtrip should preserve key structure" - def test_key_validation_required_fields(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + def test_key_validation_required_fields( + self, es256_holder_private_key_edn, es384_issuer_private_key_edn + ): """Test that all keys have required fields according to specification.""" # ES256 holder key cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) @@ -181,7 +205,9 @@ def test_key_validation_required_fields(self, es256_holder_private_key_edn, es38 required_issuer_fields = {1, 2, 3, -1, -2, -3, -4} # kty, kid, alg, crv, x, y, d assert all(field in issuer_key for field in required_issuer_fields) - def test_extract_public_keys_from_private(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + def test_extract_public_keys_from_private( + self, es256_holder_private_key_edn, es384_issuer_private_key_edn + ): """Test extracting public keys from private keys per specification.""" # Extract ES256 holder public key cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) @@ -199,20 +225,22 @@ def test_extract_public_keys_from_private(self, es256_holder_private_key_edn, es assert -4 not in public_key assert len(public_key) == 6 # kty, kid, alg, crv, x, y - def test_algorithm_curve_consistency(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + def test_algorithm_curve_consistency( + self, es256_holder_private_key_edn, es384_issuer_private_key_edn + ): """Test that algorithm and curve parameters are consistent per specification.""" # ES256 with P-256 cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) key = cbor_utils.decode(cbor_data) - assert key[3] == -7 # ES256 - assert key[-1] == 1 # P-256 + assert key[3] == -7 # ES256 + assert key[-1] == 1 # P-256 assert len(key[-2]) == 32 # P-256 coordinates are 32 bytes # ES384 with P-384 cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) key = cbor_utils.decode(cbor_data) assert key[3] == -35 # ES384 - assert key[-1] == 2 # P-384 + assert key[-1] == 2 # P-384 assert len(key[-2]) == 48 # P-384 coordinates are 48 bytes def test_private_key_ranges(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): @@ -220,13 +248,13 @@ def test_private_key_ranges(self, es256_holder_private_key_edn, es384_issuer_pri # ES256 (P-256) cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) key = cbor_utils.decode(cbor_data) - d_int = int.from_bytes(key[-4], 'big') + d_int = int.from_bytes(key[-4], "big") assert 0 < d_int < 2**256 # ES384 (P-384) cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) key = cbor_utils.decode(cbor_data) - d_int = int.from_bytes(key[-4], 'big') + d_int = int.from_bytes(key[-4], "big") assert 0 < d_int < 2**384 def test_coordinate_non_zero(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): @@ -234,16 +262,18 @@ def test_coordinate_non_zero(self, es256_holder_private_key_edn, es384_issuer_pr # ES256 cbor_data = edn_utils.diag_to_cbor(es256_holder_private_key_edn) key = cbor_utils.decode(cbor_data) - assert key[-2] != b'\x00' * 32 # X coordinate not zero - assert key[-3] != b'\x00' * 32 # Y coordinate not zero + assert key[-2] != b"\x00" * 32 # X coordinate not zero + assert key[-3] != b"\x00" * 32 # Y coordinate not zero # ES384 cbor_data = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) key = cbor_utils.decode(cbor_data) - assert key[-2] != b'\x00' * 48 # X coordinate not zero - assert key[-3] != b'\x00' * 48 # Y coordinate not zero + assert key[-2] != b"\x00" * 48 # X coordinate not zero + assert key[-3] != b"\x00" * 48 # Y coordinate not zero - def test_specification_algorithm_corrections(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + def test_specification_algorithm_corrections( + self, es256_holder_private_key_edn, es384_issuer_private_key_edn + ): """Test corrections of specification algorithm errors.""" # The specification shows incorrect algorithm values that we've corrected @@ -268,8 +298,8 @@ def test_cbor_pretty_print_structure(self, es256_holder_public_key_edn): # Should match expected structure from specification lines 2478-2490 assert len(re_decoded) == 4 # map(4) - assert re_decoded[1] == 2 # kty: EC2 - assert re_decoded[-1] == 1 # crv: P-256 + assert re_decoded[1] == 2 # kty: EC2 + assert re_decoded[-1] == 1 # crv: P-256 assert len(re_decoded[-2]) == 32 # X coordinate: 32 bytes assert len(re_decoded[-3]) == 32 # Y coordinate: 32 bytes @@ -284,7 +314,9 @@ def test_issuer_key_id_specification_compliance(self, es384_issuer_private_key_e assert isinstance(key[2], str), "Key ID must be string type" assert key[2].startswith("https://"), "Key ID must be HTTPS URL" - def test_interoperability_export_formats(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + def test_interoperability_export_formats( + self, es256_holder_private_key_edn, es384_issuer_private_key_edn + ): """Test exporting keys in formats suitable for interoperability testing.""" keys = [es256_holder_private_key_edn, es384_issuer_private_key_edn] @@ -297,13 +329,15 @@ def test_interoperability_export_formats(self, es256_holder_private_key_edn, es3 hex_export = cbor_utils.encode(public_key).hex() assert isinstance(hex_export, str) assert len(hex_export) > 0 - assert all(c in '0123456789abcdef' for c in hex_export.lower()) + assert all(c in "0123456789abcdef" for c in hex_export.lower()) # Test EDN export roundtrip edn_export = edn_utils.cbor_to_diag(cbor_utils.encode(public_key)) assert "1:2" in edn_export or "1: 2" in edn_export # kty: EC2 - def test_edn_format_specification_compliance(self, es256_holder_private_key_edn, es384_issuer_private_key_edn): + def test_edn_format_specification_compliance( + self, es256_holder_private_key_edn, es384_issuer_private_key_edn + ): """Test that EDN format parsing works correctly for specification examples.""" # Test holder key EDN holder_cbor = edn_utils.diag_to_cbor(es256_holder_private_key_edn) @@ -314,7 +348,9 @@ def test_edn_format_specification_compliance(self, es256_holder_private_key_edn, holder_roundtrip_cbor = edn_utils.diag_to_cbor(holder_roundtrip_edn) holder_roundtrip_decoded = cbor_utils.decode(holder_roundtrip_cbor) - assert holder_decoded == holder_roundtrip_decoded, "Holder EDN roundtrip should preserve key" + assert ( + holder_decoded == holder_roundtrip_decoded + ), "Holder EDN roundtrip should preserve key" # Test issuer key EDN issuer_cbor = edn_utils.diag_to_cbor(es384_issuer_private_key_edn) @@ -325,4 +361,6 @@ def test_edn_format_specification_compliance(self, es256_holder_private_key_edn, issuer_roundtrip_cbor = edn_utils.diag_to_cbor(issuer_roundtrip_edn) issuer_roundtrip_decoded = cbor_utils.decode(issuer_roundtrip_cbor) - assert issuer_decoded == issuer_roundtrip_decoded, "Issuer EDN roundtrip should preserve key" \ No newline at end of file + assert ( + issuer_decoded == issuer_roundtrip_decoded + ), "Issuer EDN roundtrip should preserve key" diff --git a/tests/test_edn_api_comprehensive.py b/tests/test_edn_api_comprehensive.py index 0cbec1e..024f137 100644 --- a/tests/test_edn_api_comprehensive.py +++ b/tests/test_edn_api_comprehensive.py @@ -19,8 +19,12 @@ def test_complete_edn_workflow_with_deterministic_validation(self): """Test complete workflow with EDN specification, deterministic salting, and validation.""" # Step 1: Define static keys for reproducible testing - issuer_key_cbor = bytes.fromhex("a60102032620012158203884a05a20e85fc48e34b761d651a74ee8a1ba5e11d7e771f1bc611ee84d05e2225820a0ae4064b94ec449d53218086d37f436b8ef60eb1da6ad50bff700c6ecf613cd23582067b8ee92d2bcd650d6d0632534910250aaee4f192b48218077084e6a04560b62") - holder_key_cbor = bytes.fromhex("a601020326200121582091e48079742cce0ef9126cfdc526d395dc2136e40deb8c47638bdcf5d7eaa56422582014279156a8e5afa6524192c16660039edb12d581e0c437bf3faa66dca9a5a278235820024b0357a9a9fa0b236cc2c100a23f2bfd822cbed11ec6bbeefe874a6de324ea") + issuer_key_cbor = bytes.fromhex( + "a60102032620012158203884a05a20e85fc48e34b761d651a74ee8a1ba5e11d7e771f1bc611ee84d05e2225820a0ae4064b94ec449d53218086d37f436b8ef60eb1da6ad50bff700c6ecf613cd23582067b8ee92d2bcd650d6d0632534910250aaee4f192b48218077084e6a04560b62" + ) + holder_key_cbor = bytes.fromhex( + "a601020326200121582091e48079742cce0ef9126cfdc526d395dc2136e40deb8c47638bdcf5d7eaa56422582014279156a8e5afa6524192c16660039edb12d581e0c437bf3faa66dca9a5a278235820024b0357a9a9fa0b236cc2c100a23f2bfd822cbed11ec6bbeefe874a6de324ea" + ) issuer_key_dict = cbor_utils.decode(issuer_key_cbor) holder_key_dict = cbor_utils.decode(holder_key_cbor) @@ -129,11 +133,16 @@ def test_complete_edn_workflow_with_deterministic_validation(self): # Validate specific disclosure values assert disclosure_claims["heat_number"] == "H240115-001" assert disclosure_claims["chemical_composition"] == { - "carbon": 0.25, "manganese": 1.20, "phosphorus": 0.040, "sulfur": 0.050 + "carbon": 0.25, + "manganese": 1.20, + "phosphorus": 0.040, + "sulfur": 0.050, } assert disclosure_claims["production_cost"] == 850.75 assert disclosure_claims["quality_test_results"] == { - "tensile_strength": 420, "yield_strength": 350, "elongation": 18.5 + "tensile_strength": 420, + "yield_strength": 350, + "elongation": 18.5, } # Step 7: Test hash consistency @@ -159,10 +168,11 @@ def test_complete_edn_workflow_with_deterministic_validation(self): protected_header = { 1: -7, # ES256 16: "application/sd-cwt", # typ - 4: issuer_thumbprint # kid + 4: issuer_thumbprint, # kid } from sd_cwt.cose_sign1 import cose_sign1_sign + sd_cwt = cose_sign1_sign(cbor_bytes, issuer.signer, protected_header=protected_header) # Step 9: Test presentation with selective disclosure @@ -185,7 +195,7 @@ def test_complete_edn_workflow_with_deterministic_validation(self): disclosures=disclosures, selected_disclosures=selected_disclosures, audience="https://customs.us.example", - nonce="test_nonce_123" + nonce="test_nonce_123", ) # Step 10: Test verification @@ -195,8 +205,7 @@ def test_complete_edn_workflow_with_deterministic_validation(self): verifier = SDCWTVerifier(issuer_resolver) is_valid, verified_claims, tags_absent = verifier.verify_presentation( - kbt=kbt, - expected_audience="https://customs.us.example" + kbt=kbt, expected_audience="https://customs.us.example" ) # Step 11: Validate verification results @@ -214,7 +223,10 @@ def test_complete_edn_workflow_with_deterministic_validation(self): # Validate selected claims are disclosed assert verified_claims["heat_number"] == "H240115-001" assert verified_claims["chemical_composition"] == { - "carbon": 0.25, "manganese": 1.20, "phosphorus": 0.040, "sulfur": 0.050 + "carbon": 0.25, + "manganese": 1.20, + "phosphorus": 0.040, + "sulfur": 0.050, } # Validate non-selected claims are NOT disclosed @@ -311,28 +323,30 @@ def test_hex_and_dictionary_validation(self): # Hex validation hex_string = cbor_bytes.hex() assert len(hex_string) % 2 == 0 # Even length - assert all(c in '0123456789abcdef' for c in hex_string) + assert all(c in "0123456789abcdef" for c in hex_string) # Specific hex pattern validation (deterministic with seed 123) - assert hex_string.startswith('a') # CBOR map marker + assert hex_string.startswith("a") # CBOR map marker # Dictionary validation decoded = cbor_utils.decode(cbor_bytes) # Validate specific CBOR data model instances validation_dict = { - "string_claim": (False, str), # Should be redacted - "number_claim": (False, int), # Should be redacted - "boolean_claim": (False, bool), # Should be redacted - "array_claim": (True, list), # Should be present (contains tag 60) - "object_claim": (True, dict), # Should be present (nested "private" redacted) + "string_claim": (False, str), # Should be redacted + "number_claim": (False, int), # Should be redacted + "boolean_claim": (False, bool), # Should be redacted + "array_claim": (True, list), # Should be present (contains tag 60) + "object_claim": (True, dict), # Should be present (nested "private" redacted) cbor_utils.create_simple_value(59): (True, list), # Redacted hashes } for key, (should_be_present, expected_type) in validation_dict.items(): if should_be_present: assert key in decoded, f"Key {key} should be present" - assert isinstance(decoded[key], expected_type), f"Key {key} should be {expected_type}" + assert isinstance( + decoded[key], expected_type + ), f"Key {key} should be {expected_type}" else: assert key not in decoded, f"Key {key} should be redacted" @@ -411,6 +425,7 @@ def test_specification_compliance_validation(self): # Test hash algorithm consistency (SHA-256) from sd_cwt.redaction import hash_disclosure + computed_hash = hash_disclosure(disclosures[0]) assert len(computed_hash) == 32 # SHA-256 = 32 bytes diff --git a/tests/test_edn_redaction_deterministic.py b/tests/test_edn_redaction_deterministic.py index a0247d8..33ad0c3 100644 --- a/tests/test_edn_redaction_deterministic.py +++ b/tests/test_edn_redaction_deterministic.py @@ -304,7 +304,7 @@ def test_hex_validation_deterministic(self): cbor_hex = cbor_bytes.hex() # Validate it's a proper hex string - assert all(c in '0123456789abcdef' for c in cbor_hex) + assert all(c in "0123456789abcdef" for c in cbor_hex) assert len(cbor_hex) % 2 == 0 # Even length # Should be reproducible with same seed @@ -405,7 +405,7 @@ def test_cbor_data_model_validation(self): 1: str, # iss - string 2: str, # sub - string 6: int, # iat - integer - 8: dict, # cnf - map + 8: dict, # cnf - map cbor_utils.create_simple_value(59): list, # redacted keys array } @@ -478,7 +478,9 @@ def test_edn_serialization_examples_for_developers(self): for _i, date in enumerate(inspection_dates): if cbor_utils.is_tag(date) and cbor_utils.get_tag_number(date) == 60: hash_value = cbor_utils.get_tag_value(date) - formatted_dates.append(f'60(h\'{hash_value.hex()[:16]}...\')') # Show first 16 hex chars + formatted_dates.append( + f"60(h'{hash_value.hex()[:16]}...')" + ) # Show first 16 hex chars else: formatted_dates.append(str(date)) @@ -486,7 +488,7 @@ def test_edn_serialization_examples_for_developers(self): hash_array = redacted_payload[simple_59] formatted_hashes = [] for hash_bytes in hash_array: - formatted_hashes.append(f'h\'{hash_bytes.hex()[:16]}...\'') # Show first 16 hex chars + formatted_hashes.append(f"h'{hash_bytes.hex()[:16]}...'") # Show first 16 hex chars redacted_edn_display = f"""{{ "iss": "https://issuer.example", @@ -517,7 +519,9 @@ def test_edn_serialization_examples_for_developers(self): print("\n=== VALIDATION CHECKS ===") # Check that tag 58 map keys became simple(59) entries - assert simple_59 in redacted_payload, "simple(59) should contain hashes of redacted map keys" + assert ( + simple_59 in redacted_payload + ), "simple(59) should contain hashes of redacted map keys" assert len(redacted_payload[simple_59]) == 2, "Should have 2 redacted map keys" print("✓ Tag 58 map keys -> simple(59) hash array") @@ -611,17 +615,17 @@ def test_disclosed_payload_reconstruction_example(self): print(f"Metrics array: {redacted_payload['metrics']}") # Show which items were redacted - metrics = redacted_payload['metrics'] + metrics = redacted_payload["metrics"] metrics_display = [] for item in metrics: if cbor_utils.is_tag(item) and cbor_utils.get_tag_number(item) == 60: hash_val = cbor_utils.get_tag_value(item) - metrics_display.append(f'60(h\'{hash_val.hex()[:12]}...\')') + metrics_display.append(f"60(h'{hash_val.hex()[:12]}...')") else: metrics_display.append(str(item)) hash_array = redacted_payload[simple_59] - hash_display = [f'h\'{h.hex()[:12]}...\'' for h in hash_array] + hash_display = [f"h'{h.hex()[:12]}...'" for h in hash_array] print(f"Metrics with tag 60: [{', '.join(metrics_display)}]") print(f"Simple(59) hashes: [{', '.join(hash_display)}]") @@ -746,9 +750,9 @@ def test_complete_before_after_edn_comparison(self): cbor_bytes, disclosures = edn_to_redacted_cbor(original_edn, salt_gen) redacted_payload = cbor_utils.decode(cbor_bytes) - print("\n" + "="*80) + print("\n" + "=" * 80) print("COMPLETE BEFORE/AFTER EDN TRANSFORMATION COMPARISON") - print("="*80) + print("=" * 80) print("\n>>> BEFORE: Original EDN with tag 58 annotations <<<") print(original_edn.strip()) @@ -763,7 +767,7 @@ def test_complete_before_after_edn_comparison(self): for item in measurements: if cbor_utils.is_tag(item) and cbor_utils.get_tag_number(item) == 60: hash_val = cbor_utils.get_tag_value(item) - measurements_formatted.append(f'60(h\'{hash_val.hex()[:8]}...\')') + measurements_formatted.append(f"60(h'{hash_val.hex()[:8]}...')") else: measurements_formatted.append(str(item)) @@ -771,7 +775,7 @@ def test_complete_before_after_edn_comparison(self): hash_array = redacted_payload[simple_59] formatted_hashes = [] for hash_bytes in hash_array: - formatted_hashes.append(f'h\'{hash_bytes.hex()[:8]}...\'') + formatted_hashes.append(f"h'{hash_bytes.hex()[:8]}...'") redacted_edn_display = f"""{{ "iss": "{redacted_payload['iss']}", @@ -863,4 +867,3 @@ def test_complete_before_after_edn_comparison(self): print("\n✓ Complete before/after comparison successful!") print("✓ Developers can see exact EDN transformation with CBOR exports") - diff --git a/tests/test_es256_key_generation.py b/tests/test_es256_key_generation.py index 4337864..a5462fd 100644 --- a/tests/test_es256_key_generation.py +++ b/tests/test_es256_key_generation.py @@ -76,7 +76,7 @@ def test_export_private_key_as_cbor(self): # Test hex representation for interoperability hex_cbor = private_key_cbor.hex() assert isinstance(hex_cbor, str) - assert all(c in '0123456789abcdef' for c in hex_cbor.lower()) + assert all(c in "0123456789abcdef" for c in hex_cbor.lower()) # Verify hex roundtrip from_hex = bytes.fromhex(hex_cbor) @@ -285,15 +285,15 @@ def test_key_component_validation(self): private_key = key[-4] # Coordinates should not be all zeros or all ones - assert x_coord != b'\x00' * 32, "X coordinate should not be all zeros" - assert y_coord != b'\x00' * 32, "Y coordinate should not be all zeros" - assert private_key != b'\x00' * 32, "Private key should not be all zeros" - assert x_coord != b'\xff' * 32, "X coordinate should not be all ones" - assert y_coord != b'\xff' * 32, "Y coordinate should not be all ones" - assert private_key != b'\xff' * 32, "Private key should not be all ones" + assert x_coord != b"\x00" * 32, "X coordinate should not be all zeros" + assert y_coord != b"\x00" * 32, "Y coordinate should not be all zeros" + assert private_key != b"\x00" * 32, "Private key should not be all zeros" + assert x_coord != b"\xff" * 32, "X coordinate should not be all ones" + assert y_coord != b"\xff" * 32, "Y coordinate should not be all ones" + assert private_key != b"\xff" * 32, "Private key should not be all ones" # Private key should be in valid range (1 to n-1 where n is curve order) - private_int = int.from_bytes(private_key, 'big') + private_int = int.from_bytes(private_key, "big") assert private_int > 0, "Private key should be positive" # P-256 curve order (approximately) p256_order = 2**256 - 2**224 + 2**192 + 2**96 - 1 @@ -324,10 +324,10 @@ def test_specification_compatibility(self): assert "-3:" in edn_output, "EDN should contain y field" # Hex encoding should be lowercase and valid - for line in edn_output.split('\n'): + for line in edn_output.split("\n"): if "h'" in line: hex_part = line.split("h'")[1].split("'")[0] - assert all(c in '0123456789abcdef' for c in hex_part.lower()) + assert all(c in "0123456789abcdef" for c in hex_part.lower()) def test_error_conditions(self): """Test error conditions and edge cases.""" diff --git a/tests/test_ietf_124.py b/tests/test_ietf_124.py index 331b250..cf78841 100644 --- a/tests/test_ietf_124.py +++ b/tests/test_ietf_124.py @@ -21,23 +21,30 @@ def test_holder_key_thumbprint(self): } """ - cbor_data = edn_utils.diag_to_cbor(holder_key_edn) holder_key_dict = cbor_utils.decode(cbor_data) assert holder_key_dict is not None assert holder_key_dict[1] == 2 assert holder_key_dict[3] == -7 assert holder_key_dict[-1] == 1 - expected_x = bytes.fromhex('8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d') - expected_y = bytes.fromhex('4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343') - expected_d = bytes.fromhex('5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5') + expected_x = bytes.fromhex( + "8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d" + ) + expected_y = bytes.fromhex( + "4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343" + ) + expected_d = bytes.fromhex( + "5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5" + ) assert holder_key_dict[-2] == expected_x assert holder_key_dict[-3] == expected_y assert holder_key_dict[-4] == expected_d thumbprint = cose_key_thumbprint(cbor_data) assert thumbprint is not None - assert thumbprint == bytes.fromhex('8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0') + assert thumbprint == bytes.fromhex( + "8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0" + ) def test_issuer_key_thumbprint(self): """Test key import and thumbprint generation for the issuer key.""" @@ -62,15 +69,24 @@ def test_issuer_key_thumbprint(self): assert issuer_key_dict[1] == 2 assert issuer_key_dict[3] == -51 assert issuer_key_dict[-1] == 2 - expected_x = bytes.fromhex('c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf') - expected_y = bytes.fromhex('8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554') - expected_d = bytes.fromhex('71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c') + expected_x = bytes.fromhex( + "c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf" + ) + expected_y = bytes.fromhex( + "8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554" + ) + expected_d = bytes.fromhex( + "71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c" + ) assert issuer_key_dict[-2] == expected_x + assert issuer_key_dict[-3] == expected_y + assert issuer_key_dict[-4] == expected_d thumbprint = cose_key_thumbprint(cbor_data) assert thumbprint is not None - assert thumbprint == bytes.fromhex('554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0') - + assert thumbprint == bytes.fromhex( + "554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0" + ) def test_minimal_spanning_example(self): """Test the minimal spanning example from the specification. @@ -286,17 +302,17 @@ def test_minimal_spanning_example(self): assert holder_payload[6] == 1725244237 disclosure_0 = cbor_utils.decode(disclosures[0]) - assert disclosure_0[0] == bytes.fromhex('bae611067bb823486797da1ebbb52f83') + assert disclosure_0[0] == bytes.fromhex("bae611067bb823486797da1ebbb52f83") assert disclosure_0[1] == "ABCD-123456" assert disclosure_0[2] == 501 disclosure_1 = cbor_utils.decode(disclosures[1]) - assert disclosure_1[0] == bytes.fromhex('8de86a012b3043ae6e4457b9e1aaab80') + assert disclosure_1[0] == bytes.fromhex("8de86a012b3043ae6e4457b9e1aaab80") assert disclosure_1[1] == 1549560720 assert len(disclosure_1) == 2 disclosure_2 = cbor_utils.decode(disclosures[2]) - assert disclosure_2[0] == bytes.fromhex('ec615c3035d5a4ff2f5ae29ded683c8e') + assert disclosure_2[0] == bytes.fromhex("ec615c3035d5a4ff2f5ae29ded683c8e") assert disclosure_2[1] == "ca" assert disclosure_2[2] == "region" @@ -304,7 +320,9 @@ def test_minimal_spanning_example(self): assert simple_59 in issuer_payload redacted_top_level = issuer_payload[simple_59] assert len(redacted_top_level) == 1 - expected_inspector_hash = bytes.fromhex('af375dc3fba1d082448642c00be7b2f7bb05c9d8fb61cfc230ddfdfb4616a693') + expected_inspector_hash = bytes.fromhex( + "af375dc3fba1d082448642c00be7b2f7bb05c9d8fb61cfc230ddfdfb4616a693" + ) assert redacted_top_level[0] == expected_inspector_hash assert 502 in issuer_payload @@ -314,12 +332,16 @@ def test_minimal_spanning_example(self): assert cbor_utils.is_tag(inspection_dates[0]) assert cbor_utils.get_tag_number(inspection_dates[0]) == 60 redacted_date_1_hash = cbor_utils.get_tag_value(inspection_dates[0]) - assert redacted_date_1_hash == bytes.fromhex('1b7fc8ecf4b1290712497d226c04b503b4aa126c603c83b75d2679c3c613f3fd') + assert redacted_date_1_hash == bytes.fromhex( + "1b7fc8ecf4b1290712497d226c04b503b4aa126c603c83b75d2679c3c613f3fd" + ) assert cbor_utils.is_tag(inspection_dates[1]) assert cbor_utils.get_tag_number(inspection_dates[1]) == 60 redacted_date_2_hash = cbor_utils.get_tag_value(inspection_dates[1]) - assert redacted_date_2_hash == bytes.fromhex('64afccd3ad52da405329ad935de1fb36814ec48fdfd79e3a108ef858e291e146') + assert redacted_date_2_hash == bytes.fromhex( + "64afccd3ad52da405329ad935de1fb36814ec48fdfd79e3a108ef858e291e146" + ) assert inspection_dates[2] == 1674004740 @@ -329,19 +351,23 @@ def test_minimal_spanning_example(self): assert simple_59 in inspection_location redacted_location_keys = inspection_location[simple_59] assert len(redacted_location_keys) == 2 - expected_region_hash = bytes.fromhex('0d4b8c6123f287a1698ff2db15764564a976fb742606e8fd00e2140656ba0df3') - expected_postal_hash = bytes.fromhex('c0b7747f960fc2e201c4d47c64fee141b78e3ab768ce941863dc8914e8f5815f') + expected_region_hash = bytes.fromhex( + "0d4b8c6123f287a1698ff2db15764564a976fb742606e8fd00e2140656ba0df3" + ) + expected_postal_hash = bytes.fromhex( + "c0b7747f960fc2e201c4d47c64fee141b78e3ab768ce941863dc8914e8f5815f" + ) assert redacted_location_keys[0] == expected_region_hash assert redacted_location_keys[1] == expected_postal_hash print("\n✓ PARTIAL DISCLOSURE VERIFIED:") - print(f" • Issuer ES384 signature: VALID") - print(f" • Holder ES256 signature: VALID") + print(" • Issuer ES384 signature: VALID") + print(" • Holder ES256 signature: VALID") print(f" • Disclosures present: {len(disclosures)}") print(f" 1. inspector_license_number = {disclosure_0[1]}") print(f" 2. inspection_dates[0] = {disclosure_1[1]} (7-Feb-2019)") print(f" 3. region = {disclosure_2[1]}") - print(f" • Omitted disclosures still redacted:") + print(" • Omitted disclosures still redacted:") print(f" 1. inspection_dates[1] with hash {redacted_date_2_hash.hex()[:16]}...") print(f" 2. postal_code with hash {expected_postal_hash.hex()[:16]}...") - print(f" • Always visible claims: iss, sub, exp, nbf, iat, cnf, country=us") + print(" • Always visible claims: iss, sub, exp, nbf, iat, cnf, country=us") diff --git a/tests/test_key_binding_token.py b/tests/test_key_binding_token.py index ed3c67d..631ac45 100644 --- a/tests/test_key_binding_token.py +++ b/tests/test_key_binding_token.py @@ -413,9 +413,9 @@ def test_complete_verification_chain(self): # Step 6: Verify audience was correctly validated during verification actual_audience = kbt_payload[3] # aud claim - assert actual_audience == expected_audience, ( - f"Audience should match: expected {expected_audience}, got {actual_audience}" - ) + assert ( + actual_audience == expected_audience + ), f"Audience should match: expected {expected_audience}, got {actual_audience}" # Additional verification: ensure KBT contains the original SD-CWT is_valid, _ = validate_sd_kbt_structure(sd_kbt) @@ -514,9 +514,9 @@ def test_ckt_based_kbt_verification(self): presentation_verifier = get_presentation_verifier( sd_cwt, credential_verifier, holder_resolver ) - assert presentation_verifier is not None, ( - "Should create presentation verifier for ckt-based cnf" - ) + assert ( + presentation_verifier is not None + ), "Should create presentation verifier for ckt-based cnf" # Step 5: Create KBT using holder's private key holder_signer = PresentationSigner(holder_key_dict) diff --git a/tests/test_redaction_tags.py b/tests/test_redaction_tags.py index a5cae77..ab6648e 100644 --- a/tests/test_redaction_tags.py +++ b/tests/test_redaction_tags.py @@ -1,6 +1,5 @@ """Test redaction process using tags 58, 59, and 60.""" - from sd_cwt import cbor_utils from sd_cwt.redaction import SeededSaltGenerator, edn_to_redacted_cbor @@ -11,7 +10,7 @@ class TestRedactionTags: def test_tag_58_to_59_and_60_transformation(self): """Test that tag 58 transforms to tag 59 for map keys and tag 60 for array elements.""" # EDN with both redactable map key and redactable array element using tag 58 - edn_with_redactables = ''' + edn_with_redactables = """ { "public_key": "visible_value", "private_key": 58("secret_value"), @@ -22,7 +21,7 @@ def test_tag_58_to_59_and_60_transformation(self): ], "another_field": "also_visible" } - ''' + """ # Use seeded generator for deterministic results seeded_gen = SeededSaltGenerator(seed=12345) @@ -43,10 +42,14 @@ def test_tag_58_to_59_and_60_transformation(self): assert len(claims["data_array"]) == 3, "Array should maintain original length" assert "item1" in claims["data_array"], "Non-redacted array element should remain" assert "item3" in claims["data_array"], "Non-redacted array element should remain" - assert "secret_item" not in claims["data_array"], "Original redacted value should not be present" + assert ( + "secret_item" not in claims["data_array"] + ), "Original redacted value should not be present" # Find the tag 60 element in the array - tag_60_elements = [item for item in claims["data_array"] if cbor_utils.is_tag(item) and item.tag == 60] + tag_60_elements = [ + item for item in claims["data_array"] if cbor_utils.is_tag(item) and item.tag == 60 + ] assert len(tag_60_elements) == 1, "Should have exactly one tag 60 element in array" assert isinstance(tag_60_elements[0].value, bytes), "Tag 60 should wrap a hash (bytes)" @@ -95,7 +98,7 @@ def test_tag_58_to_59_and_60_transformation(self): def test_tag_meaning_explanation(self): """Test that demonstrates the meaning of each tag clearly.""" # Before redaction: both use tag 58 - edn_before_redaction = ''' + edn_before_redaction = """ { "visible": "data", "to_be_redacted_key": 58("hidden_key_value"), @@ -104,7 +107,7 @@ def test_tag_meaning_explanation(self): 58("hidden_array_value") ] } - ''' + """ seeded_gen = SeededSaltGenerator(seed=999) cbor_claims, disclosures = edn_to_redacted_cbor(edn_before_redaction, seeded_gen) @@ -129,16 +132,20 @@ def test_tag_meaning_explanation(self): # Verify the transformation assert "to_be_redacted_key" not in claims, "Map key with tag 58 should be redacted" assert "visible" in claims, "Non-tagged items should remain visible" - assert len(claims["list"]) == 2, "Array should maintain original length (visible item + tag 60 hash)" + assert ( + len(claims["list"]) == 2 + ), "Array should maintain original length (visible item + tag 60 hash)" assert "visible_item" in claims["list"], "Non-tagged array items should remain" # Verify the array contains a tag 60 wrapped hash - tag_60_in_list = [item for item in claims["list"] if cbor_utils.is_tag(item) and item.tag == 60] + tag_60_in_list = [ + item for item in claims["list"] if cbor_utils.is_tag(item) and item.tag == 60 + ] assert len(tag_60_in_list) == 1, "Array should contain exactly one tag 60 element" def test_multiple_redactions_with_different_types(self): """Test multiple redactions showing tag transformations.""" - edn_complex = ''' + edn_complex = """ { "issuer": "https://example.com", "subject": "user123", @@ -156,7 +163,7 @@ def test_multiple_redactions_with_different_types(self): "hidden": 58("nested_secret") } } - ''' + """ seeded_gen = SeededSaltGenerator(seed=777) cbor_claims, disclosures = edn_to_redacted_cbor(edn_complex, seeded_gen) @@ -174,8 +181,12 @@ def test_multiple_redactions_with_different_types(self): original_array_length = 5 # public1, secret1, public2, secret2, public3 redacted_array_length = len(claims["mixed_array"]) assert original_array_length == redacted_array_length, "Array length should be preserved" - tag_60_count = len([item for item in claims["mixed_array"] if cbor_utils.is_tag(item) and item.tag == 60]) - print(f" Array elements (tag 58 → tag 60): {tag_60_count} items replaced with tag 60 hashes") + tag_60_count = len( + [item for item in claims["mixed_array"] if cbor_utils.is_tag(item) and item.tag == 60] + ) + print( + f" Array elements (tag 58 → tag 60): {tag_60_count} items replaced with tag 60 hashes" + ) # Only map keys get hashes in simple(59) - array elements use tag 60 in-place total_redacted_map_keys = 3 # secret_field1, secret_field2, nested.hidden @@ -196,8 +207,12 @@ def test_multiple_redactions_with_different_types(self): assert "secret_array_item2" not in claims["mixed_array"] # Count tag 60 elements in the array - tag_60_elements_in_mixed_array = [item for item in claims["mixed_array"] if cbor_utils.is_tag(item) and item.tag == 60] - assert len(tag_60_elements_in_mixed_array) == 2, "Should have exactly 2 tag 60 elements replacing redacted items" + tag_60_elements_in_mixed_array = [ + item for item in claims["mixed_array"] if cbor_utils.is_tag(item) and item.tag == 60 + ] + assert ( + len(tag_60_elements_in_mixed_array) == 2 + ), "Should have exactly 2 tag 60 elements replacing redacted items" # Verify disclosures created total_expected_disclosures = 5 # 3 map keys + 2 array elements @@ -213,7 +228,7 @@ def test_tag_semantics_documentation(self): documentation = { 58: "To-be-redacted marker (EDN input only)", 59: "Redacted map key hashes (CBOR output, simple value)", - 60: "Redacted array element marker (conceptual, for in-place replacement)" + 60: "Redacted array element marker (conceptual, for in-place replacement)", } print("SD-CWT Redaction Tag Semantics:") @@ -250,7 +265,9 @@ def test_tag_semantics_documentation(self): assert len(claims["arr"]) == 1, "Array should maintain length with tag 60 replacement" # Verify the array contains a tag 60 element - tag_60_in_arr = [item for item in claims["arr"] if cbor_utils.is_tag(item) and item.tag == 60] + tag_60_in_arr = [ + item for item in claims["arr"] if cbor_utils.is_tag(item) and item.tag == 60 + ] assert len(tag_60_in_arr) == 1, "Array should contain exactly one tag 60 element" assert len(disclosures) == 2, "Should have disclosures for both redacted items" diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index 8344197..9a46346 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -142,7 +142,6 @@ def test_credential_verifier_with_thumbprint_resolver(self): assert is_valid2, "Verifier2 should verify SD-CWT2" assert payload2[1] == "https://issuer2.example" - def test_end_to_end_resolution_workflow(self): """Test complete resolution workflow from key pairs to SD-CWT verification.""" # Step 1: Set up multiple issuer keys diff --git a/tests/test_simple_api_workflow.py b/tests/test_simple_api_workflow.py index c69c067..2cf055d 100644 --- a/tests/test_simple_api_workflow.py +++ b/tests/test_simple_api_workflow.py @@ -1,6 +1,5 @@ """Test the simple API workflow from annotation to presentation verification.""" - from sd_cwt import ( SDCWTIssuer, SDCWTPresenter, @@ -48,14 +47,14 @@ def test_complete_workflow_guide_example(self): "carbon": 0.25, "manganese": 1.20, "phosphorus": 0.040, - "sulfur": 0.050 + "sulfur": 0.050, }, "production_cost": 850.75, "quality_test_results": { "tensile_strength": 420, "yield_strength": 350, - "elongation": 18.5 - } + "elongation": 18.5, + }, } print("✓ Defined mandatory and optional claims") @@ -68,7 +67,7 @@ def test_complete_workflow_guide_example(self): optional_claims=optional_claims, holder_public_key=holder_public_key_cbor, issuer="https://steel-manufacturer.example", - subject="https://customs-broker.example" + subject="https://customs-broker.example", ) assert isinstance(sd_cwt, bytes), "SD-CWT should be bytes" @@ -92,7 +91,7 @@ def test_complete_workflow_guide_example(self): disclosures=disclosures, selected_disclosures=selected_disclosures, audience="https://customs.us.example", - nonce="1234567890" + nonce="1234567890", ) assert isinstance(kbt, bytes), "KBT should be bytes" @@ -111,8 +110,7 @@ def test_complete_workflow_guide_example(self): # Step 6: Verify presentation is_valid, verified_claims, tags_absent = verifier.verify_presentation( - kbt=kbt, - expected_audience="https://customs.us.example" + kbt=kbt, expected_audience="https://customs.us.example" ) assert is_valid, "Presentation should be cryptographically valid" @@ -134,7 +132,9 @@ def test_complete_workflow_guide_example(self): # Step 8: Check that base claims (mandatory to disclose) are present for claim_name in base_claims: assert claim_name in verified_claims, f"Base claim {claim_name} should be present" - assert verified_claims[claim_name] == base_claims[claim_name], f"Base claim {claim_name} should match" + assert ( + verified_claims[claim_name] == base_claims[claim_name] + ), f"Base claim {claim_name} should match" print("✓ Base claims (mandatory to disclose) present and correct") @@ -147,7 +147,9 @@ def test_complete_workflow_guide_example(self): # Step 10: Check that non-selected optional claims are NOT present non_selected_claims = [claim for claim in optional_claims if claim not in selected_claims] for claim_name in non_selected_claims: - assert claim_name not in verified_claims, f"Non-selected claim {claim_name} should not be disclosed" + assert ( + claim_name not in verified_claims + ), f"Non-selected claim {claim_name} should not be disclosed" print("✓ Non-selected optional claims are properly redacted") @@ -172,7 +174,7 @@ def test_edn_annotation_workflow(self): edn = create_edn_with_annotations( base_claims=base_claims, optional_claims=optional_claims, - holder_public_key=holder_public_key_cbor + holder_public_key=holder_public_key_cbor, ) assert isinstance(edn, str), "EDN should be string" @@ -184,7 +186,7 @@ def test_edn_annotation_workflow(self): def test_presentation_edn_cleaning(self): """Test EDN cleaning for presentations.""" - original_edn = ''' + original_edn = """ { 1: "https://issuer.example", 2: "https://subject.example", @@ -194,7 +196,7 @@ def test_presentation_edn_cleaning(self): "heat_number": 58("H240115-001"), "cost": 58(850.75) } - '''.strip() + """.strip() selected_claims = ["heat_number"] @@ -202,8 +204,9 @@ def test_presentation_edn_cleaning(self): assert isinstance(presentation_edn, str), "Presentation EDN should be string" assert '"heat_number": "H240115-001"' in presentation_edn, "Selected claims should be clean" - assert "58(" not in presentation_edn or presentation_edn.count("58(") < original_edn.count("58("), \ - "Tag 58 should be removed from selected claims" + assert "58(" not in presentation_edn or presentation_edn.count("58(") < original_edn.count( + "58(" + ), "Tag 58 should be removed from selected claims" print("✓ Presentation EDN cleaning test passed") @@ -215,6 +218,7 @@ def test_tag_absence_verification(self): # Create a mock verifier to test tag detection def mock_resolver(kid): return {"mock": "key"} + verifier = SDCWTVerifier(mock_resolver) # Test payload with no redaction tags (clean) @@ -224,7 +228,7 @@ def mock_resolver(kid): 6: 1725244200, 8: {"cnf": "data"}, "production_date": "2024-01-15", - "heat_number": "H240115-001" + "heat_number": "H240115-001", } clean_claims, tags_absent = verifier._extract_clean_claims(clean_payload) @@ -241,12 +245,12 @@ def test_error_handling(self): def mock_resolver(kid): return {"mock": "key"} + verifier = SDCWTVerifier(mock_resolver) # Invalid KBT should return False is_valid, claims, tags_absent = verifier.verify_presentation( - kbt=b"invalid_kbt", - expected_audience="https://test.example" + kbt=b"invalid_kbt", expected_audience="https://test.example" ) assert not is_valid, "Invalid KBT should not verify" @@ -273,23 +277,16 @@ def test_workflow_with_complex_claims(self): # Complex claims with nested structures base_claims = { "document_type": "steel_certificate", - "issuer_info": { - "name": "Steel Manufacturing Corp", - "license": "SMC-2024-001" - } + "issuer_info": {"name": "Steel Manufacturing Corp", "license": "SMC-2024-001"}, } optional_claims = { "detailed_composition": { - "elements": { - "carbon": 0.25, - "manganese": 1.20, - "silicon": 0.30 - }, - "additives": ["chromium", "nickel"] + "elements": {"carbon": 0.25, "manganese": 1.20, "silicon": 0.30}, + "additives": ["chromium", "nickel"], }, "test_results": [420, 350, 18.5], - "confidential_notes": "Internal batch notes here" + "confidential_notes": "Internal batch notes here", } # Test issuance @@ -297,7 +294,7 @@ def test_workflow_with_complex_claims(self): sd_cwt, edn, disclosures = issuer.issue_credential( base_claims=base_claims, optional_claims=optional_claims, - holder_public_key=holder_public_key_cbor + holder_public_key=holder_public_key_cbor, ) # Test presentation @@ -306,7 +303,7 @@ def test_workflow_with_complex_claims(self): sd_cwt=sd_cwt, disclosures=disclosures, selected_disclosures=disclosures[:1], # Select one disclosure - audience="https://verifier.example" + audience="https://verifier.example", ) # Test verification @@ -315,8 +312,7 @@ def test_workflow_with_complex_claims(self): verifier = SDCWTVerifier(issuer_resolver) is_valid, claims, tags_absent = verifier.verify_presentation( - kbt=kbt, - expected_audience="https://verifier.example" + kbt=kbt, expected_audience="https://verifier.example" ) assert is_valid, "Complex claims presentation should verify" diff --git a/tests/test_specification_private_keys.py b/tests/test_specification_private_keys.py index 9f8286c..fcf48a4 100644 --- a/tests/test_specification_private_keys.py +++ b/tests/test_specification_private_keys.py @@ -63,9 +63,15 @@ def test_import_holder_private_key(self, holder_private_key_edn): assert len(key[-4]) == 32, "Private key should be 32 bytes" # Verify exact values from specification - expected_x = bytes.fromhex('8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d') - expected_y = bytes.fromhex('4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343') - expected_d = bytes.fromhex('5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5') + expected_x = bytes.fromhex( + "8554eb275dcd6fbd1c7ac641aa2c90d92022fd0d3024b5af18c7cc61ad527a2d" + ) + expected_y = bytes.fromhex( + "4dc7ae2c677e96d0cc82597655ce92d5503f54293d87875d1e79ce4770194343" + ) + expected_d = bytes.fromhex( + "5759a86e59bb3b002dde467da4b52f3d06e6c2cd439456cf0485b9b864294ce5" + ) assert key[-2] == expected_x, "X coordinate should match specification" assert key[-3] == expected_y, "Y coordinate should match specification" @@ -94,9 +100,15 @@ def test_import_issuer_private_key(self, issuer_private_key_edn): assert len(key[-4]) == 48, "Private key should be 48 bytes" # Verify exact values from specification - expected_x = bytes.fromhex('c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf') - expected_y = bytes.fromhex('8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554') - expected_d = bytes.fromhex('71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c') + expected_x = bytes.fromhex( + "c31798b0c7885fa3528fbf877e5b4c3a6dc67a5a5dc6b307b728c3725926f2abe5fb4964cd91e3948a5493f6ebb6cbbf" + ) + expected_y = bytes.fromhex( + "8f6c7ec761691cad374c4daa9387453f18058ece58eb0a8e84a055a31fb7f9214b27509522c159e764f8711e11609554" + ) + expected_d = bytes.fromhex( + "71c54d2221937ea612db1221f0d3ddf771c9381c4e3be41d5aa0a89d685f09cfef74c4bbf104783fd57e87ab227d074c" + ) assert key[-2] == expected_x, "X coordinate should match specification" assert key[-3] == expected_y, "Y coordinate should match specification" @@ -120,7 +132,7 @@ def test_convert_holder_key_to_cbor(self, holder_private_key_edn): # Test hex representation for interoperability hex_cbor = cbor_data.hex() assert isinstance(hex_cbor, str) - assert all(c in '0123456789abcdef' for c in hex_cbor.lower()) + assert all(c in "0123456789abcdef" for c in hex_cbor.lower()) def test_convert_issuer_key_to_cbor(self, issuer_private_key_edn): """Test converting issuer key to CBOR format.""" @@ -230,7 +242,9 @@ def test_specification_thumbprints(self, holder_private_key_edn, issuer_private_ holder_public = {k: v for k, v in holder_key.items() if k != -4} holder_thumbprint = CoseKeyThumbprint.compute(holder_public, "sha256") - expected_holder = bytes.fromhex('8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0') + expected_holder = bytes.fromhex( + "8343d73cdfcb81f2c7cd11a5f317be8eb34e4807ec8c9ceb282495cffdf037e0" + ) assert holder_thumbprint == expected_holder, "Holder thumbprint should match specification" # Test issuer key thumbprint @@ -239,7 +253,9 @@ def test_specification_thumbprints(self, holder_private_key_edn, issuer_private_ issuer_public = {k: v for k, v in issuer_key.items() if k != -4} issuer_thumbprint = CoseKeyThumbprint.compute(issuer_public, "sha256") - expected_issuer = bytes.fromhex('554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0') + expected_issuer = bytes.fromhex( + "554550a611c9807b3462cfec4a690a1119bc43b571da1219782133f5fd6dbcb0" + ) assert issuer_thumbprint == expected_issuer, "Issuer thumbprint should match specification" def test_cbor_pretty_print_format(self, holder_private_key_edn): @@ -256,13 +272,15 @@ def test_cbor_pretty_print_format(self, holder_private_key_edn): # Should have exactly 5 fields: kty, alg, crv, x, y assert len(decoded) == 5 - assert decoded[1] == 2 # kty: EC2 - assert decoded[3] == -7 # alg: ES256 (corrected from spec's -9) - assert decoded[-1] == 1 # crv: P-256 + assert decoded[1] == 2 # kty: EC2 + assert decoded[3] == -7 # alg: ES256 (corrected from spec's -9) + assert decoded[-1] == 1 # crv: P-256 assert len(decoded[-2]) == 32 # x coordinate assert len(decoded[-3]) == 32 # y coordinate - def test_key_compatibility_with_cryptography(self, holder_private_key_edn, issuer_private_key_edn): + def test_key_compatibility_with_cryptography( + self, holder_private_key_edn, issuer_private_key_edn + ): """Test that keys are compatible with cryptographic operations.""" # Import both keys holder_cbor = edn_utils.diag_to_cbor(holder_private_key_edn) @@ -272,18 +290,18 @@ def test_key_compatibility_with_cryptography(self, holder_private_key_edn, issue issuer_key = cbor_utils.decode(issuer_cbor) # Test holder key (P-256) - holder_private_int = int.from_bytes(holder_key[-4], 'big') + holder_private_int = int.from_bytes(holder_key[-4], "big") assert 0 < holder_private_int < 2**256, "Holder private key should be in valid range" # Test issuer key (P-384) - issuer_private_int = int.from_bytes(issuer_key[-4], 'big') + issuer_private_int = int.from_bytes(issuer_key[-4], "big") assert 0 < issuer_private_int < 2**384, "Issuer private key should be in valid range" # Verify coordinates are not zero - assert holder_key[-2] != b'\x00' * 32, "Holder X coordinate should not be zero" - assert holder_key[-3] != b'\x00' * 32, "Holder Y coordinate should not be zero" - assert issuer_key[-2] != b'\x00' * 48, "Issuer X coordinate should not be zero" - assert issuer_key[-3] != b'\x00' * 48, "Issuer Y coordinate should not be zero" + assert holder_key[-2] != b"\x00" * 32, "Holder X coordinate should not be zero" + assert holder_key[-3] != b"\x00" * 32, "Holder Y coordinate should not be zero" + assert issuer_key[-2] != b"\x00" * 48, "Issuer X coordinate should not be zero" + assert issuer_key[-3] != b"\x00" * 48, "Issuer Y coordinate should not be zero" def test_edn_format_validation(self, holder_private_key_edn, issuer_private_key_edn): """Test that EDN format is valid and parseable.""" @@ -296,7 +314,9 @@ def test_edn_format_validation(self, holder_private_key_edn, issuer_private_key_ holder_roundtrip_cbor = edn_utils.diag_to_cbor(holder_roundtrip_edn) holder_roundtrip_decoded = cbor_utils.decode(holder_roundtrip_cbor) - assert holder_decoded == holder_roundtrip_decoded, "Holder EDN roundtrip should preserve key" + assert ( + holder_decoded == holder_roundtrip_decoded + ), "Holder EDN roundtrip should preserve key" # Test issuer key EDN issuer_cbor = edn_utils.diag_to_cbor(issuer_private_key_edn) @@ -307,7 +327,9 @@ def test_edn_format_validation(self, holder_private_key_edn, issuer_private_key_ issuer_roundtrip_cbor = edn_utils.diag_to_cbor(issuer_roundtrip_edn) issuer_roundtrip_decoded = cbor_utils.decode(issuer_roundtrip_cbor) - assert issuer_decoded == issuer_roundtrip_decoded, "Issuer EDN roundtrip should preserve key" + assert ( + issuer_decoded == issuer_roundtrip_decoded + ), "Issuer EDN roundtrip should preserve key" def test_specification_algorithm_correction(self, holder_private_key_edn): """Test that we correctly handle the specification's algorithm error.""" From 82d2b55dffbd1596f815479df47f8691b2516a69 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 1 Nov 2025 14:36:50 -0400 Subject: [PATCH 8/8] mypy --- src/sd_cwt/cbor_utils.py | 2 +- src/sd_cwt/cose_keys.py | 4 ++-- src/sd_cwt/holder_binding.py | 2 +- src/sd_cwt/redaction.py | 4 ++-- src/sd_cwt/sd_cwt.py | 6 +++--- src/sd_cwt/simple_api.py | 12 ++++++++---- src/sd_cwt/verifiers.py | 4 ++-- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/sd_cwt/cbor_utils.py b/src/sd_cwt/cbor_utils.py index 372e57a..d9cfc28 100644 --- a/src/sd_cwt/cbor_utils.py +++ b/src/sd_cwt/cbor_utils.py @@ -97,7 +97,7 @@ def is_simple_value(obj: Any, value: Union[int, None] = None) -> bool: Returns: True if the object is a CBOR simple value (and matches value if specified) """ - if not isinstance(obj, CBORSimpleValue): + if not isinstance(obj, CBORSimpleValue): # type: ignore[misc] return False if value is not None: return obj.value == value diff --git a/src/sd_cwt/cose_keys.py b/src/sd_cwt/cose_keys.py index 15440e0..282268e 100644 --- a/src/sd_cwt/cose_keys.py +++ b/src/sd_cwt/cose_keys.py @@ -2,7 +2,7 @@ """COSE Key generation and management.""" -from typing import Any, Optional +from typing import Any, Optional, cast from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec @@ -73,7 +73,7 @@ def cose_key_to_dict(cose_key: bytes) -> dict[int, Any]: Returns: COSE key as a dictionary """ - return cbor_utils.decode(cose_key) + return cast(dict[int, Any], cbor_utils.decode(cose_key)) def cose_key_get_public(cose_key: bytes) -> bytes: diff --git a/src/sd_cwt/holder_binding.py b/src/sd_cwt/holder_binding.py index dfcd87d..d32c071 100644 --- a/src/sd_cwt/holder_binding.py +++ b/src/sd_cwt/holder_binding.py @@ -97,7 +97,7 @@ def create_sd_kbt( protected_header[4] = key_id # kid # Unprotected header (empty for SD-KBT) - unprotected_header: dict[str, Any] = {} + unprotected_header: dict[int, Any] = {} # Encode payload payload_bytes = cbor_utils.encode(kbt_payload) diff --git a/src/sd_cwt/redaction.py b/src/sd_cwt/redaction.py index a12f08a..2ad61be 100644 --- a/src/sd_cwt/redaction.py +++ b/src/sd_cwt/redaction.py @@ -2,7 +2,7 @@ import hashlib import secrets -from typing import Any, Optional, Protocol +from typing import Any, Optional, Protocol, cast from . import cbor_utils, edn_utils @@ -94,7 +94,7 @@ def cbor_to_dict(cbor_bytes: bytes) -> dict[Any, Any]: Returns: Decoded dictionary """ - return cbor_utils.decode(cbor_bytes) + return cast(dict[Any, Any], cbor_utils.decode(cbor_bytes)) def generate_salt(length: int = 16, salt_generator: Optional[SaltGenerator] = None) -> bytes: diff --git a/src/sd_cwt/sd_cwt.py b/src/sd_cwt/sd_cwt.py index c65dc41..9011b18 100644 --- a/src/sd_cwt/sd_cwt.py +++ b/src/sd_cwt/sd_cwt.py @@ -69,7 +69,7 @@ def create_sd_cwt_with_holder_binding( ) # Create protected header for issuer signature - protected_header = { + protected_header: dict[int, Any] = { 1: issuer_signer.algorithm, # Algorithm } @@ -142,7 +142,7 @@ def validate_sd_cwt_presentation(sd_kbt: bytes) -> dict[str, Any]: """ from .holder_binding import validate_sd_kbt_structure - result = { + result: dict[str, Any] = { "valid": False, "sd_cwt": None, "disclosures": [], @@ -219,7 +219,7 @@ def extract_verified_claims(sd_kbt: bytes) -> dict[str, Any]: - claims: Complete claims map with disclosed values (if valid) - errors: List of errors encountered """ - result = {"valid": False, "claims": {}, "errors": []} + result: dict[str, Any] = {"valid": False, "claims": {}, "errors": []} # First validate the presentation validation_result = validate_sd_cwt_presentation(sd_kbt) diff --git a/src/sd_cwt/simple_api.py b/src/sd_cwt/simple_api.py index df9d837..5b043cb 100644 --- a/src/sd_cwt/simple_api.py +++ b/src/sd_cwt/simple_api.py @@ -2,7 +2,7 @@ import re import time -from typing import Any, Optional +from typing import Any, Callable, Optional from . import cbor_utils from .cose_sign1 import cose_sign1_sign @@ -44,7 +44,7 @@ def create_edn_with_annotations( optional_claims: dict[str, Any], issuer: str = "https://issuer.example", subject: str = "https://subject.example", - holder_public_key: bytes = None, + holder_public_key: Optional[bytes] = None, use_holder_thumbprint: bool = False, issued_at: Optional[int] = None, ) -> str: @@ -378,6 +378,7 @@ def _create_sd_cwt_with_selected_disclosures( ] # Re-wrap with tag if original was tagged + new_cose_sign1: Any if cbor_utils.is_tag(cose_sign1): new_cose_sign1 = cbor_utils.create_tag( cbor_utils.get_tag_number(cose_sign1), new_cose_sign1_value @@ -392,7 +393,7 @@ def _create_sd_cwt_with_selected_disclosures( class SDCWTVerifier: """Simple API for verifying SD-CWT presentations.""" - def __init__(self, public_key_resolver): + def __init__(self, public_key_resolver: Callable[[bytes], dict[int, Any]]) -> None: """Initialize with a public key resolver. Args: @@ -403,7 +404,10 @@ def __init__(self, public_key_resolver): self.credential_verifier = CredentialVerifier(public_key_resolver) def verify_presentation( - self, kbt: bytes, expected_audience: str, holder_key_resolver=None + self, + kbt: bytes, + expected_audience: str, + holder_key_resolver: Optional[Callable[[bytes], dict[int, Any]]] = None, ) -> tuple[bool, Optional[dict[str, Any]], bool]: """Verify an SD-CWT presentation and extract claims. diff --git a/src/sd_cwt/verifiers.py b/src/sd_cwt/verifiers.py index 44999ef..8a2c6cf 100644 --- a/src/sd_cwt/verifiers.py +++ b/src/sd_cwt/verifiers.py @@ -5,7 +5,7 @@ - PresentationVerifier: Verifies KBT presentations using holder's key """ -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, cast from . import cbor_utils from .cose_sign1 import ES256Verifier, cose_sign1_verify @@ -222,7 +222,7 @@ def ckt_based_resolver(kid: bytes) -> dict[int, Any]: def single_key_resolver(kid: bytes) -> dict[int, Any]: if kid == holder_thumbprint: - return holder_key + return cast(dict[int, Any], holder_key) raise ValueError(f"Key not found: {kid.hex()}") return PresentationVerifier(single_key_resolver)