diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e26c95..2458697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,44 @@ # Changelog -## Unreleased - -### Added - -- **[SPEC][SDK]** Added the `com.agentrust-io.manifest` Agent Plugins 1.0.0 - extension profile. It resolves an HTTPS manifest by raw-byte digest, verifies - it against independently trusted keys, and compares the local bundle with a - signed `source_bundle` binding. Absent, unreachable, unverifiable, and - mismatched references remain distinct outcomes. - -- **[SPEC][SDK]** Added the signed `composition-only` profile for repository and - pre-execution manifests. Every omitted artifact must be named in - `unbound_artifacts`; overlap and undeclared omissions fail closed. These - documents verify as `INCOMPLETE` and cannot claim Level 0 or above. - -- **[SDK]** `parse_tpm_attest()` now exposes the common signed `TPMS_ATTEST` - header and opaque union payload, while `parse_tpm_nv_certify()` enforces the - signed type and parses the `TPMS_NV_CERTIFY_INFO` carried by - `TPM2_NV_Certify`. This lets cMCP retire its remaining local NV-certify wire - parser. Size-prefixed attestations now reject undeclared trailing bytes. - -### Security +## Unreleased + +### Added + + +- **[SPEC][SDK]** Added the `com.agentrust-io.manifest` Agent Plugins 1.0.0 + extension profile. It resolves an HTTPS manifest by raw-byte digest, verifies + it against independently trusted keys, and compares the local bundle with a + signed `source_bundle` binding. Absent, unreachable, unverifiable, and + mismatched references remain distinct outcomes. + +- **[SPEC][SDK]** Added the signed `composition-only` profile for repository and + pre-execution manifests. Every omitted artifact must be named in + `unbound_artifacts`; overlap and undeclared omissions fail closed. These + documents verify as `INCOMPLETE` and cannot claim Level 0 or above. + +- **[SDK]** `parse_tpm_attest()` now exposes the common signed `TPMS_ATTEST` + header and opaque union payload, while `parse_tpm_nv_certify()` enforces the + signed type and parses the `TPMS_NV_CERTIFY_INFO` carried by + `TPM2_NV_Certify`. This lets cMCP retire its remaining local NV-certify wire + parser. Size-prefixed attestations now reject undeclared trailing bytes. + +**[SDK] `AM-VEC-COSE-002` … `AM-VEC-COSE-015` publish the COSE negative conformance vectors.** Fourteen cases a conforming verifier must not accept, covering the protected/unprotected header split, CBOR tagging and framing, payload presence, the issuer authorization boundary, the two JSON parser divergences RFC 8259 leaves open (duplicate member names, non-finite numbers), version routing in both directions, and the payload depth bound. `011` and `015` are a pair: #243 records `NaN` and `Infinity` as one class of defect, but they are not one code path in every parser, so a verifier that special-cases `NaN` passes the first and fails the second. They complete the portable contract other-language SDKs are written against: until now the suite proved an implementation could accept a valid envelope, not that it rejected an invalid one. + +`AM-VEC-COSE-014` is the reverse of `012` and the reason the version gate is described as bidirectional: a v0.2 manifest must not fall back to the v0.1 detached signature block, because a one-way gate is not a gate. It expects `MISMATCH` rather than `INCOMPATIBLE_VERSION`, since the verifier does support 0.2 and reporting otherwise would state something untrue about its capabilities. + +The hybrid authorization case, a `COSE_Sign` carrying one authorized component key alongside one unauthorized one, is deliberately not a vector: it contains an ML-DSA-65 signature, ML-DSA-65 signing is hedged, and `cryptography` 49 exposes no deterministic mode, so the bytes differ on every regeneration. It remains covered by a per-run test in the Python suite, and the vector README states the rule as binding on other languages so it is not mistaken for out of scope. + +Each negative carries `signature_valid`, recording whether the Ed25519 signature over the RFC 9052 `Sig_structure` verifies. Where it is true, a verifier cannot pass the vector by rejecting a broken signature and never reaching the rule the vector names. The vectors whose defect is in the payload are signed over the malformed bytes rather than having bytes swapped into an already-signed envelope, which is what keeps that guarantee. Two declare `false` by design: `AM-VEC-COSE-002` tampers with the protected header, which is the rule under test, and `AM-VEC-COSE-008` has a nil payload, so there is no `Sig_structure` to verify over. + +`AM-VEC-COSE-009` is byte-identical to `AM-VEC-COSE-001` and differs only in `context.trusted_key_issuers`. Nothing about the object explains the rejection, so a verifier that stops at "the signature verifies under a trusted key" returns `VALID` and has no authorization boundary at all. + +### Fixed + +- The committed conformance vectors are now diffed against a fresh in-memory regeneration by the test suite, so a vector edited by hand, or a generator change made without regenerating, fails CI rather than shipping as a contract nobody can reproduce. + +- The vectors had been stale each time `SIGNED_FIELDS` gained a member without being regenerated alongside it: `intent` in 0.11.0, `profile` and `unbound_artifacts` in #306, `source_bundle` in #307. In each case they they published a `signature.signed_fields` list omitting those fields, and `AM-VEC-018` carried the `manifest_hash_in_report` that followed from the shorter list. No signature or expected result changes, because none of these manifests declares any of those fields and `signing_pre_image` omits absent ones, so the signed bytes are identical either way. What was wrong is what the suite told other languages to build their pre-image from. The regeneration test above is what surfaced it, and is what stops it recurring. + +### Security - `verify_manifest()` now fails closed when a core identity, validity, or artifact-container claim is missing. A valid signature no longer turns such a structurally incomplete object into a `VALID` manifest; legacy v0.1 issuer omission remains compatible unless issuer authorization is configured. @@ -81,7 +98,7 @@ The digest covers every file in the bundle, including ones the adapter does not **[SPEC] Section 6.4 is an informative crosswalk to OCSF runtime evidence** (#269). There was no defined join key between a manifest and the OCSF events emitted under it, so a consumer holding both could not tell they described the same agent without an out-of-band convention, and implementers were left to invent a second identity mechanism for a job this specification already does. The new section records the intended correspondence and deliberately requires nothing. -Three things it gets right that a normative version could not yet: it is written against the **`ai_operation` profile**, which is what actually contributes `ai_agent`, rather than against an event class (the `Agent Inventory Info [5050]` class proposed for this does not exist in OCSF as of this writing; a Discovery-category class for agent trust-base inventory is now proposed upstream at `ocsf/ocsf-schema#1724`); it maps `ai_agent.uid` to the durable identity and `instance_uid` to the session-scoped one, which is what OCSF's own definitions ask for, instead of collapsing both onto `agent_id`; and it notes that `session_uid` is not an `ai_agent` attribute at all. +Three things it gets right that a normative version could not yet: it is written against the **`ai_operation` profile**, which is what actually contributes `ai_agent`, rather than against an event class (the `Agent Inventory Info [5050]` class proposed for this does not exist in OCSF as of this writing; a Discovery-category class for agent trust-base inventory is now proposed upstream at `ocsf/ocsf-schema#1724`); it maps `ai_agent.uid` to the durable identity and `instance_uid` to the session-scoped one, which is what OCSF's own definitions ask for, instead of collapsing both onto `agent_id`; and it notes that `session_uid` is not an `ai_agent` attribute at all. It stays informative because `agent_id` is one field serving both roles, and section 3.1 says the `/agent//` path is "a convention, not a requirement" — so a conformant `agent_id` may be stable and carry no instance scope. Requiring it to populate `instance_uid`, which OCSF defines as explicitly distinct from the stable `uid`, would force a stable identifier into the non-stable field and cost a consumer the ability to separate "every run of this agent" from "this run". Resolving that means deciding whether `agent_id` splits, which belongs in CoSAI WS4 alongside the canonical `@context` URL that section 3.1 already defers there. diff --git a/python/tests/test_vectors.py b/python/tests/test_vectors.py index 45b5f06..d4e74cd 100644 --- a/python/tests/test_vectors.py +++ b/python/tests/test_vectors.py @@ -38,6 +38,38 @@ def _load_vector(file_name: str) -> dict[str, Any]: VECTOR_FILES = [entry["file"] for entry in _load_index()] +def test_committed_vectors_match_a_fresh_regeneration() -> None: + """The committed JSON must be what ``generate.py`` produces today. + + The vectors are committed so a consumer in another language never has to + run Python, which means the files and the generator can drift apart + silently. Rebuilding them in memory and diffing closes that: a vector + edited by hand, or a generator change made without regenerating, fails + here rather than shipping as a contract nobody can reproduce. + + Reproducibility rests on the fixed seed and Ed25519 determinism (RFC 8032), + so this is stable rather than merely usually true. It is also why there is + no post-quantum vector: ML-DSA-65 signing is hedged, and a vector whose + bytes changed on every run could not be asserted this way. + """ + from tests.vectors.generate import build + + rebuilt = {v["id"]: v for v in build()} + committed = { + p.stem: json.loads(p.read_text()) for p in VECTORS_DIR.glob("AM-VEC-*.json") + } + + assert rebuilt.keys() == committed.keys(), ( + "the set of committed vectors differs from what generate.py builds; " + "run `python -m tests.vectors.generate`" + ) + for vid, expected in rebuilt.items(): + assert committed[vid] == expected, ( + f"{vid} on disk differs from a fresh regeneration; run " + f"`python -m tests.vectors.generate` and review the diff" + ) + + def test_index_lists_every_vector_file() -> None: on_disk = {p.name for p in VECTORS_DIR.glob("AM-VEC-*.json")} in_index = set(VECTOR_FILES) @@ -83,8 +115,36 @@ def test_vector(file_name: str) -> None: assert got == want, f"{vector['id']}: fields_verified.{field} expected {want}, got {got}" +# Only vectors that pin an encoding. A negative vector carries `envelope_hex` +# but no `expected.cose`, because its bytes are malformed by construction and +# pinning their decomposition would assert that a verifier can parse something +# it is being told to reject. COSE_VECTOR_FILES = [ - f for f in VECTOR_FILES if "envelope_hex" in _load_vector(f) + f for f in VECTOR_FILES if "cose" in _load_vector(f).get("expected", {}) +] + +COSE_NEGATIVE_FILES = [ + f + for f in VECTOR_FILES + if "envelope_hex" in _load_vector(f) + and "cose" not in _load_vector(f).get("expected", {}) +] + +# Every COSE-series negative, selected by id rather than by the presence of the +# key under test, so a vector that forgets to declare signature_valid fails +# instead of quietly dropping out of the check. +# +# The membership rule is "in the COSE series and not the positive encoding +# vector". That pulls in AM-VEC-COSE-014, whose subject is a manifest document +# rather than an envelope because the rule it tests is that such a document +# must not be accepted outside a COSE envelope. It still has to declare and +# justify signature_valid, so it belongs here even though it is not in +# COSE_NEGATIVE_FILES. +COSE_SIGNED_NEGATIVE_FILES = [ + f + for f in VECTOR_FILES + if f.startswith("AM-VEC-COSE-") + and "cose" not in _load_vector(f).get("expected", {}) ] @@ -129,3 +189,209 @@ def test_cose_vector_encoding_is_pinned(file_name: str) -> None: keypair = ed25519_from_private_bytes(bytes(range(32))) regenerated = sign_cose_sign1(json.loads(payload.decode()), keypair) assert regenerated == envelope + + +@pytest.mark.parametrize( + "file_name", + COSE_NEGATIVE_FILES, + ids=[f.removesuffix(".json") for f in COSE_NEGATIVE_FILES], +) +def test_cose_negative_vector_is_not_silently_unparseable(file_name: str) -> None: + """A negative vector must be rejected for its own reason, not by accident. + + The vector schema records that a manifest is rejected, not why, so a + verifier could pass one of these by failing to decode the CBOR at all. + These vectors are built by mutating a valid envelope, so the envelope must + still decode as CBOR even where the mutation makes it inadmissible. This + asserts that, so a vector cannot degrade into "rejected because it was + garbage" without the suite noticing. + """ + import cbor2 + + vector = _load_vector(file_name) + envelope = bytes.fromhex(vector["envelope_hex"]) + + # Trailing-byte vectors are deliberately undecodable past the first + # object, which is the property under test, so they are exempt. + if "Trailing bytes" in vector["description"]: + return + + decoded = cbor2.loads(envelope) + body = decoded.value if isinstance(decoded, cbor2.CBORTag) else decoded + assert isinstance(body, (list, tuple)), ( + f"{vector['id']}: the mutated envelope should still be a CBOR array, " + f"otherwise the vector tests decoder robustness rather than the rule " + f"it names" + ) + assert len(body) == 4, f"{vector['id']}: should still be four elements" + + +@pytest.mark.parametrize( + "file_name", + COSE_SIGNED_NEGATIVE_FILES, + ids=[f.removesuffix(".json") for f in COSE_SIGNED_NEGATIVE_FILES], +) +def test_cose_negative_vector_declares_whether_its_signature_is_valid( + file_name: str, +) -> None: + """``signature_valid`` must be present and must be true. + + It is the property that separates a vector testing the rule it names from + one a verifier passes by rejecting a broken signature and never reaching + that rule. Asserting it here means a vector cannot be added, or an existing + one mutated, in a way that quietly turns it into an incidental signature + failure. + + The two exceptions are declared rather than tolerated: AM-VEC-COSE-002 + invalidates the signature on purpose, since a tampered protected header is + the rule under test, and AM-VEC-COSE-008 has a nil payload, so there is no + Sig_structure to verify over in the first place. + """ + vector = _load_vector(file_name) + assert "signature_valid" in vector, ( + f"{vector['id']}: every negative COSE vector must declare whether its " + f"signature verifies" + ) + + if vector["id"] in {"AM-VEC-COSE-002", "AM-VEC-COSE-008"}: + assert vector["signature_valid"] is False + return + assert vector["signature_valid"] is True, ( + f"{vector['id']}: a verifier could pass this by rejecting the " + f"signature and never applying the rule the vector names" + ) + + +@pytest.mark.parametrize( + "file_name", + COSE_SIGNED_NEGATIVE_FILES, + ids=[f.removesuffix(".json") for f in COSE_SIGNED_NEGATIVE_FILES], +) +def test_cose_negative_vector_signature_claim_is_true(file_name: str) -> None: + """Re-derive ``signature_valid`` the way a foreign implementation would. + + Using only the public key published in ``keys.json``, so the claim is + checked against the bytes on disk rather than trusted from the generator + that wrote them. Which pre-image applies follows the envelope the vector + carries: the RFC 9052 Sig_structure for an ``envelope_hex`` vector, and the + RFC 8785 signing pre-image for AM-VEC-COSE-014, whose subject is a manifest + document with a v0.1 detached signature block. + """ + import base64 + + import cbor2 + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + + from agent_manifest._cose import _sig_structure_sign1 + from agent_manifest._signing import signing_pre_image + + vector = _load_vector(file_name) + keys = json.loads((VECTORS_DIR / "keys.json").read_text()) + raw = base64.urlsafe_b64decode(keys["public_key_b64url"] + "=" * 4) + public_key = Ed25519PublicKey.from_public_bytes(raw) + + if "envelope_hex" in vector: + decoded = cbor2.loads(bytes.fromhex(vector["envelope_hex"])) + body = decoded.value if isinstance(decoded, cbor2.CBORTag) else decoded + protected, _unprotected, payload, signature = body + if payload is None: + assert vector["signature_valid"] is False, vector["id"] + return + pre_image = _sig_structure_sign1(protected, payload) + else: + manifest = vector["manifest"] + block = manifest["signature"] + assert block["key_id"] == keys["key_id"], ( + f"{vector['id']}: signed by a key other than the published one" + ) + signature = base64.urlsafe_b64decode(block["signature_value"] + "=" * 4) + pre_image = signing_pre_image(manifest) + + try: + public_key.verify(signature, pre_image) + verifies = True + except InvalidSignature: + verifies = False + + assert verifies is vector["signature_valid"], ( + f"{vector['id']}: signature_valid says {vector['signature_valid']} but " + f"the signature on disk {'verifies' if verifies else 'does not verify'}" + ) + + +def _repaired_envelopes() -> dict[str, tuple[Any, dict[str, Any]]]: + """Each phase 3 vector with its named defect removed and nothing else. + + Built from the same helpers that build the vectors, so a repair cannot + silently diverge from the thing it is repairing. The subject is bytes for + the envelope vectors and a manifest document for AM-VEC-COSE-014, matching + what each vector carries. + """ + from agent_manifest._canonicalize import canonicalize + from agent_manifest._cose import sign_cose_sign1 + + from tests.vectors.generate import ( + KP, + _sign_payload, + base_context, + base_manifest, + cose_manifest, + ) + + context = base_context() + valid_payload = canonicalize(cose_manifest()) + # The carrier 010, 011 and 013 hang their defect on. Asserted below to be + # benign on its own, so those three cannot be passing on the carrier. + carrier = canonicalize(cose_manifest(attestation={"placeholder": 0})) + + return { + # 009's defect is entirely in the context, so the repair is to drop + # the issuer binding and leave the envelope untouched. + "AM-VEC-COSE-009": (sign_cose_sign1(cose_manifest(), KP), context), + "AM-VEC-COSE-010": (_sign_payload(valid_payload), context), + "AM-VEC-COSE-011": ( + _sign_payload( + carrier.replace(b'{"placeholder":0}', b'{"nonce_skew_seconds":0}') + ), + context, + ), + "AM-VEC-COSE-012": (_sign_payload(valid_payload), context), + "AM-VEC-COSE-013": (_sign_payload(carrier), context), + "AM-VEC-COSE-015": ( + _sign_payload( + carrier.replace(b'{"placeholder":0}', b'{"nonce_skew_seconds":0}') + ), + context, + ), + # 014's defect is the version/envelope pairing, so the repair is to put + # the document back at the version its envelope is for. This is + # AM-VEC-001. + "AM-VEC-COSE-014": (base_manifest(version="0.1"), context), + } + + +@pytest.mark.parametrize("vector_id", sorted(_repaired_envelopes())) +def test_cose_negative_vector_isolates_its_named_defect(vector_id: str) -> None: + """Remove only the defect a vector names, and it must verify VALID. + + This is what separates a vector that tests its rule from one that happens + to be rejected for some other reason it also contains. ``signature_valid`` + rules out an incidental signature failure; this rules out everything else, + by showing the named defect is the sole thing standing between the + envelope and a VALID result. + + It matters most for AM-VEC-COSE-011 and 013, which hang their defect on an + ``attestation`` object. If that carrier were not itself benign the vectors + would have two defects, and an implementation could pass them without + implementing the rule under test. + """ + envelope, context = _repaired_envelopes()[vector_id] + result = verify_manifest(envelope, VerificationContext(**context), RevocationStore()) + + assert result.result.value == "VALID", ( + f"{vector_id}: with its named defect removed the envelope still does " + f"not verify ({result.result.value}), so the vector carries a second " + f"defect and does not isolate the rule it names" + ) + assert result.signature_verified is True, vector_id diff --git a/python/tests/vectors/AM-VEC-001.json b/python/tests/vectors/AM-VEC-001.json index fbe56aa..5c2a86e 100644 --- a/python/tests/vectors/AM-VEC-001.json +++ b/python/tests/vectors/AM-VEC-001.json @@ -45,13 +45,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-002.json b/python/tests/vectors/AM-VEC-002.json index f27b911..2d05f7f 100644 --- a/python/tests/vectors/AM-VEC-002.json +++ b/python/tests/vectors/AM-VEC-002.json @@ -45,13 +45,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-003.json b/python/tests/vectors/AM-VEC-003.json index 93b4a58..f18ed91 100644 --- a/python/tests/vectors/AM-VEC-003.json +++ b/python/tests/vectors/AM-VEC-003.json @@ -45,13 +45,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-004.json b/python/tests/vectors/AM-VEC-004.json index 0e42e5c..1a31bb3 100644 --- a/python/tests/vectors/AM-VEC-004.json +++ b/python/tests/vectors/AM-VEC-004.json @@ -46,13 +46,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-006.json b/python/tests/vectors/AM-VEC-006.json index 31519e2..77b24be 100644 --- a/python/tests/vectors/AM-VEC-006.json +++ b/python/tests/vectors/AM-VEC-006.json @@ -45,13 +45,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-007.json b/python/tests/vectors/AM-VEC-007.json index e9d5d8f..a8910f0 100644 --- a/python/tests/vectors/AM-VEC-007.json +++ b/python/tests/vectors/AM-VEC-007.json @@ -45,13 +45,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-008.json b/python/tests/vectors/AM-VEC-008.json index 048aa4d..4285eb0 100644 --- a/python/tests/vectors/AM-VEC-008.json +++ b/python/tests/vectors/AM-VEC-008.json @@ -45,13 +45,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-009.json b/python/tests/vectors/AM-VEC-009.json index 7847278..ccd70c0 100644 --- a/python/tests/vectors/AM-VEC-009.json +++ b/python/tests/vectors/AM-VEC-009.json @@ -56,13 +56,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-010.json b/python/tests/vectors/AM-VEC-010.json index 4af6eba..3de109c 100644 --- a/python/tests/vectors/AM-VEC-010.json +++ b/python/tests/vectors/AM-VEC-010.json @@ -46,13 +46,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-011.json b/python/tests/vectors/AM-VEC-011.json index 6329a14..6ac5413 100644 --- a/python/tests/vectors/AM-VEC-011.json +++ b/python/tests/vectors/AM-VEC-011.json @@ -55,13 +55,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-012.json b/python/tests/vectors/AM-VEC-012.json index 80c5e00..5d8aad4 100644 --- a/python/tests/vectors/AM-VEC-012.json +++ b/python/tests/vectors/AM-VEC-012.json @@ -58,13 +58,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-013.json b/python/tests/vectors/AM-VEC-013.json index c306956..fdbde0a 100644 --- a/python/tests/vectors/AM-VEC-013.json +++ b/python/tests/vectors/AM-VEC-013.json @@ -50,13 +50,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-014.json b/python/tests/vectors/AM-VEC-014.json index 07f9785..7472a72 100644 --- a/python/tests/vectors/AM-VEC-014.json +++ b/python/tests/vectors/AM-VEC-014.json @@ -48,13 +48,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-015.json b/python/tests/vectors/AM-VEC-015.json index c902f9e..e45c774 100644 --- a/python/tests/vectors/AM-VEC-015.json +++ b/python/tests/vectors/AM-VEC-015.json @@ -51,13 +51,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-016.json b/python/tests/vectors/AM-VEC-016.json index 2a84e37..c48ef7c 100644 --- a/python/tests/vectors/AM-VEC-016.json +++ b/python/tests/vectors/AM-VEC-016.json @@ -48,13 +48,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-017.json b/python/tests/vectors/AM-VEC-017.json index b6b1584..2631993 100644 --- a/python/tests/vectors/AM-VEC-017.json +++ b/python/tests/vectors/AM-VEC-017.json @@ -45,13 +45,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-018.json b/python/tests/vectors/AM-VEC-018.json index 65e7836..a64e275 100644 --- a/python/tests/vectors/AM-VEC-018.json +++ b/python/tests/vectors/AM-VEC-018.json @@ -45,18 +45,22 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] }, "attestation": { "platform": "tpm", - "manifest_hash_in_report": "sha256:55a8dfda7985bf883ae26ef4a487ba5aa689640c0d633fd03bd909fe9f37b4e9" + "manifest_hash_in_report": "sha256:be209f45a3f44fef55001b626a46468505860f7a85245967d2365586ce2da9e0" } }, "context": { diff --git a/python/tests/vectors/AM-VEC-019.json b/python/tests/vectors/AM-VEC-019.json index 869f817..a1e4d32 100644 --- a/python/tests/vectors/AM-VEC-019.json +++ b/python/tests/vectors/AM-VEC-019.json @@ -58,13 +58,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-020.json b/python/tests/vectors/AM-VEC-020.json index c4f3d92..5b68d21 100644 --- a/python/tests/vectors/AM-VEC-020.json +++ b/python/tests/vectors/AM-VEC-020.json @@ -46,13 +46,17 @@ "expires_at", "issuer", "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", "artifacts", "delegation_chain", "hitl_record", "prior_transparency_log_entry", "log_retention", "data_scope", - "operational_lifecycle" + "operational_lifecycle", + "intent" ] } }, diff --git a/python/tests/vectors/AM-VEC-COSE-002.json b/python/tests/vectors/AM-VEC-COSE-002.json new file mode 100644 index 0000000..d9de8ff --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-002.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-002", + "description": "A tampered protected header invalidates the signature.", + "spec_refs": [ + "cose-envelope-v0.2 3", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d2845874a5013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c0768696e6a656374656410781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a05902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840819b17572c67eece4a1b20557f1d861a295c1fc3287b8a5cd1e6f93e48ec8553e22fcd94eba9ac6edfdfae0316cda3e35609637f27d2bba0bc148dc6c82b8409", + "signature_valid": false, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-003.json b/python/tests/vectors/AM-VEC-COSE-003.json new file mode 100644 index 0000000..85518b6 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-003.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-003", + "description": "alg present in the unprotected header is rejected, never read.", + "spec_refs": [ + "cose-envelope-v0.2 3", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a10138305902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840819b17572c67eece4a1b20557f1d861a295c1fc3287b8a5cd1e6f93e48ec8553e22fcd94eba9ac6edfdfae0316cda3e35609637f27d2bba0bc148dc6c82b8409", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-004.json b/python/tests/vectors/AM-VEC-COSE-004.json new file mode 100644 index 0000000..f31abf6 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-004.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-004", + "description": "A vendor-tree typ alias is rejected. The signature over this envelope is valid, so typ is the only defect.", + "spec_refs": [ + "cose-envelope-v0.2 3", + "cose-envelope-v0.2 7" + ], + "envelope_hex": "d284586ea4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c1078236170706c69636174696f6e2f766e642e6167656e742d6d616e69666573742b636f7365a05902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840432cf0b993fce90e738e8c917e99f39d7d6832fcdedceae5d9aaa51fe996ceceed2e3f7351b9a516f8a9dde6c73e3ed618c3c4252d0c1a638a5e90441763f000", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-005.json b/python/tests/vectors/AM-VEC-COSE-005.json new file mode 100644 index 0000000..d2ebc5e --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-005.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-005", + "description": "An unprotected header injected after signing does not change the verdict: kid is read from the protected header only.", + "spec_refs": [ + "cose-envelope-v0.2 4.1", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a104582000000000000000000000000000000000000000000000000000000000000000005902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840819b17572c67eece4a1b20557f1d861a295c1fc3287b8a5cd1e6f93e48ec8553e22fcd94eba9ac6edfdfae0316cda3e35609637f27d2bba0bc148dc6c82b8409", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "VALID", + "signature_verified": true + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-006.json b/python/tests/vectors/AM-VEC-COSE-006.json new file mode 100644 index 0000000..27b4091 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-006.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-006", + "description": "An untagged COSE structure is rejected rather than inferred.", + "spec_refs": [ + "cose-envelope-v0.2 2", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "84586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a05902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840819b17572c67eece4a1b20557f1d861a295c1fc3287b8a5cd1e6f93e48ec8553e22fcd94eba9ac6edfdfae0316cda3e35609637f27d2bba0bc148dc6c82b8409", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-007.json b/python/tests/vectors/AM-VEC-COSE-007.json new file mode 100644 index 0000000..c75b8ad --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-007.json @@ -0,0 +1,21 @@ +{ + "id": "AM-VEC-COSE-007", + "description": "Trailing bytes after the COSE object are rejected.", + "spec_refs": [ + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a05902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840819b17572c67eece4a1b20557f1d861a295c1fc3287b8a5cd1e6f93e48ec8553e22fcd94eba9ac6edfdfae0316cda3e35609637f27d2bba0bc148dc6c82b840900", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-008.json b/python/tests/vectors/AM-VEC-COSE-008.json new file mode 100644 index 0000000..280c1b3 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-008.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-008", + "description": "A detached (nil) payload is rejected; this profile is inline only.", + "spec_refs": [ + "cose-envelope-v0.2 4", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a0f65840819b17572c67eece4a1b20557f1d861a295c1fc3287b8a5cd1e6f93e48ec8553e22fcd94eba9ac6edfdfae0316cda3e35609637f27d2bba0bc148dc6c82b8409", + "signature_valid": false, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-009.json b/python/tests/vectors/AM-VEC-COSE-009.json new file mode 100644 index 0000000..4ee93df --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-009.json @@ -0,0 +1,27 @@ +{ + "id": "AM-VEC-COSE-009", + "description": "A trusted key not authorized for the manifest's issuer is rejected. The envelope is byte-identical to AM-VEC-COSE-001 and the signature verifies; only the issuer binding differs.", + "spec_refs": [ + "5.3", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a05902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840819b17572c67eece4a1b20557f1d861a295c1fc3287b8a5cd1e6f93e48ec8553e22fcd94eba9ac6edfdfae0316cda3e35609637f27d2bba0bc148dc6c82b8409", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + }, + "trusted_key_issuers": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": [ + "spiffe://trust.example/other-authority" + ] + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": true + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-010.json b/python/tests/vectors/AM-VEC-COSE-010.json new file mode 100644 index 0000000..ef2b665 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-010.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-010", + "description": "A payload with a duplicate member name is rejected rather than resolved. The signature over these bytes is valid: the two issuer values are the only defect.", + "spec_refs": [ + "cose-envelope-v0.2 4", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a059024f7b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f61747461636b6572222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840667127d45369037bcc57a1540143ba2cf4324f70c6ded79721978266964f1c96c3ab2a39ab73ea1d95769b0f65c5755b1f8a02d1a506b77dbc38e89a2792ea00", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-011.json b/python/tests/vectors/AM-VEC-COSE-011.json new file mode 100644 index 0000000..2fffb25 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-011.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-011", + "description": "A payload containing the non-JSON literal NaN is rejected, not accepted as a parser extension. The signature over these bytes is valid.", + "spec_refs": [ + "cose-envelope-v0.2 4", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a059024d7b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c226174746573746174696f6e223a7b226e6f6e63655f736b65775f7365636f6e6473223a4e614e7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d584029de996cfa360ace89a6f26c7fb5ebe65d62e57c48cf8a294f6d703dcf8c2e668a50122c17f9f9e95f598967c763b1568ffad9f80ab04a27173dad5224b24904", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-012.json b/python/tests/vectors/AM-VEC-COSE-012.json new file mode 100644 index 0000000..367b556 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-012.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-012", + "description": "A payload declaring version 0.1 inside a 0.2 COSE envelope returns INCOMPATIBLE_VERSION. The envelope is well formed and the signature over it is valid.", + "spec_refs": [ + "cose-envelope-v0.2 6", + "2.4" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a05902247b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e31227d5840592fc0c2bb179ddfd0b7f2c9a87e34c175644b407c614990636701adab4b2be912824f1602bee2ec678b7093be37de9f2b14cf57a266a81828f318315cbf270f", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "INCOMPATIBLE_VERSION", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-013.json b/python/tests/vectors/AM-VEC-COSE-013.json new file mode 100644 index 0000000..73da989 --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-013.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-013", + "description": "A payload nested past the accepted depth is rejected with a verdict, not a stack exhaustion. The signature over these bytes is valid.", + "spec_refs": [ + "cose-envelope-v0.2 4", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a05904147b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c226174746573746174696f6e223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a7b2261223a317d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d5840cf96327fea5f5f41a99b3d5668f22e54137c93e138896ab6bf172f710c8ce42e8250fa9af4c1cd86f9331496b398efc8eb7d0d13d6537b36f22d34aa2b35ea01", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-014.json b/python/tests/vectors/AM-VEC-COSE-014.json new file mode 100644 index 0000000..113057f --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-014.json @@ -0,0 +1,76 @@ +{ + "id": "AM-VEC-COSE-014", + "description": "A version 0.2 manifest presented with a v0.1 detached signature block is rejected. The signature is valid and the version is supported; the envelope is the defect.", + "spec_refs": [ + "cose-envelope-v0.2 6", + "2.4" + ], + "manifest": { + "manifest_id": "018f4a3b-2c1d-7e5f-a8b9-0d1e2f3a4b5c", + "agent_id": "spiffe://trust.example/agent/kyc/prod", + "version": "0.2", + "issued_at": "2025-01-01T00:00:00Z", + "expires_at": "2099-12-31T23:59:59Z", + "issuer": "spiffe://trust.example/signing-authority", + "crypto_profile": "standard", + "artifacts": { + "system_prompt": { + "hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_bundle": { + "hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "model_identity": { + "model_hash": null, + "version": "claude-3", + "deployment_type": "api" + } + }, + "delegation_chain": [], + "hitl_record": null, + "signature": { + "algorithm": "Ed25519", + "key_id": "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c", + "key_type": "software", + "signed_at": "2025-01-01T00:00:00Z", + "signature_value": "DyP2zkq1nN-cS9ZofxjorGNzuomwKhNtV8f4KV7RCjZCAg_GAeQu2xVDw16-fp__L93fPLR3AwT1W6tCJmQpCg", + "signed_fields": [ + "@context", + "@type", + "manifest_id", + "previous_manifest_id", + "agent_id", + "version", + "min_verifier_version", + "issued_at", + "expires_at", + "issuer", + "crypto_profile", + "profile", + "unbound_artifacts", + "source_bundle", + "artifacts", + "delegation_chain", + "hitl_record", + "prior_transparency_log_entry", + "log_retention", + "data_scope", + "operational_lifecycle", + "intent" + ] + } + }, + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/AM-VEC-COSE-015.json b/python/tests/vectors/AM-VEC-COSE-015.json new file mode 100644 index 0000000..fc2765d --- /dev/null +++ b/python/tests/vectors/AM-VEC-COSE-015.json @@ -0,0 +1,22 @@ +{ + "id": "AM-VEC-COSE-015", + "description": "A payload containing the non-JSON literal Infinity is rejected. Companion to AM-VEC-COSE-011: a verifier that special-cases NaN passes that one and fails this. The signature over these bytes is valid.", + "spec_refs": [ + "cose-envelope-v0.2 4", + "cose-envelope-v0.2 6" + ], + "envelope_hex": "d284586aa4013203781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b6a736f6e04582056475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c10781f6170706c69636174696f6e2f6167656e742d6d616e69666573742b636f7365a05902527b226167656e745f6964223a227370696666653a2f2f74727573742e6578616d706c652f6167656e742f6b79632f70726f64222c22617274696661637473223a7b226d6f64656c5f6964656e74697479223a7b226465706c6f796d656e745f74797065223a22617069222c2276657273696f6e223a22636c617564652d33227d2c22706f6c6963795f62756e646c65223a7b2268617368223a227368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262227d2c2273797374656d5f70726f6d7074223a7b2268617368223a227368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161227d7d2c226174746573746174696f6e223a7b226e6f6e63655f736b65775f7365636f6e6473223a496e66696e6974797d2c2263727970746f5f70726f66696c65223a227374616e64617264222c22657870697265735f6174223a22323039392d31322d33315432333a35393a35395a222c226973737565645f6174223a22323032352d30312d30315430303a30303a30305a222c22697373756572223a227370696666653a2f2f74727573742e6578616d706c652f7369676e696e672d617574686f72697479222c226d616e69666573745f6964223a2230313866346133622d326331642d376535662d613862392d306431653266336134623563222c2276657273696f6e223a22302e32227d58401a816a06dd096ad18443f7d778aaae6382eaec28742d98227b0aad4e60db01bc2e287079195aa92ce66a6ede232d145d8d651d56d43bfa7f33466796d12bf700", + "signature_valid": true, + "context": { + "system_prompt_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_bundle_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "model_version": "claude-3", + "trusted_keys": { + "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg" + } + }, + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} diff --git a/python/tests/vectors/README.md b/python/tests/vectors/README.md index 4204a13..829e20f 100644 --- a/python/tests/vectors/README.md +++ b/python/tests/vectors/README.md @@ -73,7 +73,136 @@ The `cose` block is the point of these vectors: an implementation must produce Only Ed25519 envelopes can be pinned this way. ML-DSA-65 signing is hedged, so a post-quantum or hybrid envelope differs on every run and only its structure -is stable. +is stable. There is deliberately no post-quantum COSE vector: one whose bytes +changed on every regeneration would be a snapshot rather than a contract, and +shipping a private seed for consumers to regenerate against would break the +rule below that no private key material is written to disk. + +### Negative COSE vectors + +`AM-VEC-COSE-002` onward are envelopes a conforming verifier **must not +accept**. They carry `envelope_hex` and an expected result, and deliberately +**no `expected.cose` block**: the bytes are malformed by construction, so +pinning their decomposition would assert that a verifier can parse something +it is being told to reject. + +```jsonc +{ + "id": "AM-VEC-COSE-003", + "description": "alg present in the unprotected header is rejected, never read.", + "envelope_hex": "d284586aa401270378...", + "signature_valid": true, // the Ed25519 signature over the + "context": { ... }, // Sig_structure verifies; see below + "expected": { + "result": "MISMATCH", + "signature_verified": false + } +} +``` + +Consume them exactly as the positive ones: decode `envelope_hex` and run your +verifier over the bytes. The only difference is what you assert. + +Three properties are worth knowing before you rely on them. + +**They state that a manifest is rejected, not why.** Every structural +rejection maps to `MISMATCH`, so a verifier that rejects one of these for the +wrong reason still passes. Each vector's `description` and `spec_refs` name +the rule actually under test, and an implementation that wants stronger +assurance should check it rejects for that reason. + +**`signature_valid` tells you whether the signature is the reason.** Where it +is `true`, the signature verifies under the published key, so a verifier cannot +pass the vector by rejecting a broken signature and never reaching the rule the +vector names. Every negative declares it, and a test re-derives it from the +bytes on disk rather than trusting the generator. Which pre-image applies +follows the envelope: the RFC 9052 `Sig_structure` for an `envelope_hex` +vector, and the RFC 8785 signing pre-image for `AM-VEC-COSE-014`, whose subject +is a manifest document with a v0.1 detached signature block. + +Two are `false`, both by design: `AM-VEC-COSE-002` tampers with the protected +header, which is the rule under test, and `AM-VEC-COSE-008` has a nil payload, +so there is no `Sig_structure` to verify over. + +Note that `signature_valid` is a fact about the bytes and +`expected.signature_verified` is what your verifier should report. They differ +wherever a rule fires before signature appraisal: `AM-VEC-COSE-010` carries a +valid signature over a payload the verifier rejects before it gets that far. + +**One of them expects `VALID`.** `AM-VEC-COSE-005` injects an unprotected +header after signing, and a conforming verifier must still return `VALID`, +because nothing in the unprotected header is covered by the signature and +section 6 step 7 evaluates it last. A verifier that merged the two halves, or +read `kid` from the malleable one, fails it. It is filed with the negatives +because it tests the same rule from the other side. + +#### What each negative covers + +| Vector | Rule under test | Expected | +|--------|-----------------|----------| +| `AM-VEC-COSE-002` | A tampered protected header invalidates the signature | `MISMATCH` | +| `AM-VEC-COSE-003` | `alg` in the unprotected header is rejected, never read | `MISMATCH` | +| `AM-VEC-COSE-004` | A vendor-tree `typ` alias is rejected | `MISMATCH` | +| `AM-VEC-COSE-005` | An unprotected header injected after signing changes nothing | `VALID` | +| `AM-VEC-COSE-006` | An untagged COSE structure is rejected, not inferred | `MISMATCH` | +| `AM-VEC-COSE-007` | Trailing bytes after the COSE object are rejected | `MISMATCH` | +| `AM-VEC-COSE-008` | A detached (nil) payload is rejected; this profile is inline only | `MISMATCH` | +| `AM-VEC-COSE-009` | A trusted key not authorized for the manifest's issuer | `MISMATCH` | +| `AM-VEC-COSE-010` | A duplicate JSON member name is rejected, not resolved | `MISMATCH` | +| `AM-VEC-COSE-011` | The non-JSON literal `NaN` is rejected, not accepted | `MISMATCH` | +| `AM-VEC-COSE-012` | A `0.1` payload in a `0.2` envelope routes on the payload | `INCOMPATIBLE_VERSION` | +| `AM-VEC-COSE-013` | A payload nested past the depth bound yields a verdict | `MISMATCH` | +| `AM-VEC-COSE-014` | A `0.2` manifest may not fall back to the v0.1 envelope | `MISMATCH` | +| `AM-VEC-COSE-015` | The non-JSON literal `Infinity` is rejected likewise | `MISMATCH` | + +`AM-VEC-COSE-009` is worth calling out. Its envelope is **byte-identical** to +`AM-VEC-COSE-001`; only `context.trusted_key_issuers` differs, binding the +signing key to an issuer other than the manifest's. Nothing about the object +can explain the rejection, so a verifier that stops at "the signature verifies +under a trusted key" returns `VALID` and has no authorization boundary at all. +It is the one negative that reports `signature_verified: true`. + +`AM-VEC-COSE-010`, `011` and `013` place their defect inside `attestation`, +which the schema types as a free-form object, so the rest of the document is +valid and the named defect is the only thing wrong with it. All three are +signed **over** the malformed payload rather than having bytes swapped into an +already-signed envelope, which is what keeps them from degrading into +signature-failure tests. + +`AM-VEC-COSE-014` is the only vector in the COSE series carrying `manifest` +rather than `envelope_hex`, because the rule under test is precisely that a +v0.2 document must **not** be accepted outside a COSE envelope. It is the other +half of `012`: the version gate is bidirectional, and a one-way gate is not a +gate, since anyone unable to produce a valid COSE envelope would simply present +the manifest in the envelope still accepted. It expects `MISMATCH` rather than +`INCOMPATIBLE_VERSION` deliberately, because the verifier does support 0.2; +what it will not do is verify 0.2 through the v0.1 path, and reporting an +unsupported version would state something untrue about its capabilities. + +`test_cose_negative_vector_isolates_its_named_defect` asserts the isolation +claim rather than leaving it to the descriptions: it strips only the defect +each vector names and requires the result to verify `VALID`. If a vector +carried a second defect, the repaired object would not verify and the test +says so. + +#### One case that is deliberately not a vector + +The hybrid authorization case, a `COSE_Sign` carrying one authorized component +key alongside one unauthorized one, is **not** here and cannot be. A hybrid +envelope contains an ML-DSA-65 signature, ML-DSA-65 signing is hedged, and +`cryptography` 49 exposes no deterministic mode, so the bytes differ on every +regeneration. A vector that could not be regenerated would be a snapshot rather +than a contract, and exempting one from the regeneration check would remove the +guarantee that makes the rest of the suite trustworthy. + +It is covered instead by +`test_every_hybrid_signer_must_be_authorized_for_the_issuer` in +[`tests/test_cose.py`](../test_cose.py), which generates its keys per run. An +implementation in another language should treat the rule as binding and test it +locally the same way: **every** signer in a `COSE_Sign` must be authorized for +the payload's `issuer`, not merely one of them. `AM-VEC-COSE-009` fixes the +single-signer half of that rule portably. If a deterministic ML-DSA-65 signing +mode becomes available, the hybrid half can join it. `context` maps field-for-field onto the SDK's `VerificationContext`, so a Python consumer is just `VerificationContext(**vector["context"])`. Other languages @@ -120,6 +249,19 @@ The Python reference assertion lives in `ATTESTATION_UNAVAILABLE` (attestation enforced but absent). * HITL approved / missing / expired, and memory-baseline TTL expiry. +`AM-VEC-COSE-001` … `AM-VEC-COSE-015` cover the v0.2 envelope: one vector +pinning the encoding byte for byte, and fourteen negatives spanning the +protected/unprotected header split, CBOR tagging and framing, payload +presence, the issuer authorization boundary, the two JSON parser divergences +(duplicate member names, non-finite numbers), version routing in both +directions, and the payload depth bound. + +`011` and `015` are a pair on purpose. #243 records `NaN` and `Infinity` as one +class of defect, but they are not one code path in every parser, so a verifier +that special-cases `NaN` passes `011` and fails `015`. `-Infinity` travels the +same path as `Infinity` in every parser checked and is covered by `015` rather +than given a third vector. + > Note: `AM-VEC-013` returns overall `VALID` while `memory_baseline` is > `EXPIRED` — this faithfully encodes the reference engine's behaviour (an > expired baseline is surfaced per-field but is not, on its own, a hard @@ -135,3 +277,8 @@ python -m tests.vectors.generate Regenerate only when the engine's normative behaviour changes, and review the diff. The generated files are committed so consumers don't need to run Python. + +`test_committed_vectors_match_a_fresh_regeneration` rebuilds every vector in +memory and diffs it against the committed copy, so a file edited by hand, or a +generator change made without regenerating, fails the suite rather than +shipping as a contract nobody can reproduce. diff --git a/python/tests/vectors/generate.py b/python/tests/vectors/generate.py index 670c8a3..0300029 100644 --- a/python/tests/vectors/generate.py +++ b/python/tests/vectors/generate.py @@ -29,6 +29,7 @@ """ from __future__ import annotations +import base64 import copy import hashlib import json @@ -38,8 +39,25 @@ import cbor2 from agent_manifest._canonicalize import canonicalize +from agent_manifest._cose import ( + ALG_ED25519, + COSE_SIGN1_TAG, + HDR_ALG, + HDR_CONTENT_TYPE, + HDR_KID, + HDR_TYP, + MEDIA_TYPE_MANIFEST_COSE, + MEDIA_TYPE_MANIFEST_JSON, + _sig_structure_sign1, + payload_hash, + sign_cose_sign1, +) from agent_manifest._delegation import DelegationHopSigner -from agent_manifest._signing import Ed25519Signer, ed25519_from_private_bytes +from agent_manifest._signing import ( + Ed25519Signer, + ed25519_from_private_bytes, + signing_pre_image, +) HERE = Path(__file__).parent @@ -159,8 +177,6 @@ def cose_encoding_vector() -> dict[str, Any]: pinned this way: ML-DSA-65 signing is hedged, so the signature bytes differ per run and only the structure is stable. """ - from agent_manifest._cose import payload_hash, sign_cose_sign1 - manifest = cose_manifest() envelope = sign_cose_sign1(manifest, KP) protected, unprotected, payload, signature = cbor2.loads(envelope).value @@ -191,6 +207,401 @@ def cose_encoding_vector() -> dict[str, Any]: } +def _detached_signature_is_valid(manifest: dict[str, Any]) -> bool: + """The ``signature_valid`` counterpart for a v0.1 detached signature block. + + Same meaning as :func:`_signature_is_valid`, over the pre-image the v0.1 + envelope signs rather than an RFC 9052 Sig_structure. Only AM-VEC-COSE-014 + needs it, because it is the one COSE-series vector whose subject is a + manifest document. + """ + block = manifest.get("signature") + if not block: + return False + try: + raw = base64.urlsafe_b64decode(block["signature_value"] + "====") + KP.public_key.verify(raw, signing_pre_image(manifest)) + except Exception: + return False + return True + + +def _signature_is_valid(envelope: bytes) -> bool: + """Does the Ed25519 signature in *envelope* verify over its Sig_structure? + + Recorded on every negative vector as ``signature_valid``. It is the + difference between a vector that tests the rule it names and one that a + verifier passes by rejecting a broken signature and never reaching that + rule. Computed from the finished bytes rather than passed in, so it cannot + drift from what the vector actually contains. + """ + try: + decoded = cbor2.loads(envelope) + body = decoded.value if isinstance(decoded, cbor2.CBORTag) else decoded + protected, _unprotected, payload, signature = body + if payload is None: + return False + KP.public_key.verify(signature, _sig_structure_sign1(protected, payload)) + except Exception: + return False + return True + + +def _cose_negative( + vid: str, + description: str, + spec_refs: list[str], + envelope: bytes, + expected_result: str, + *, + signature_verified: bool = False, + context: dict[str, Any] | None = None, +) -> dict[str, Any]: + """A COSE vector whose envelope a conforming verifier must not accept. + + Negatives carry `envelope_hex` and an expected result, and deliberately + not an `expected.cose` block: the bytes are malformed by construction, so + pinning their decomposition would assert that a verifier can parse + something it is being told to reject. + + The schema states *that* a manifest is rejected, not *why*. A verifier + that rejects one of these for the wrong reason still passes. ``signature_valid`` + narrows that: where it is true, a rejection cannot have come from signature + verification, so the named rule is the only thing left to reject on. + """ + return { + "id": vid, + "description": description, + "spec_refs": spec_refs, + "envelope_hex": envelope.hex(), + "signature_valid": _signature_is_valid(envelope), + "context": base_context() if context is None else context, + "expected": { + "result": expected_result, + "signature_verified": signature_verified, + }, + } + + +def _sign_payload(payload: bytes) -> bytes: + """A well-formed COSE_Sign1 carrying *payload* verbatim. + + Used by the vectors whose defect is in the payload. Signing over the + malformed bytes rather than swapping them into an already-signed envelope + is what keeps the payload rule the only thing wrong with the object: a + verifier that checked the signature and stopped would accept it. + """ + protected = cbor2.dumps( + { + HDR_ALG: ALG_ED25519, + HDR_CONTENT_TYPE: MEDIA_TYPE_MANIFEST_JSON, + HDR_KID: hashlib.sha256(KP.public_bytes).digest(), + HDR_TYP: MEDIA_TYPE_MANIFEST_COSE, + }, + canonical=True, + ) + signature = KP.private_key.sign(_sig_structure_sign1(protected, payload)) + return _retag(COSE_SIGN1_TAG, [protected, {}, payload, signature]) + + +def _signed_cose_parts() -> tuple[bytes, bytes, dict[Any, Any], bytes, bytes]: + """A valid envelope and its four decoded elements, for mutation.""" + envelope = sign_cose_sign1(cose_manifest(), KP) + protected, unprotected, payload, signature = cbor2.loads(envelope).value + return envelope, protected, dict(unprotected), payload, signature + + +def _retag(tag: int, body: list[Any]) -> bytes: + return cbor2.dumps(cbor2.CBORTag(tag, body), canonical=True) + + +def cose_negative_vectors() -> list[dict[str, Any]]: + """The negative cases a conforming verifier must reject. + + 002 to 005 are named in section 9 of the envelope specification. 006 to 008 + are places a CBOR implementation genuinely differs, so they catch + cross-language divergence rather than restating v0.1 behaviour. 009 to 013 + are the cases carried over from the phase 2 security follow-up on issue + #243: the authorization boundary, the two JSON parser divergences, version + routing, and the depth bound. + + Every one of 009 to 013 signs over the payload under test rather than + swapping bytes into an already-signed envelope, so each carries a valid + signature and the rule it names is the only reason to reject it. + """ + vectors: list[dict[str, Any]] = [] + _, protected, _, payload, signature = _signed_cose_parts() + + # 002: the protected header is covered by the signature, so editing it + # invalidates the signature even when the edit is semantically harmless. + # This is the case re-serialising a header to inspect it would break. + tampered_header = dict(cbor2.loads(protected)) + tampered_header[7] = "injected" + vectors.append(_cose_negative( + "AM-VEC-COSE-002", + "A tampered protected header invalidates the signature.", + ["cose-envelope-v0.2 3", "cose-envelope-v0.2 6"], + _retag(18, [cbor2.dumps(tampered_header, canonical=True), {}, payload, signature]), + "MISMATCH", + )) + + # 003: alg placed in the unprotected header, which is not covered by the + # signature. A verifier that reads alg from the malleable half can be told + # which algorithm to use by anyone who can modify the object in transit. + vectors.append(_cose_negative( + "AM-VEC-COSE-003", + "alg present in the unprotected header is rejected, never read.", + ["cose-envelope-v0.2 3", "cose-envelope-v0.2 6"], + _retag(18, [protected, {1: -49}, payload, signature]), + "MISMATCH", + )) + + # 004: a vendor-tree alias for typ. Section 7 forbids accepting one: + # two valid type values for one object type is the ambiguity typ removes. + # + # The signature is computed over the aliased header rather than copied from + # a differently-signed object, so the signature is valid and the typ value + # is the only defect. Editing typ in an already-signed header would have + # broken the signature too, and a verifier that checked the signature and + # never implemented the typ rule would have passed the vector for the wrong + # reason. + aliased_header = dict(cbor2.loads(protected)) + aliased_header[16] = "application/vnd.agent-manifest+cose" + aliased_protected = cbor2.dumps(aliased_header, canonical=True) + aliased_signature = KP.private_key.sign( + _sig_structure_sign1(aliased_protected, payload) + ) + vectors.append(_cose_negative( + "AM-VEC-COSE-004", + ( + "A vendor-tree typ alias is rejected. The signature over this " + "envelope is valid, so typ is the only defect." + ), + ["cose-envelope-v0.2 3", "cose-envelope-v0.2 7"], + _retag(18, [aliased_protected, {}, payload, aliased_signature]), + "MISMATCH", + )) + + # 005: the positive half of the same rule. An unprotected header injected + # after signing MUST NOT change the verdict, because nothing in it is + # covered and step 7 is evaluated last. A verifier that merged the two + # halves, or took kid from the malleable one, would fail this. + vectors.append(_cose_negative( + "AM-VEC-COSE-005", + ( + "An unprotected header injected after signing does not change the " + "verdict: kid is read from the protected header only." + ), + ["cose-envelope-v0.2 4.1", "cose-envelope-v0.2 6"], + _retag(18, [protected, {4: b"\x00" * 32}, payload, signature]), + "VALID", + signature_verified=True, + )) + + # 006: untagged. The tag is what tells a relying party which procedure + # applies; inferring it from the array shape is the guess this envelope + # exists to remove. + vectors.append(_cose_negative( + "AM-VEC-COSE-006", + "An untagged COSE structure is rejected rather than inferred.", + ["cose-envelope-v0.2 2", "cose-envelope-v0.2 6"], + cbor2.dumps([protected, {}, payload, signature], canonical=True), + "MISMATCH", + )) + + # 007: trailing bytes. CBOR decoders commonly stop at the end of the first + # object and ignore the rest, which would let one octet string carry a + # second manifest behind the first. + valid = sign_cose_sign1(cose_manifest(), KP) + vectors.append(_cose_negative( + "AM-VEC-COSE-007", + "Trailing bytes after the COSE object are rejected.", + ["cose-envelope-v0.2 6"], + valid + b"\x00", + "MISMATCH", + )) + + # 008: detached payload. Permitted by SCITT, not by this profile, and a + # verifier that accepts nil here would verify a signature over bytes it + # never saw. + vectors.append(_cose_negative( + "AM-VEC-COSE-008", + "A detached (nil) payload is rejected; this profile is inline only.", + ["cose-envelope-v0.2 4", "cose-envelope-v0.2 6"], + _retag(18, [protected, {}, None, signature]), + "MISMATCH", + )) + + # 009: the signing key is trusted, but not for this issuer. The envelope is + # byte-identical to AM-VEC-COSE-001, which is the point: only the context + # differs, so nothing about the object can explain the rejection. A + # verifier that stops at "the signature verifies under a trusted key" + # returns VALID here and has no authorization boundary at all. + vectors.append(_cose_negative( + "AM-VEC-COSE-009", + ( + "A trusted key not authorized for the manifest's issuer is " + "rejected. The envelope is byte-identical to AM-VEC-COSE-001 and " + "the signature verifies; only the issuer binding differs." + ), + ["5.3", "cose-envelope-v0.2 6"], + sign_cose_sign1(cose_manifest(), KP), + "MISMATCH", + signature_verified=True, + context=base_context( + trusted_key_issuers={KEY_ID: ["spiffe://trust.example/other-authority"]} + ), + )) + + # 010: a duplicate member name. RFC 8259 section 4 states that the + # behaviour of an implementation given these is unpredictable, and RFC 8785 + # forbids them outright. The second issuer is an attacker-chosen value, so + # a last-wins parser and a first-wins parser attribute the same signed + # bytes to two different authorities, and both consider the signature + # valid. Rejecting is the only answer that cannot differ between them. + canonical = canonicalize(cose_manifest()) + first_issuer = canonical.index(b'"issuer":') + after_issuer = canonical.index(b",", first_issuer) + duplicated = ( + canonical[:after_issuer] + + b',"issuer":"spiffe://trust.example/attacker"' + + canonical[after_issuer:] + ) + vectors.append(_cose_negative( + "AM-VEC-COSE-010", + ( + "A payload with a duplicate member name is rejected rather than " + "resolved. The signature over these bytes is valid: the two " + "issuer values are the only defect." + ), + ["cose-envelope-v0.2 4", "cose-envelope-v0.2 6"], + _sign_payload(duplicated), + "MISMATCH", + )) + + # 011: NaN. Not JSON (RFC 8259 section 6 admits no non-finite values), but + # several parsers accept it as an extension, Python's own among them unless + # told otherwise. It is placed in `attestation`, which is a free-form + # object, so the manifest is schema-valid everywhere else and the literal + # is the only thing wrong with it. + placeholder = canonicalize(cose_manifest(attestation={"placeholder": 0})) + non_finite = placeholder.replace( + b'{"placeholder":0}', b'{"nonce_skew_seconds":NaN}' + ) + assert b"NaN" in non_finite, "the non-finite literal was not spliced in" + vectors.append(_cose_negative( + "AM-VEC-COSE-011", + ( + "A payload containing the non-JSON literal NaN is rejected, not " + "accepted as a parser extension. The signature over these bytes " + "is valid." + ), + ["cose-envelope-v0.2 4", "cose-envelope-v0.2 6"], + _sign_payload(non_finite), + "MISMATCH", + )) + + # 012: a 0.1 payload in a 0.2 envelope. Section 6 step 3 routes on the + # payload's own version, so this must come back INCOMPATIBLE_VERSION rather + # than being verified under 0.2 rules because the envelope looks like one. + # Distinct from the other negatives in expected result, deliberately: an + # unsupported version is a capability statement, not a malformed object. + vectors.append(_cose_negative( + "AM-VEC-COSE-012", + ( + "A payload declaring version 0.1 inside a 0.2 COSE envelope " + "returns INCOMPATIBLE_VERSION. The envelope is well formed and " + "the signature over it is valid." + ), + ["cose-envelope-v0.2 6", "2.4"], + _sign_payload(canonicalize(cose_manifest(version="0.1"))), + "INCOMPATIBLE_VERSION", + )) + + # 013: nesting past the accepted depth. A manifest is untrusted input, so + # this has to produce a verdict rather than exhaust the stack - a verifier + # that recurses without a bound crashes on it instead of returning + # anything. Nested inside `attestation` for the same reason as 011: the + # rest of the document is valid, so depth is the only defect. + deeply_nested = placeholder.replace( + b'{"placeholder":0}', + ('{"a":' * 80 + "1" + "}" * 80).encode(), + ) + vectors.append(_cose_negative( + "AM-VEC-COSE-013", + ( + "A payload nested past the accepted depth is rejected with a " + "verdict, not a stack exhaustion. The signature over these bytes " + "is valid." + ), + ["cose-envelope-v0.2 4", "cose-envelope-v0.2 6"], + _sign_payload(deeply_nested), + "MISMATCH", + )) + + # 014: the other half of the version gate. #274 made it bidirectional, so a + # 0.2 manifest must not fall back to the v0.1 detached signature block any + # more than a 0.1 payload may be verified under 0.2 rules (012). A one-way + # gate is not a gate: an attacker who cannot produce a valid COSE envelope + # would simply present the manifest in the envelope that is still accepted. + # + # It is the one vector in the COSE series carrying `manifest` rather than + # `envelope_hex`, because the rule under test is precisely that this + # document must not be accepted outside a COSE envelope. The signature over + # it is valid and the version is one the verifier supports, so the envelope + # pairing is the only defect: the same document at version 0.1 is + # AM-VEC-001, which verifies VALID. + # + # MISMATCH rather than INCOMPATIBLE_VERSION, deliberately. The verifier + # supports 0.2; what it will not do is verify 0.2 here. Reporting an + # unsupported version would state something untrue about its capabilities. + fallback = base_manifest(version="0.2") + vectors.append({ + "id": "AM-VEC-COSE-014", + "description": ( + "A version 0.2 manifest presented with a v0.1 detached signature " + "block is rejected. The signature is valid and the version is " + "supported; the envelope is the defect." + ), + "spec_refs": ["cose-envelope-v0.2 6", "2.4"], + "manifest": fallback, + # Carried for the same reason as on every other negative, over the + # pre-image this envelope signs rather than a Sig_structure. Without it + # a verifier could pass the vector by rejecting the signature and never + # reaching the version rule. + "signature_valid": _detached_signature_is_valid(fallback), + "context": base_context(), + "expected": { + "result": "MISMATCH", + "signature_verified": False, + }, + }) + + # 015: the other literal finding 2 on #243 names. NaN and Infinity are one + # class of defect but not one code path in every parser, so a verifier + # could special-case NaN, pass 011, and still accept this. -Infinity + # travels the same path as Infinity in every parser checked, so it is + # covered by this vector rather than given a third. + infinite = placeholder.replace( + b'{"placeholder":0}', b'{"nonce_skew_seconds":Infinity}' + ) + assert b"Infinity" in infinite, "the non-finite literal was not spliced in" + vectors.append(_cose_negative( + "AM-VEC-COSE-015", + ( + "A payload containing the non-JSON literal Infinity is rejected. " + "Companion to AM-VEC-COSE-011: a verifier that special-cases NaN " + "passes that one and fails this. The signature over these bytes " + "is valid." + ), + ["cose-envelope-v0.2 4", "cose-envelope-v0.2 6"], + _sign_payload(infinite), + "MISMATCH", + )) + + return vectors + + def _vector( vid: str, description: str, @@ -437,6 +848,7 @@ def build() -> list[dict[str, Any]]: # --- version 0.2, COSE envelope (ADR-0011, issue #243) ----------------- vectors.append(cose_encoding_vector()) + vectors.extend(cose_negative_vectors()) return vectors @@ -457,10 +869,14 @@ def main() -> None: index = { "suite": "agent-manifest-verification", - "spec_version": "0.1", + # The suite is written against the 0.2 spec, which defines both + # envelopes. It said 0.1 while nearly half the vectors target the v0.2 + # COSE envelope, which is the first thing a consumer reads. + "spec_version": "0.2", "description": "Language-neutral verification conformance vectors. " - "Each vector: a manifest, a VerificationContext, and the " - "expected VerificationResult.", + "Each vector: a manifest or a COSE envelope, a " + "VerificationContext, and the expected " + "VerificationResult.", # A vector carries either `manifest` (a version 0.1 document with a # detached signature block) or `envelope_hex` (a version 0.2 COSE # object). The envelope follows the manifest version, so a consumer diff --git a/python/tests/vectors/index.json b/python/tests/vectors/index.json index 5beeea3..0418812 100644 --- a/python/tests/vectors/index.json +++ b/python/tests/vectors/index.json @@ -1,7 +1,7 @@ { "suite": "agent-manifest-verification", - "spec_version": "0.1", - "description": "Language-neutral verification conformance vectors. Each vector: a manifest, a VerificationContext, and the expected VerificationResult.", + "spec_version": "0.2", + "description": "Language-neutral verification conformance vectors. Each vector: a manifest or a COSE envelope, a VerificationContext, and the expected VerificationResult.", "envelopes": { "manifest": "v0.1 detached signature over an RFC 8785 pre-image", "envelope_hex": "v0.2 COSE_Sign1 / COSE_Sign, CBOR" @@ -112,6 +112,76 @@ "id": "AM-VEC-COSE-001", "file": "AM-VEC-COSE-001.json", "description": "COSE_Sign1 encoding is pinned byte-for-byte: CBOR tag 18, a four-element array, and a zero-length unprotected header map before any receipt is attached." + }, + { + "id": "AM-VEC-COSE-002", + "file": "AM-VEC-COSE-002.json", + "description": "A tampered protected header invalidates the signature." + }, + { + "id": "AM-VEC-COSE-003", + "file": "AM-VEC-COSE-003.json", + "description": "alg present in the unprotected header is rejected, never read." + }, + { + "id": "AM-VEC-COSE-004", + "file": "AM-VEC-COSE-004.json", + "description": "A vendor-tree typ alias is rejected. The signature over this envelope is valid, so typ is the only defect." + }, + { + "id": "AM-VEC-COSE-005", + "file": "AM-VEC-COSE-005.json", + "description": "An unprotected header injected after signing does not change the verdict: kid is read from the protected header only." + }, + { + "id": "AM-VEC-COSE-006", + "file": "AM-VEC-COSE-006.json", + "description": "An untagged COSE structure is rejected rather than inferred." + }, + { + "id": "AM-VEC-COSE-007", + "file": "AM-VEC-COSE-007.json", + "description": "Trailing bytes after the COSE object are rejected." + }, + { + "id": "AM-VEC-COSE-008", + "file": "AM-VEC-COSE-008.json", + "description": "A detached (nil) payload is rejected; this profile is inline only." + }, + { + "id": "AM-VEC-COSE-009", + "file": "AM-VEC-COSE-009.json", + "description": "A trusted key not authorized for the manifest's issuer is rejected. The envelope is byte-identical to AM-VEC-COSE-001 and the signature verifies; only the issuer binding differs." + }, + { + "id": "AM-VEC-COSE-010", + "file": "AM-VEC-COSE-010.json", + "description": "A payload with a duplicate member name is rejected rather than resolved. The signature over these bytes is valid: the two issuer values are the only defect." + }, + { + "id": "AM-VEC-COSE-011", + "file": "AM-VEC-COSE-011.json", + "description": "A payload containing the non-JSON literal NaN is rejected, not accepted as a parser extension. The signature over these bytes is valid." + }, + { + "id": "AM-VEC-COSE-012", + "file": "AM-VEC-COSE-012.json", + "description": "A payload declaring version 0.1 inside a 0.2 COSE envelope returns INCOMPATIBLE_VERSION. The envelope is well formed and the signature over it is valid." + }, + { + "id": "AM-VEC-COSE-013", + "file": "AM-VEC-COSE-013.json", + "description": "A payload nested past the accepted depth is rejected with a verdict, not a stack exhaustion. The signature over these bytes is valid." + }, + { + "id": "AM-VEC-COSE-014", + "file": "AM-VEC-COSE-014.json", + "description": "A version 0.2 manifest presented with a v0.1 detached signature block is rejected. The signature is valid and the version is supported; the envelope is the defect." + }, + { + "id": "AM-VEC-COSE-015", + "file": "AM-VEC-COSE-015.json", + "description": "A payload containing the non-JSON literal Infinity is rejected. Companion to AM-VEC-COSE-011: a verifier that special-cases NaN passes that one and fails this. The signature over these bytes is valid." } ] }