"""Tests for building minimal Aliro Access Documents. v1 scope: MSO contains only the fields needed for a Reader to verify the Access Credential public key. No Access Data Elements, no schedules, no access rules. Those arrive later. Reference: Aliro spec \u00a77.2 and ISO 18013-5 \u00a79.1.2. """ import cbor2 import pytest from cryptography.hazmat.primitives.asymmetric import ec from aliro_harness.issuer.access_document import ( ALIRO_DOCTYPE_ACCESS, MSO_KEY_DEVICE_KEY_INFO, MSO_KEY_DIGEST_ALG, MSO_KEY_DOC_TYPE, MSO_KEY_TIME_VERIFICATION_REQUIRED, MSO_KEY_VALIDITY_INFO, MSO_KEY_VALUE_DIGESTS, MSO_KEY_VERSION, build_access_document, ) from aliro_harness.issuer.cose import verify_cose_sign1 from aliro_harness.issuer.kid import compute_issuer_kid @pytest.fixture def issuer_key(): return ec.generate_private_key(ec.SECP256R1()) @pytest.fixture def access_credential_pub(): return ec.generate_private_key(ec.SECP256R1()).public_key() def test_access_document_verifies_with_issuer_public_key(issuer_key, access_credential_pub): doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) payload = verify_cose_sign1(doc, issuer_key.public_key()) assert isinstance(payload, bytes) assert len(payload) > 0 def test_access_document_payload_is_canonical_cbor_map(issuer_key, access_credential_pub): doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) payload = verify_cose_sign1(doc, issuer_key.public_key()) mso = cbor2.loads(payload) assert isinstance(mso, dict) def test_access_document_mso_has_required_aliro_keys(issuer_key, access_credential_pub): doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) mso = cbor2.loads(verify_cose_sign1(doc, issuer_key.public_key())) required = { MSO_KEY_VERSION, MSO_KEY_DIGEST_ALG, MSO_KEY_VALUE_DIGESTS, MSO_KEY_DEVICE_KEY_INFO, MSO_KEY_DOC_TYPE, MSO_KEY_VALIDITY_INFO, MSO_KEY_TIME_VERIFICATION_REQUIRED, } assert required.issubset(mso.keys()), f"missing: {required - mso.keys()}" def test_access_document_keys_are_text_strings_not_ints(issuer_key, access_credential_pub): """Aliro \u00a77.2.2: keys replaced by integers 'still encoded as text strings'.""" doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) mso = cbor2.loads(verify_cose_sign1(doc, issuer_key.public_key())) for key in mso: assert isinstance(key, str), f"MSO key {key!r} must be tstr, got {type(key).__name__}" def test_access_document_docType_is_aliro_a(issuer_key, access_credential_pub): doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) mso = cbor2.loads(verify_cose_sign1(doc, issuer_key.public_key())) assert mso[MSO_KEY_DOC_TYPE] == ALIRO_DOCTYPE_ACCESS == "aliro-a" def test_access_document_digest_alg_is_sha256(issuer_key, access_credential_pub): doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) mso = cbor2.loads(verify_cose_sign1(doc, issuer_key.public_key())) assert mso[MSO_KEY_DIGEST_ALG] == "SHA-256" def test_access_document_time_verification_required_is_false_for_v1( issuer_key, access_credential_pub ): """v1 test CA emits permissive documents; Reader may ignore time checks.""" doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) mso = cbor2.loads(verify_cose_sign1(doc, issuer_key.public_key())) assert mso[MSO_KEY_TIME_VERIFICATION_REQUIRED] is False def test_access_document_device_key_is_access_credential_pubkey( issuer_key, access_credential_pub ): doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) mso = cbor2.loads(verify_cose_sign1(doc, issuer_key.public_key())) device_key_info = mso[MSO_KEY_DEVICE_KEY_INFO] cose_key = device_key_info["1"] assert cose_key[1] == 2 # kty = EC2 assert cose_key[-1] == 1 # crv = P-256 expected = access_credential_pub.public_numbers() assert cose_key[-2] == expected.x.to_bytes(32, "big") assert cose_key[-3] == expected.y.to_bytes(32, "big") def test_access_document_fits_within_applet_storage_budget(issuer_key, access_credential_pub): """The applet's CredentialStore reserves ACCESS_DOC_MAX_LEN = 1024 bytes for the Access Document. If this test ever fails, either the AD has grown (add fields? new namespaces?) or the card-side budget needs a bump — sync with CredentialStore.ACCESS_DOC_MAX_LEN on the applet side. """ doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) assert len(doc) <= 1024, f"Access Document is {len(doc)} bytes, exceeds 1024-byte applet budget" def test_access_document_kid_in_unprotected_header_matches_issuer( issuer_key, access_credential_pub ): doc = build_access_document( issuer_private_key=issuer_key, access_credential_public_key=access_credential_pub, ) decoded = cbor2.loads(doc) _protected, unprotected, _payload, _sig = decoded assert unprotected[4] == compute_issuer_kid(issuer_key.public_key())