Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,49 @@ All notable changes to Agent Manifest are documented here. Format follows [Keep

## [Unreleased]

### Added

**[SDK] COSE_Sign1 signing and verification for manifest version 0.2** ([`_cose.py`](python/src/agent_manifest/_cose.py), [ADR-0011](docs/adr/0011-signature-envelope.md), [ADR-0013](docs/adr/0013-cbor-library-for-cose.md)). Phase 2 of the envelope migration (issue #243). `verify_manifest()` now takes either a dict or bytes: **a dict is a v0.1 manifest and verifies exactly as it does today**, bytes are a v0.2 COSE envelope. The envelope follows the manifest `version`, never a flag, so no existing record is reinterpreted.

What the envelope buys is structural rather than incremental. `alg` is in the protected header and covered by the signature, so the downgrade fixed in 0.6.0 by an explicit cross-check cannot be expressed at all. The payload is verified as received, so RFC 8785 is no longer an input to verification — it stays the producer-side determinism rule and the basis of the hash bound into hardware. Receipts (label 394), the TEE attestation report, and HITL approvals attach in the unprotected header, which retires the `signed_fields` coverage table, the `hitl_record.approvals` normalization rule, and the `transparency_log_entry` ordering rule together. Hardware now binds `sha256` of the payload bytes, with no field subset to keep in sync.

Hybrid is one `COSE_Sign` with two signers rather than two `COSE_Sign1` objects, so both signatures covering identical payload bytes is a property of the structure. A verifier that cannot perform ML-DSA-65 returns `UNVERIFIABLE` and never falls back to the classical entry.

**[SDK] New runtime dependency: `cbor2`.** Serialization only. The COSE structures are built in this repository and every signature stays on `cryptography` (and optionally `pyoqs`), so the SDK's crypto surface is unchanged. [ADR-0013](docs/adr/0013-cbor-library-for-cose.md) records why no COSE library was taken: neither `pycose` nor `cwt` ships the RFC 9964 code points that half this envelope needs, and both widen the dependency closure to do less.

### Fixed

**[SDK] The `pq` extra was uninstallable, and the module it imported is not the package it meant.** `pip install "agent-manifest[pq]"` required `pyoqs`, which is not published on PyPI, so the post-quantum profile could not be installed by following the documented instruction. Worse, the module name the SDK imports — `oqs` — belongs to an unrelated project on PyPI, and `_signing` treated a successful import as proof of liboqs. Installing that package turned every ML-DSA-65 call into an `AttributeError` (18 test failures) and made the SDK report a post-quantum capability it did not have.

**ML-DSA-65 now comes from `cryptography`**, which implements it through OpenSSL as of 47.0.0 and is already a required dependency. The `pq` extra is now `cryptography>=47` and installs cleanly. Deployments already carrying the liboqs bindings keep working: the backend is chosen by the key material, not by configuration, because the two differ in private key encoding — cryptography uses the 32-byte seed, liboqs the expanded secret key. **Public keys are the same 1952-byte encoding in both**, so `key_id`, the COSE `kid`, and every signature a third party verifies are unchanged and interoperable across backends. A build with neither backend still reports `UNVERIFIABLE` rather than accusing the manifest, unchanged from #245.

The capability check no longer trusts a module name: liboqs is now identified by its API. Thirteen post-quantum tests that skipped on every machine without liboqs — across signing, hybrid mode, evidence packs, and the COSE envelope — now execute against real FIPS 204 signatures.

### Changed

**[SPEC][SDK] Ed25519 is identified by `-19`, not the deprecated `EdDSA` `-8`** ([ADR-0014](docs/adr/0014-fully-specified-ed25519-code-point.md)). **RFC 9864 (Standards Track, October 2025) deprecated the polymorphic `EdDSA` identifier** and registered fully-specified ones; the IANA COSE Algorithms registry marks `-8` deprecated. Issue #243's "code points are settled: EdDSA -8" was therefore already out of date when phase 1 was written — the ML-DSA-65 half (`-49`) was confirmed correct against IANA. A producer now signs with `-19`; a verifier accepts both and **keeps accepting `-8` indefinitely**, because manifests are audit records with regulated retention and a signature cannot be re-issued under a new identifier without re-signing.

`-8` and `-19` name one algorithm, so anything reasoning about *which* algorithm signed compares algorithms rather than code points: a `post-quantum` profile is satisfied by neither, and a `COSE_Sign` carrying one entry of each is rejected as a single algorithm signed twice rather than accepted as a hybrid signature.

The cost is recorded in the ADR rather than glossed: **no COSE library implements RFC 9864 yet**, so a `-19` manifest cannot currently be verified by third-party tooling — the interop check runs against `-8` fixtures over the same structures for as long as that holds.

**[SDK] `AM-VEC-COSE-001` pins the COSE_Sign1 encoding byte-for-byte.** A vector now carries either `manifest` (v0.1) or `envelope_hex` (v0.2 COSE), and the same conformance loop runs both, since the engine selects the procedure from what it is handed. The vector fixes every element — tag, protected header, `unprotected_hex` of `a0`, payload, signature, and the payload hash hardware binds — so another language's SDK has something to agree with rather than only its own round-trip. Ed25519 only: ML-DSA-65 signing is hedged, so post-quantum envelopes differ per run and only their structure is stable.

**[SDK] A version 0.2 manifest may not use the v0.1 envelope.** The version gate now binds in both directions: a manifest declaring `version: "0.2"` while carrying a detached `signature` block is rejected rather than verified under v0.1 rules. `signature` is not a v0.2 field at all, since the COSE structure is the signature, so such a document claims the new version while using the envelope with the unauthenticated algorithm identifier and the canonicalize-before-verify step that ADR-0011 moved away from. Accepting it would have made the gate advisory and left the phase 5 deprecation with nothing to enforce. Version 0.1 manifests are unaffected.

**[SDK] `manifest sign` and `manifest verify` handle version 0.2.** `sign` selects the envelope from the manifest's `version` field, so a `0.2` manifest is written as a COSE envelope in binary CBOR and a `0.1` manifest is unchanged; there is no flag, consistent with ADR-0011. `verify` detects the envelope from the CBOR tag rather than the file extension, since guessing a format by filename is the ambiguity the media-type rules exist to remove. Signing a `0.2` manifest requires `--output`, because binary CBOR down stdout would be corrupted by the terminal. New reference page: [COSE envelope](docs/api-reference/cose.md).

**[SDK] `POST /verify/cose` accepts a version 0.2 COSE manifest as raw CBOR.** The body is the `COSE_Sign1`/`COSE_Sign` object itself under `Content-Type: application/agent-manifest+cose` — the registered media type is the gate, rather than base64 inside a JSON wrapper, since the object is self-contained and the type exists to identify it. Three properties are deliberate: **only the exact media type is accepted** (a vendor-tree alias, `application/cbor`, and an absent type are all refused — the server never sniffs the body); **no key material crosses the wire**, so trust comes from a `cose_context` configured server-side when the router is built, and an unconfigured endpoint returns `UNVERIFIABLE` rather than `VALID`; and **the body is bounded before it is parsed**, with `Content-Length` checked when present and the stream capped regardless, because a declared length is attacker-controlled. A malformed or unverifiable envelope is a verdict (200 with a non-`VALID` result), not a transport error, and parser detail is never reflected back, so the endpoint cannot be used as an oracle for the decoder. Results carry `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`. Authentication, authorization and rate limiting remain deployment concerns (spec 5.1: mTLS with the agent's SPIFFE SVID).

**[SPEC] `version` MUST be `"0.2"`, resolving a contradiction inside the specification.** Section 2.4's compatibility matrix already planned for `0.2` manifests while the section 3 field table still required `"0.1"`, so a producer following the specification could never emit a manifest the COSE envelope governs. The field table now says `"0.2"`; `"0.1"` continues to identify a v0.1 manifest, which stays verifiable. The `@context` change had described v0.2 as differing from v0.1 "in the `@context` value alone" — accurate for that change in isolation, but v0.2 as a release also carries the COSE envelope, and [ADR-0012](docs/adr/0012-context-uri-moved-to-controlled-domain.md) is amended with a note saying so.

**[SDK] `0.2` is now a supported manifest version.** `AM-VEC-007`, which used `0.2` as its stand-in for a version from the future, now uses `0.3`. Its expected result is unchanged.

**[SPEC][SDK] BREAKING: the `@context` URI moves to `https://manifest.agentrust-io.com/v0.2/context.json`**, and the specification is republished as v0.2 ([`spec/agent-manifest-spec-v0.2.md`](spec/agent-manifest-spec-v0.2.md), [ADR-0012](docs/adr/0012-context-uri-moved-to-controlled-domain.md)). The v0.1 URI `https://agentmanifest.agentrust.io/v0.1/context.json` named `agentrust.io`, a domain this project has never controlled: registered to a third party behind Domains By Proxy, paid through mid-2027, and it has never resolved. Every manifest issued to date was therefore identified under somebody else's name, which is untenable in an identity specification.

Consumers **cut over rather than dual-accepting**, matching the TRACE v0.2 profile migration (`agentrust-io/trace-spec#107`) that fixed the identical defect. An implementation that kept honouring the v0.1 URI would keep validating manifests named on a domain we do not own. Manifests already issued under v0.1 stay checkable against the v0.1 specification, which remains published.

**Nothing else about the manifest format changed.** No field is added, removed or re-typed; v0.2 differs from v0.1 in the `@context` value alone. The version bump exists to force the cut-over. Serving the context document at the new URL is follow-up work; what changes today is that the domain is ours to serve from.
**No field is added, removed or re-typed** by the `@context` change itself. Serving the context document at the new URL is follow-up work; what changes today is that the domain is ours to serve from. Note that v0.2 as a release is not only this: ADR-0011 assigns the COSE signature envelope to manifest version `0.2` as well, so a v0.2 manifest carries the new `@context` **and** is signed as a COSE object. The two changes share a version number.

## [0.10.0] — 2026-08-01

Expand Down
14 changes: 11 additions & 3 deletions docs/adr/0012-context-uri-moved-to-controlled-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,17 @@ the same: an implementation that continued honouring the v0.1 URL would keep val
manifests named under a domain we do not own, which is precisely the condition being
fixed. A permissive migration would leave the defect in place indefinitely.

**The manifest format itself does not change.** No field is added, removed or
re-typed. v0.2 differs from v0.1 in the `@context` value alone. The version bump exists
to force the cut-over, not to signal a schema change.
**The manifest schema itself does not change.** No field is added, removed or
re-typed. The version bump exists to force the cut-over, not to signal a schema change.

*Amended 2026-08-05:* as written, this section said v0.2 "differs from v0.1 in the
`@context` value alone". That was true of this change in isolation and is not true of
v0.2 as a release. ADR-0011 had already assigned the COSE envelope to manifest version
`0.2`, so the two changes share a version number: v0.2 carries the new `@context` **and**
the COSE signature envelope. Leaving the claim unqualified produced a contradiction
inside the specification - section 2.4 planned for `0.2` manifests while the section 3
field table still required `"0.1"` - which is corrected in the same change as this note.
The schema statement stands; the "value alone" statement does not.

## Consequences

Expand Down
Loading
Loading