feat(cose): implement the v0.2 COSE envelope behind the version gate (#243 phase 2) - #274
Open
zohebk8s wants to merge 1 commit into
Open
feat(cose): implement the v0.2 COSE envelope behind the version gate (#243 phase 2)#274zohebk8s wants to merge 1 commit into
zohebk8s wants to merge 1 commit into
Conversation
Phase 2 of the ADR-0011 migration, tracked in agentrust-io#243. COSE signing and verification land alongside the existing path, selected by the manifest version field rather than by a flag. A version 0.1 manifest verifies exactly as it does today and no existing record is reinterpreted. WHAT THE ENVELOPE BUYS The signed bytes now travel with the signature, so a verifier never re-serialises a manifest to check one. RFC 8785 stays the producer side determinism rule and the basis of the hash bound into hardware, but it is no longer an input to verification. The algorithm identifier moved into the protected header where the signature covers it, so the downgrade fixed in 0.6.0 by an explicit cross check cannot be expressed at all. Receipts, the TEE attestation report and HITL approvals attach in the unprotected header, which retires the signed_fields coverage table, the hitl_record.approvals normalisation 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. PHASE 2 OPEN ITEMS, CLOSED The library question is settled in ADR-0013: cbor2 for serialisation, with the COSE structures built in this repository. Neither pycose nor cwt ships the RFC 9964 code points, and both widen the dependency closure to do less. The review ADR-0011 asked for is recorded there with evidence rather than impressions, including the finding that cbor2 6.x is a Rust extension with no pure Python fallback, and that the pin resolves to 5.9.0 on manylinux_2_17. The suite runs against both ends of the range. The unprotected map question had only one answer: a three element array is not a COSE_Sign1, so the map is always emitted and is now pinned byte for byte in AM-VEC-COSE-001. TWO SPECIFICATION CORRECTIONS The specification contradicted itself. Section 2.4 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". RFC 9864 (Standards Track, October 2025) deprecated the polymorphic EdDSA identifier -8 and registered fully specified ones. Issue agentrust-io#243 recorded the code points as settled before that was accounted for. ML-DSA-65 is confirmed correct against IANA at -49; Ed25519 moves to -19, recorded in ADR-0014. Verifiers keep accepting -8 indefinitely, because manifests are audit records with regulated retention and a signature cannot be reissued under a new identifier without resigning. SECURITY FIXES FOUND WHILE IMPLEMENTING Three defects were introduced by this work and fixed before it left the branch. Reusing the shared pipeline skipped the key to issuer authorisation check, so any trusted key could sign for any issuer, a regression against what the v0.1 engine enforces. A payload carrying duplicate member names was accepted, which matters because parsers resolve them differently and two verifiers could read a different issuer or expires_at out of identical signed bytes. A deeply nested payload raised RecursionError out of verify_manifest instead of returning a verdict. The version gate also bound in only one direction. A manifest declaring version 0.2 while carrying a v0.1 detached signature block verified VALID, which would have made the gate advisory and left the phase 5 deprecation with nothing to enforce. It is now rejected. PREREQUISITE INCLUDED ML-DSA-65 now comes from cryptography, which implements it as of 47.0.0 and is already a required dependency. The pq extra previously 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 belongs to an unrelated project on PyPI, and a successful import was treated as proof of liboqs, which turned every ML-DSA call into an AttributeError and made the SDK claim a capability it did not have. The capability check now identifies liboqs by its API. Deployments already carrying the bindings keep working, since the backend is chosen by the key material. Thirteen post quantum tests that skipped on every machine without liboqs now execute against real FIPS 204 signatures. This is strictly a prerequisite rather than phase 2 scope, and is included because _cose.py cannot sign or verify the post quantum profile without it. CARRIED CHANGE The verify_bytes methods on Ed25519Verifier, MlDsa65Verifier and HybridVerifier are carried in this commit rather than left out. They were already in the working tree for separate TRACE work, and the ML-DSA backend change rewrites the body of MlDsa65Verifier.verify_bytes, so the two cannot be separated cleanly. They are additive and used by nothing else here. VERIFICATION 868 tests pass with 6 skipped, all six being hardware gates. The same suite passes on both ends of the cbor2 range. mypy strict and bandit are clean and _cose.py is at 99 percent, the single uncovered line being a defensive RecursionError branch that cbor2's decoder does not reach. The COSE_Sign1 and COSE_Sign structures were verified by pycose, an independent implementation with no relationship to this project, which parses them, verifies the signatures and rejects both a flipped signature byte and a modified payload. That check runs against -8 fixtures because no COSE library implements RFC 9864 or RFC 9964 yet, so the identifiers themselves have no third party opinion available today. The cost is recorded in ADR-0014 rather than glossed. Refs agentrust-io#243 Signed-off-by: Mohammed Zoheb Shaik <zoheb.shaik7@gmail.com>
Contributor
|
🟡 Contributor Check: MEDIUM
Automated check by AgenTrust Contributor Check. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements phase 2 of the ADR-0011 migration, tracked in #243. COSE signing and verification land alongside the existing path and are selected by the manifest
versionfield, never by a flag. A version 0.1 manifest verifies exactly as it does today and no existing record is reinterpreted.What the envelope buys
signature.algorithm, outside the signaturealgin the protected header, covered by itsha256of the payload bytesCOSE_Sign, shared payload by structureRFC 8785 has not gone away. It remains the producer side determinism rule and the basis of the hash bound into hardware. What changed is that a verifier no longer has to reproduce it.
Three v0.1 mechanisms are deleted rather than ported, because each existed only to work around JSON having no home for data that attaches after signing: the
signed_fieldscoverage table, thehitl_record.approvalsnormalisation rule, and thetransparency_log_entryordering rule.Phase 2 open items
AM-VEC-COSE-001,unprotected_hex: "a0"The library review is recorded with evidence rather than impressions. Neither
pycosenorcwtships the RFC 9964 code points, and both widen the dependency closure to do less. Two findings came out of it.cbor26.x is a Rust extension with no pure Python fallback, and under the>=5.6,<7pinmanylinux_2_17resolves to 5.9.0 while other platforms get 6.1.4. The two majors also decode tag contents differently, so the suite runs against both ends of the range.Two specification corrections
The specification contradicted itself. Section 2.4 planned for
0.2manifests 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".RFC 9864 deprecated
EdDSA-8. Standards Track, October 2025, registering fully specified identifiers in its place. Issue #243 recorded the code points as settled before that was accounted for. ML-DSA-65 is confirmed correct against IANA at-49. Ed25519 moves to-19, recorded in ADR-0014. Verifiers keep accepting-8indefinitely, because manifests are audit records with regulated retention and a signature cannot be reissued under a new identifier without resigning.Security fixes found while implementing
Three defects were introduced by this work and fixed before it left the branch.
issuerorexpires_atout of identical signed bytes and both consider the signature valid.NaNandInfinitywere the same class. Both rejected at parse time, which is a parse level check rather than a re canonicalisation.RecursionErrorout ofverify_manifestinstead of returning a verdict.The version gate also bound in only one direction. A manifest declaring
version: "0.2"while carrying a v0.1 detached signature block verifiedVALID, which would have made the gate advisory and left the phase 5 deprecation with nothing to enforce. Now rejected.Prerequisite included
ML-DSA-65 now comes from
cryptography, which implements it as of 47.0.0 and is already a required dependency.The
pqextra previously requiredpyoqs, which is not published on PyPI, so the post quantum profile could not be installed by following the documented instruction. The module name the SDK imports also belongs to an unrelated project on PyPI, and a successful import was treated as proof of liboqs, which turned every ML-DSA call into anAttributeErrorand made the SDK claim a capability it did not have. The capability check now identifies liboqs by its API. Deployments already carrying the bindings keep working, because the backend is chosen by the key material.Thirteen post quantum tests that skipped on every machine without liboqs now execute against real FIPS 204 signatures. This is a prerequisite rather than phase 2 scope, included because
_cose.pycannot sign or verify the post quantum profile without it. Happy to split it out if reviewers prefer.HTTP and CLI
POST /verify/cosetakes the COSE object as raw CBOR underapplication/agent-manifest+cose.application/cbor, and an absent type are refused with 415, and the body is never sniffed.UNVERIFIABLE, neverVALID.Content-Lengthchecked when present and the stream capped regardless.manifest signselects the envelope from the manifest version.manifest verifydetects it from the CBOR tag rather than the file extension.Verification
Run against a fresh clone of this branch, not a working tree.
cbor2floor (5.6.0)_cose.pycoverageAM-VEC-COSE-001regenerated from sourcepycose)VALIDThe single uncovered line in
_cose.pyis a defensiveRecursionErrorbranch that the decoder does not reach.The
COSE_Sign1andCOSE_Signstructures were verified bypycose, an independent implementation with no relationship to this project, which parses them, verifies the signatures and rejects both a flipped signature byte and a modified payload. That check runs against-8fixtures because no COSE library implements RFC 9864 or RFC 9964 yet, so neither of this profile's algorithm identifiers has a third party opinion available today. The cost is recorded in ADR-0014 rather than glossed.One carried change
The
verify_bytesmethods onEd25519Verifier,MlDsa65VerifierandHybridVerifierare carried here rather than left out. They were already in the working tree for separate TRACE work, and the ML-DSA backend change rewrites the body ofMlDsa65Verifier.verify_bytes, so the two could not be separated cleanly. They are additive and nothing else in this PR uses them.Not included
Manifest.versionstill defaults to"0.1"in the SDK model. With the version gate now binding in both directions, flipping it would push every default constructed manifest onto the COSE envelope, so it belongs with the phase 5 deprecation rather than here. A v0.2 example file belongs with the phase 3 vectors.Refs #243