From dcaef161584835b703cbc545ba27284f50b2f92d Mon Sep 17 00:00:00 2001 From: Joe Feser Date: Tue, 8 Sep 2026 01:58:01 -0500 Subject: [PATCH 1/2] Define packet assembly attestation draft --- README.md | 12 + package.json | 3 +- rfcs/0010-packet-assembly-attestation.md | 208 ++++++++ schemas/README.md | 16 + .../v0.1-draft/README.md | 44 ++ .../fixtures/construction/profile.json | 7 + .../fixtures/construction/runtime.json | 6 + .../fixtures/construction/tool.json | 6 + .../invalid/changed-claim.invalid.json | 94 ++++ ...onstruction-artifact-mismatch.invalid.json | 94 ++++ .../packet-digest-mismatch.invalid.json | 94 ++++ .../self-asserted-identity.invalid.json | 94 ++++ .../invalid/signature-mismatch.invalid.json | 94 ++++ .../source-revision-substitution.invalid.json | 94 ++++ .../v0.1-draft/fixtures/manifest.json | 53 ++ .../packet-assembly-attestation.valid.json | 94 ++++ .../fixtures/verification-context.json | 46 ++ .../packet-assembly-attestation.schema.json | 179 +++++++ .../validate-packet-assembly-attestation.mjs | 499 ++++++++++++++++++ ...idate-packet-assembly-attestation.test.mjs | 79 +++ 20 files changed, 1815 insertions(+), 1 deletion(-) create mode 100644 rfcs/0010-packet-assembly-attestation.md create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/profile.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/runtime.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/tool.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/changed-claim.invalid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/construction-artifact-mismatch.invalid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/packet-digest-mismatch.invalid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/self-asserted-identity.invalid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/signature-mismatch.invalid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/source-revision-substitution.invalid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.valid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/verification-context.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json create mode 100644 scripts/validate-packet-assembly-attestation.mjs create mode 100644 scripts/validate-packet-assembly-attestation.test.mjs diff --git a/README.md b/README.md index bf97c88..403c308 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,7 @@ bugs, broken examples, or unclear conformance requirements. | [RFC-0007](rfcs/0007-audit-trail-and-evidence-set.md) | Audit Trail and Evidence Set | Draft | | [RFC-0008](rfcs/0008-transport-boundary.md) | Transport Boundary | Draft | | [RFC-0009](rfcs/0009-conformance-and-profiles.md) | Conformance and Profiles | Draft | +| [RFC-0010](rfcs/0010-packet-assembly-attestation.md) | Packet Assembly Attestation | Experimental Draft | ## HACP 0.2 Draft Status @@ -391,6 +392,11 @@ executable candidate package: - closed candidate schemas and computed vectors in [schemas/v0.3-candidate/](schemas/v0.3-candidate/). +Packet-construction provenance is explored separately in +[RFC-0010](rfcs/0010-packet-assembly-attestation.md) and the +[experimental packet-assembly-attestation package](schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md). +It does not modify or extend the published v0.3-candidate package. + Implementation-specific trial artefacts are not required to review this public draft. The v0.3 candidate package includes bounded public evidence, conformance fixtures, and an independent-implementation report. Future work may add other @@ -531,6 +537,12 @@ v0.2 artefacts: - [fixtures/happy-path/](fixtures/happy-path/) - [fixtures/risk-cases/](fixtures/risk-cases/) +experimental future record families: + +- [packet-assembly-attestation v0.1 draft](schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md): + authenticated construction-claim shape, trust-bound validation harness, and + positive/negative fixtures. This is not part of HACP v0.3-candidate. + ## Canonical Vocabulary Sources The draft prose explains the contracts, but machine-readable vocabulary should diff --git a/package.json b/package.json index 1618908..eeecfd8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "hacp:doctor": "node scripts/hacp-doctor.mjs", "hacp:cli-bridge-examples": "node scripts/validate-cli-bridge-examples.mjs", "hacp:v03-candidate": "node scripts/compute-vectors.mjs --check && node --test scripts/compute-vectors.test.mjs scripts/validate-external-bundle.test.mjs", - "hacp:v03-external": "node scripts/validate-external-bundle.mjs" + "hacp:v03-external": "node scripts/validate-external-bundle.mjs", + "hacp:packet-assembly-attestation": "node scripts/validate-packet-assembly-attestation.mjs --check && node --test scripts/validate-packet-assembly-attestation.test.mjs" }, "dependencies": { "ajv": "^8.17.1", diff --git a/rfcs/0010-packet-assembly-attestation.md b/rfcs/0010-packet-assembly-attestation.md new file mode 100644 index 0000000..c2b2d8d --- /dev/null +++ b/rfcs/0010-packet-assembly-attestation.md @@ -0,0 +1,208 @@ +# RFC-0010: Packet Assembly Attestation + +Status: Experimental Draft + +Copyright (c) 2026 Joe Feser. Licensed under CC BY 4.0. + +This draft uses normative keywords to express design intent. It is a separate, +experimental record family. It does not change, republish, or version-bump the +HACP v0.3-candidate schemas. + +## Abstract + +A packet digest preserves integrity after construction, but it does not answer +who assembled the packet or how that identity was authenticated. A Packet +Assembly Attestation binds an authenticated builder principal to a specific +construction claim: the exact packet digest, exact source revisions and +digests, construction profile, tool and runtime identities, construction time, +declared transformations, and limitations. + +The record is an attestation. It is not proof that the packet was assembled +correctly, that its sources were authentic, that a human approved it, that a +runtime admitted it, or that an executor performed the work. + +## Motivation + +HACP v0.3-candidate task packets have domain-separated content digests and +source references. Those fields support integrity and interworking checks after +construction. They do not authenticate the builder. This is a real but +non-breaking limitation of the candidate package and does not invalidate its +current integrity, conformance, or WITS interworking evidence. + +RFC-0001's base-draft `created_by` field is self-asserted artifact authorship. +It remains useful display metadata, but it is not authenticated construction +provenance. A bare `createdBy` or equivalent string MUST NOT satisfy this RFC. + +## Roles + +Implementations MUST keep these roles explicit and MUST NOT infer one from +another: + +| Role | Responsibility | +| --- | --- | +| Builder or assembler | Constructs the packet from declared inputs. | +| Authorized human approver | Decides whether a bounded continuation may proceed. | +| Runtime issuer or admitter | Admits a continuation under an approved runtime profile. | +| Executor | Performs or attempts the authorized work. | + +One principal MAY occupy more than one role, but each role still requires its +own applicable evidence. A builder signature is not human approval. Human +approval is not runtime admission. Runtime admission is not execution proof. + +## Record + +The experimental record kind is +`hacp.experimental.packet_assembly_attestation`. The draft schema version is +`hacp-packet-assembly-attestation-0.1-draft`. + +The record MUST contain: + +- a unique attestation identifier; +- an authenticated builder principal claim, authentication method, + verification-profile reference, and key identifier; +- the exact packet identifier, schema version, and digest; +- one or more exact source references, revisions, representations, and + digests; +- construction profile identity, version, reference, and digest; +- construction tool identity, version, and artifact digest; +- construction runtime identity, version, and artifact digest; +- construction time; +- declared transformations and their input/output references; +- declared limitations; +- a domain-separated claim digest; +- a cryptographic binding; and +- the required non-authority boundary statement. + +The draft machine-readable contract is +[`schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json`](../schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json). + +## Canonical Claim and Cryptographic Binding + +The claim preimage is the complete top-level attestation record with +`claimDigest` and `cryptographicBinding` omitted. Producers MUST serialize the +following envelope with RFC 8785 JSON Canonicalization Scheme: + +```json +{"domain":"io.hacp.packet-assembly-attestation-claim.v0.1-draft","record":{}} +``` + +`record` is the claim preimage described above. Producers MUST hash the UTF-8 +envelope bytes with SHA-256 and place the lowercase hexadecimal result in +`claimDigest.value`. Producers MUST sign the same UTF-8 envelope bytes using +the declared cryptographic scheme. The initial draft fixture profile supports +Ed25519 only. + +Digest equality proves integrity after construction. It does not establish +builder identity, source authenticity, correct interpretation, approval, or +execution authority. The signature shows that the holder of the corresponding +private key signed the construction claim. Builder authentication exists only +when an independently selected verification profile binds that key to the +declared principal and authentication method. + +## Trust-Anchor Selection + +The receiving owner system or an independently authorized verifier MUST select +the accepted verification profile and trust anchors. The attestation MUST NOT +select its own trust merely by naming a profile or key. + +A verification profile claiming support for this record MUST define: + +- admitted builder principals and authentication methods; +- key or certificate discovery and rotation; +- trust-anchor ownership and current status; +- revocation and expiry processing; +- algorithm policy; +- source revision and digest resolution policy; +- failure and audit behavior; and +- redaction rules for runtime or environment identity. + +An unknown, expired, revoked, self-asserted, or otherwise unverifiable builder +MUST fail closed as `UNTRUSTED_BUILDER_PRINCIPAL`. A valid signature from an +untrusted key does not authenticate the builder. + +## Verification + +A verifier claiming this experimental profile MUST, in order: + +1. validate the closed record shape; +2. recompute and compare the domain-separated claim digest; +3. resolve the builder principal, authentication method, and key through the + independently selected verification profile; +4. verify the cryptographic binding over the canonical claim envelope; +5. compare the exact packet identifier, schema version, and digest with the + packet under review; and +6. resolve each source revision and digest under the profile's source policy. + +The experimental fixture harness uses these stable diagnostics: + +| Code | Meaning | +| --- | --- | +| `SCHEMA_VALIDATION_FAILED` | The record is not the closed draft shape. | +| `CLAIM_DIGEST_MISMATCH` | The canonical construction claim changed. | +| `UNTRUSTED_BUILDER_PRINCIPAL` | No accepted profile binds the declared principal, method, and key. | +| `SIGNATURE_INVALID` | The cryptographic binding does not verify. | +| `PACKET_DIGEST_MISMATCH` | The attestation does not bind the exact packet under review. | +| `SOURCE_REVISION_MISMATCH` | A declared source revision differs from trusted resolution context. | +| `SOURCE_DIGEST_MISMATCH` | Resolved source bytes do not match the declared digest. | +| `CONSTRUCTION_ARTIFACT_MISMATCH` | A declared profile, tool, or runtime identity does not match resolved artifact bytes. | + +Failure MUST NOT fall back to an unauthenticated `createdBy` value. It MUST NOT +activate approval, admission, execution, retry, or packet-reconstruction +authority. + +## Correctness Boundary + +Authenticated construction provenance remains an attestation, not proof that +the packet was correctly assembled. A conforming verifier can establish that a +trusted profile associates a principal with the signing key and that the exact +claim has not changed. It cannot establish, from this record alone, that: + +- the builder selected every relevant source; +- a source was truthful or authoritative; +- the builder interpreted a source correctly; +- a declared transformation was semantically correct; +- the declared construction time came from a trusted timestamping authority; +- omitted context was irrelevant; +- the packet should be approved; +- a runtime should admit the packet; or +- any execution or external effect occurred. + +Those judgments require separate evidence and, where consequential, an +authorized human decision. + +## HACP v0.3-candidate Disposition + +The absence of this record is a non-breaking HACP v0.3-candidate limitation. It +becomes a correctness defect only when an implementation claims that the +packet builder was authenticated or that correct packet construction was +proven without the required evidence. + +This RFC and its experimental schema do not modify the v0.3-candidate task +packet or any of its digest domains, fixtures, manifests, or publication +evidence. + +## Conformance Evidence + +The experimental package includes a positive signed fixture and negative +fixtures for source-revision substitution, packet-digest mismatch, +self-asserted identity under an untrusted key, signature corruption, and +post-signature claim mutation. The included keys and identities are synthetic, +public test material and MUST NOT be trusted outside the fixture harness. + +Run: + +```bash +npm run hacp:packet-assembly-attestation +``` + +Passing the harness proves only that this implementation validates the bounded +fixture contract. It does not authenticate any production builder or promote +the record beyond experimental draft status. + +## Open Questions + +- Which authentication profiles should be standardized first: workload + identity, organizational signing keys, or another mechanism? +- How should source resolvers express repository-specific revision semantics? +- Should future profiles support threshold or transparency-log bindings? +- Which construction details require redaction while remaining auditable? diff --git a/schemas/README.md b/schemas/README.md index 77f7522..9837083 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -30,6 +30,8 @@ Need to choose a concrete stop reason? See - `https://hacp.example/schemas/v0.2-draft/` - `https://hacp.example/schemas/v0.3-candidate/` for executable candidate interworking contracts; exact digest domains remain owner review gates. + - `https://hacp.io/schemas/experimental/packet-assembly-attestation/v0.1-draft/` + for the separate experimental construction-provenance record family. Versioning for future phases should preserve backward-readable draft history rather than rewriting old identifiers. @@ -101,6 +103,20 @@ Validate the generated candidate corpus with: npm run hacp:v03-candidate ``` +### Experimental Packet Assembly Attestation + +- [record, fixtures, and explicit non-claims](experimental/packet-assembly-attestation/v0.1-draft/README.md) + +Validate the separate experimental package with: + +```bash +npm run hacp:packet-assembly-attestation +``` + +This record family does not change or version-bump HACP v0.3-candidate. Its +authenticated construction claim remains an attestation, not proof of correct +assembly, approval, runtime admission, or execution. + ## Evidence Field Compatibility The base RFCs require simple `evidence` arrays so the minimum contract stays diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md b/schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md new file mode 100644 index 0000000..53f9382 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md @@ -0,0 +1,44 @@ +# Packet Assembly Attestation v0.1 Draft + +Status: experimental draft record family. This directory is not part of the +HACP v0.3-candidate package and does not change that package's schemas, digest +domains, fixtures, or publication status. + +The record authenticates a bounded construction claim when an independently +selected verification profile binds the declared builder principal and key. It +keeps the builder or assembler, authorized human approver, runtime issuer or +admitter, and executor as distinct roles. + +It does not prove correct construction. It does not establish source +authenticity, approval, runtime admission, execution, completion, or external +effects. A bare `createdBy` string is insufficient. + +## Validate + +From the repository root: + +```bash +npm ci +npm run hacp:packet-assembly-attestation +``` + +The validator checks the closed schema, RFC 8785 claim digest, Ed25519 fixture +signature, independently supplied synthetic trust binding, exact packet +binding, and source revision/digest resolution. All fixture identities and keys +are public test material with no production authority. + +## Files + +- `packet-assembly-attestation.schema.json`: closed experimental record shape. +- `fixtures/manifest.json`: exact expected valid and invalid outcomes. +- `fixtures/verification-context.json`: synthetic, independently supplied + builder trust and source-resolution context for the harness. +- `fixtures/construction/`: exact synthetic profile, tool, and runtime identity + descriptors bound by the valid attestation. +- `fixtures/valid/`: one authenticated construction claim. +- `fixtures/invalid/`: source substitution, packet mismatch, self-asserted + identity, signature corruption, changed-claim, and construction-artifact + mismatch cases. + +See [RFC-0010](../../../../rfcs/0010-packet-assembly-attestation.md) for trust, +verification, role-separation, and non-claim semantics. diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/profile.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/profile.json new file mode 100644 index 0000000..86f89f4 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/profile.json @@ -0,0 +1,7 @@ +{ + "kind": "packet-assembly-construction-profile", + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "transformationPolicy": "select-declared-source-and-structure-bounded-fields", + "fixtureOnly": true +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/runtime.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/runtime.json new file mode 100644 index 0000000..1a19b6e --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/runtime.json @@ -0,0 +1,6 @@ +{ + "kind": "packet-assembly-runtime-identity", + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "fixtureOnly": true +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/tool.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/tool.json new file mode 100644 index 0000000..6de6618 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/tool.json @@ -0,0 +1,6 @@ +{ + "kind": "packet-assembly-tool-identity", + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "fixtureOnly": true +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/changed-claim.invalid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/changed-claim.invalid.json new file mode 100644 index 0000000..0467ae2 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/changed-claim.invalid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_001", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "23ff4fdd7cb924f63f39f76ef3f3289bfa4ad632c3132e52948c1ca8afc7f67f" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "ddce6e85dacd107ab1a1f7e7c24825a8b88a62cefe4286faa9bea0c7123ed363" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "95249a6bad0f78812bda0062bf4b77ba2ce14efb4c9525275a250b76e0dfa0e4" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Undeclared post-signature construction change.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "e2237e3e0ff24da30e23c541d66228f08f50b249426bfa0e91b0d637989f0989" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "_xxkuogDdaEFAHtv6eyVCX8i2TVgqUNKd2MD3Vn3y_AIrm2aVF9bvVsl5cyThavVPLOT1DkM-NFNZI1C5HYvDg" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/construction-artifact-mismatch.invalid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/construction-artifact-mismatch.invalid.json new file mode 100644 index 0000000..6b5a105 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/construction-artifact-mismatch.invalid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_001", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "23ff4fdd7cb924f63f39f76ef3f3289bfa4ad632c3132e52948c1ca8afc7f67f" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "95249a6bad0f78812bda0062bf4b77ba2ce14efb4c9525275a250b76e0dfa0e4" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "d9bf8d821f4e59bde53b48056ff09aeb23173c084a4e0faf388821758f2d5095" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "s3-PUW9i2ZzfTBZWvzEAFZfDdtCsRYRaJtfzYhu3hpMP8WS5h7rAw2WPD2updfu8QjhOVeKPtfAOZw_M0-O8Ag" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/packet-digest-mismatch.invalid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/packet-digest-mismatch.invalid.json new file mode 100644 index 0000000..0acf838 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/packet-digest-mismatch.invalid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_001", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "23ff4fdd7cb924f63f39f76ef3f3289bfa4ad632c3132e52948c1ca8afc7f67f" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "ddce6e85dacd107ab1a1f7e7c24825a8b88a62cefe4286faa9bea0c7123ed363" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "95249a6bad0f78812bda0062bf4b77ba2ce14efb4c9525275a250b76e0dfa0e4" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "f58bf658120a100d95f50037f69f1f2586d5df5846bffca2d59fc1bbbd1f9fcf" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "KOYoMZjPYDGb1BcPmIp-g11RLHF3WFRQKTPOmvI4KoLwfZTIgJX7Tj5vu-DUpIn4skmfZuAUjAMqvqqGecbjAA" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/self-asserted-identity.invalid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/self-asserted-identity.invalid.json new file mode 100644 index 0000000..a39afc2 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/self-asserted-identity.invalid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_001", + "builder": { + "principalId": "principal://example.invalid/self-asserted-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/self-asserted-builder/untrusted-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "23ff4fdd7cb924f63f39f76ef3f3289bfa4ad632c3132e52948c1ca8afc7f67f" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "ddce6e85dacd107ab1a1f7e7c24825a8b88a62cefe4286faa9bea0c7123ed363" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "95249a6bad0f78812bda0062bf4b77ba2ce14efb4c9525275a250b76e0dfa0e4" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "e0e210865cffe688e29e546bd6ea79264ac33a75e90821bc773a60316280950c" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/self-asserted-builder/untrusted-1", + "signature": "Mj5WdPhdUvz9dJUXa-a4faBK8TbCkJqagI1Iv7VfkOCG73i1JmXsYV7-ECKlAmgjv-TW_4w6-JJiynKFbzdIAg" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/signature-mismatch.invalid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/signature-mismatch.invalid.json new file mode 100644 index 0000000..eac0271 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/signature-mismatch.invalid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_001", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "23ff4fdd7cb924f63f39f76ef3f3289bfa4ad632c3132e52948c1ca8afc7f67f" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "ddce6e85dacd107ab1a1f7e7c24825a8b88a62cefe4286faa9bea0c7123ed363" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "95249a6bad0f78812bda0062bf4b77ba2ce14efb4c9525275a250b76e0dfa0e4" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "e2237e3e0ff24da30e23c541d66228f08f50b249426bfa0e91b0d637989f0989" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "AxxkuogDdaEFAHtv6eyVCX8i2TVgqUNKd2MD3Vn3y_AIrm2aVF9bvVsl5cyThavVPLOT1DkM-NFNZI1C5HYvDg" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/source-revision-substitution.invalid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/source-revision-substitution.invalid.json new file mode 100644 index 0000000..ef1d3e3 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/invalid/source-revision-substitution.invalid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_001", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:0000000000000000000000000000000000000000", + "digest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "23ff4fdd7cb924f63f39f76ef3f3289bfa4ad632c3132e52948c1ca8afc7f67f" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "ddce6e85dacd107ab1a1f7e7c24825a8b88a62cefe4286faa9bea0c7123ed363" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "95249a6bad0f78812bda0062bf4b77ba2ce14efb4c9525275a250b76e0dfa0e4" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "3e4fdabb882bf59caf98e6311971cd8220d4e97fb5c6e3fb077f81900e8bd292" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "T8VLurOewtStmMFY7ZeR8CrYq_Gn0WDQpk4wgk9PNKtm-NmhRWk0AXA49lI_98CKJ__e-4JHrrOswT2D_VZFAw" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json new file mode 100644 index 0000000..f965abf --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json @@ -0,0 +1,53 @@ +{ + "schema": "hacp.packet_assembly_attestation.conformance_manifest.v1", + "status": "experimental_draft", + "sourceIssue": "https://github.com/joefeser/hacp/issues/69", + "hacpEvidenceBase": "671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "candidatePackageModified": false, + "verificationContext": "verification-context.json", + "expectedValid": [ + { + "path": "valid/packet-assembly-attestation.valid.json", + "expectedCodes": [] + } + ], + "expectedInvalid": [ + { + "path": "invalid/source-revision-substitution.invalid.json", + "expectedCodes": [ + "SOURCE_REVISION_MISMATCH" + ] + }, + { + "path": "invalid/packet-digest-mismatch.invalid.json", + "expectedCodes": [ + "PACKET_DIGEST_MISMATCH" + ] + }, + { + "path": "invalid/self-asserted-identity.invalid.json", + "expectedCodes": [ + "UNTRUSTED_BUILDER_PRINCIPAL" + ] + }, + { + "path": "invalid/signature-mismatch.invalid.json", + "expectedCodes": [ + "SIGNATURE_INVALID" + ] + }, + { + "path": "invalid/changed-claim.invalid.json", + "expectedCodes": [ + "CLAIM_DIGEST_MISMATCH" + ] + }, + { + "path": "invalid/construction-artifact-mismatch.invalid.json", + "expectedCodes": [ + "CONSTRUCTION_ARTIFACT_MISMATCH" + ] + } + ], + "boundaryStatement": "Passing these fixtures proves bounded harness behavior only. It does not authenticate a production builder or prove correct packet construction." +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.valid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.valid.json new file mode 100644 index 0000000..233d543 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.valid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_001", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "23ff4fdd7cb924f63f39f76ef3f3289bfa4ad632c3132e52948c1ca8afc7f67f" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "ddce6e85dacd107ab1a1f7e7c24825a8b88a62cefe4286faa9bea0c7123ed363" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "raw-bytes", + "value": "95249a6bad0f78812bda0062bf4b77ba2ce14efb4c9525275a250b76e0dfa0e4" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "e2237e3e0ff24da30e23c541d66228f08f50b249426bfa0e91b0d637989f0989" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "_xxkuogDdaEFAHtv6eyVCX8i2TVgqUNKd2MD3Vn3y_AIrm2aVF9bvVsl5cyThavVPLOT1DkM-NFNZI1C5HYvDg" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/verification-context.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/verification-context.json new file mode 100644 index 0000000..d8043ea --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/verification-context.json @@ -0,0 +1,46 @@ +{ + "schema": "hacp.packet_assembly_attestation.fixture_verification_context.v1", + "fixtureOnly": true, + "profileRef": "profile://example.invalid/packet-builder-test/v0.1", + "trustedBuilders": [ + { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "publicKeyJwk": { + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "kty": "OKP" + }, + "status": "active" + } + ], + "subjectPacket": { + "path": "schemas/v0.3-candidate/fixtures/valid/success/task-packet.valid.json" + }, + "sourceResolutions": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "path": "schemas/v0.3-candidate/task-packet.schema.json" + } + ], + "constructionArtifactResolutions": [ + { + "kind": "profile", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "path": "schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/profile.json" + }, + { + "kind": "tool", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "path": "schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/tool.json" + }, + { + "kind": "runtime", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "path": "schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/runtime.json" + } + ], + "boundaryStatement": "Synthetic fixture trust only. This context authenticates no production builder, source, approval, runtime admission, executor, or external effect." +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json new file mode 100644 index 0000000..99ca3d2 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json @@ -0,0 +1,179 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hacp.io/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json", + "title": "HACP Experimental Packet Assembly Attestation v0.1 Draft", + "type": "object", + "additionalProperties": false, + "required": [ + "recordKind", + "schemaVersion", + "attestationId", + "builder", + "packetBinding", + "sourceBindings", + "construction", + "constructedAt", + "declaredTransformations", + "limitations", + "claimDigest", + "cryptographicBinding", + "boundaryStatement" + ], + "properties": { + "recordKind": { "const": "hacp.experimental.packet_assembly_attestation" }, + "schemaVersion": { "const": "hacp-packet-assembly-attestation-0.1-draft" }, + "attestationId": { "$ref": "#/$defs/identifier" }, + "builder": { + "type": "object", + "additionalProperties": false, + "required": ["principalId", "authenticationMethod", "verificationProfileRef", "keyId"], + "properties": { + "principalId": { "$ref": "#/$defs/reference" }, + "authenticationMethod": { "$ref": "#/$defs/identifier" }, + "verificationProfileRef": { "$ref": "#/$defs/reference" }, + "keyId": { "$ref": "#/$defs/reference" } + } + }, + "packetBinding": { + "type": "object", + "additionalProperties": false, + "required": ["packetId", "packetSchemaVersion", "packetDigest"], + "properties": { + "packetId": { "$ref": "#/$defs/identifier" }, + "packetSchemaVersion": { "$ref": "#/$defs/identifier" }, + "packetDigest": { "$ref": "#/$defs/recordDigest" } + } + }, + "sourceBindings": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["sourceRef", "revision", "digest"], + "properties": { + "sourceRef": { "$ref": "#/$defs/reference" }, + "revision": { "$ref": "#/$defs/identifier" }, + "digest": { "$ref": "#/$defs/sourceDigest" } + } + } + }, + "construction": { + "type": "object", + "additionalProperties": false, + "required": ["profile", "tool", "runtime"], + "properties": { + "profile": { "$ref": "#/$defs/versionedArtifact" }, + "tool": { "$ref": "#/$defs/versionedArtifact" }, + "runtime": { "$ref": "#/$defs/versionedArtifact" } + } + }, + "constructedAt": { "type": "string", "format": "date-time" }, + "declaredTransformations": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["transformationId", "description", "inputRefs", "outputRefs"], + "properties": { + "transformationId": { "$ref": "#/$defs/identifier" }, + "description": { "$ref": "#/$defs/text" }, + "inputRefs": { "$ref": "#/$defs/stringSet" }, + "outputRefs": { "$ref": "#/$defs/stringSet" } + } + } + }, + "limitations": { "$ref": "#/$defs/stringSet" }, + "claimDigest": { + "allOf": [ + { "$ref": "#/$defs/recordDigest" }, + { + "type": "object", + "properties": { + "digestDomain": { "const": "io.hacp.packet-assembly-attestation-claim.v0.1-draft" } + } + } + ] + }, + "cryptographicBinding": { + "type": "object", + "additionalProperties": false, + "required": ["scheme", "keyId", "signature"], + "properties": { + "scheme": { "const": "ed25519" }, + "keyId": { "$ref": "#/$defs/reference" }, + "signature": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{86}$" + } + } + }, + "boundaryStatement": { + "const": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects." + } + }, + "$defs": { + "identifier": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S(?:.*\\S)?$" + }, + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "pattern": "^\\S(?:.*\\S)?$" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "stringSet": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "sourceDigest": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "representation", "value"], + "properties": { + "algorithm": { "const": "sha256" }, + "representation": { "enum": ["raw-bytes", "git-blob", "json-rfc8785-jcs"] }, + "value": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + } + }, + "recordDigest": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "canonicalization", "digestDomain", "value"], + "properties": { + "algorithm": { "const": "sha256" }, + "canonicalization": { "const": "json-rfc8785-jcs" }, + "digestDomain": { "$ref": "#/$defs/identifier" }, + "value": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + } + }, + "versionedArtifact": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version", "artifactRef", "artifactDigest"], + "properties": { + "id": { "$ref": "#/$defs/identifier" }, + "version": { "$ref": "#/$defs/identifier" }, + "artifactRef": { "$ref": "#/$defs/reference" }, + "artifactDigest": { "$ref": "#/$defs/sourceDigest" } + } + } + } +} diff --git a/scripts/validate-packet-assembly-attestation.mjs b/scripts/validate-packet-assembly-attestation.mjs new file mode 100644 index 0000000..49f8738 --- /dev/null +++ b/scripts/validate-packet-assembly-attestation.mjs @@ -0,0 +1,499 @@ +#!/usr/bin/env node + +import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; +import canonicalize from 'canonicalize'; +import { + createHash, + createPrivateKey, + createPublicKey, + sign, + verify, +} from 'node:crypto'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const packageRoot = path.join( + repoRoot, + 'schemas/experimental/packet-assembly-attestation/v0.1-draft', +); +const fixtureRoot = path.join(packageRoot, 'fixtures'); +const schemaPath = path.join(packageRoot, 'packet-assembly-attestation.schema.json'); +const subjectPacketPath = path.join( + repoRoot, + 'schemas/v0.3-candidate/fixtures/valid/success/task-packet.valid.json', +); +const sourceSchemaPath = path.join(repoRoot, 'schemas/v0.3-candidate/task-packet.schema.json'); + +const CLAIM_DOMAIN = 'io.hacp.packet-assembly-attestation-claim.v0.1-draft'; +const SOURCE_REF = 'repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json'; +const SOURCE_REVISION = 'git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06'; +const EXPECTED_SOURCE_SHA256 = '9b7447bac08afcdb88ca5c275d1f897d6aff8a83f41e068bb79480f185da3dea'; +const EXPECTED_PACKET_DIGEST = 'c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4'; +const TRUST_PROFILE_REF = 'profile://example.invalid/packet-builder-test/v0.1'; +const TRUSTED_PRINCIPAL = 'principal://example.invalid/packet-builder'; +const TRUSTED_KEY_ID = 'key://example.invalid/packet-builder/fixture-ed25519-1'; +const AUTHENTICATION_METHOD = 'profile_bound_ed25519_key'; +const BOUNDARY_STATEMENT = 'This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.'; + +// RFC 8032 test-vector seeds. These are public fixture material, not secrets. +const TRUSTED_SEED = '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60'; +const UNTRUSTED_SEED = '4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb'; + +function stableJson(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} + +async function readJson(file) { + return JSON.parse(await readFile(file, 'utf8')); +} + +function privateKeyFromSeed(seedHex) { + const prefix = Buffer.from('302e020100300506032b657004220420', 'hex'); + return createPrivateKey({ + key: Buffer.concat([prefix, Buffer.from(seedHex, 'hex')]), + format: 'der', + type: 'pkcs8', + }); +} + +function sourceDigest(bytes) { + return { + algorithm: 'sha256', + representation: 'raw-bytes', + value: createHash('sha256').update(bytes).digest('hex'), + }; +} + +function declaredArtifact(descriptor, artifactRef) { + return { + id: descriptor.id, + version: descriptor.version, + artifactRef, + artifactDigest: sourceDigest(Buffer.from(stableJson(descriptor), 'utf8')), + }; +} + +function equalDigest(left, right) { + return left?.algorithm === right?.algorithm + && left?.canonicalization === right?.canonicalization + && left?.digestDomain === right?.digestDomain + && left?.value === right?.value; +} + +function claimRecord(attestation) { + const record = structuredClone(attestation); + delete record.claimDigest; + delete record.cryptographicBinding; + return record; +} + +export function canonicalClaimInput(attestation) { + const serialized = canonicalize({ domain: CLAIM_DOMAIN, record: claimRecord(attestation) }); + if (serialized === undefined) throw new Error('RFC 8785 claim serialization failed.'); + return Buffer.from(serialized, 'utf8'); +} + +export function digestClaim(attestation) { + return { + algorithm: 'sha256', + canonicalization: 'json-rfc8785-jcs', + digestDomain: CLAIM_DOMAIN, + value: createHash('sha256').update(canonicalClaimInput(attestation)).digest('hex'), + }; +} + +function finalizeAttestation(attestation, privateKey, keyId) { + const next = claimRecord(attestation); + next.claimDigest = digestClaim(next); + next.cryptographicBinding = { + scheme: 'ed25519', + keyId, + signature: sign(null, canonicalClaimInput(next), privateKey).toString('base64url'), + }; + return next; +} + +function candidatePacketDigest(packet) { + const record = structuredClone(packet); + const domain = record.digest.digestDomain; + delete record.digest; + const serialized = canonicalize({ domain, record }); + if (serialized === undefined) throw new Error('Candidate packet serialization failed.'); + return { + algorithm: 'sha256', + canonicalization: 'json-rfc8785-jcs', + digestDomain: domain, + value: createHash('sha256').update(serialized, 'utf8').digest('hex'), + }; +} + +async function schemaValidator() { + const schema = await readJson(schemaPath); + const ajv = new Ajv2020({ allErrors: true, strict: true }); + addFormats(ajv); + return ajv.compile(schema); +} + +export async function buildCorpus() { + const trustedPrivateKey = privateKeyFromSeed(TRUSTED_SEED); + const untrustedPrivateKey = privateKeyFromSeed(UNTRUSTED_SEED); + const trustedPublicKey = createPublicKey(trustedPrivateKey).export({ format: 'jwk' }); + const subjectPacket = await readJson(subjectPacketPath); + const sourceBytes = await readFile(sourceSchemaPath); + const resolvedSourceDigest = sourceDigest(sourceBytes); + + if (!equalDigest(subjectPacket.digest, candidatePacketDigest(subjectPacket)) + || subjectPacket.digest.value !== EXPECTED_PACKET_DIGEST) { + throw new Error('Pinned candidate task packet digest is not reproducible.'); + } + if (resolvedSourceDigest.value !== EXPECTED_SOURCE_SHA256) { + throw new Error('Pinned source revision no longer matches the expected source bytes.'); + } + + const constructionDescriptors = { + profile: { + kind: 'packet-assembly-construction-profile', + id: 'org.hacp.fixture.packet-assembly', + version: '0.1-test', + transformationPolicy: 'select-declared-source-and-structure-bounded-fields', + fixtureOnly: true, + }, + tool: { + kind: 'packet-assembly-tool-identity', + id: 'fixture-packet-builder', + version: '1.0.0-test', + fixtureOnly: true, + }, + runtime: { + kind: 'packet-assembly-runtime-identity', + id: 'nodejs-fixture-runtime', + version: '24.14.0-test', + fixtureOnly: true, + }, + }; + const constructionRefs = { + profile: 'fixture://packet-assembly/construction/profile.json', + tool: 'fixture://packet-assembly/construction/tool.json', + runtime: 'fixture://packet-assembly/construction/runtime.json', + }; + + const verificationContext = { + schema: 'hacp.packet_assembly_attestation.fixture_verification_context.v1', + fixtureOnly: true, + profileRef: TRUST_PROFILE_REF, + trustedBuilders: [ + { + principalId: TRUSTED_PRINCIPAL, + authenticationMethod: AUTHENTICATION_METHOD, + keyId: TRUSTED_KEY_ID, + publicKeyJwk: trustedPublicKey, + status: 'active', + }, + ], + subjectPacket: { + path: path.relative(repoRoot, subjectPacketPath), + }, + sourceResolutions: [ + { + sourceRef: SOURCE_REF, + revision: SOURCE_REVISION, + path: path.relative(repoRoot, sourceSchemaPath), + }, + ], + constructionArtifactResolutions: Object.entries(constructionRefs).map(([kind, artifactRef]) => ({ + kind, + artifactRef, + path: `schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/construction/${kind}.json`, + })), + boundaryStatement: 'Synthetic fixture trust only. This context authenticates no production builder, source, approval, runtime admission, executor, or external effect.', + }; + + const unsigned = { + recordKind: 'hacp.experimental.packet_assembly_attestation', + schemaVersion: 'hacp-packet-assembly-attestation-0.1-draft', + attestationId: 'assembly_attestation_fixture_001', + builder: { + principalId: TRUSTED_PRINCIPAL, + authenticationMethod: AUTHENTICATION_METHOD, + verificationProfileRef: TRUST_PROFILE_REF, + keyId: TRUSTED_KEY_ID, + }, + packetBinding: { + packetId: subjectPacket.packetId, + packetSchemaVersion: subjectPacket.schemaVersion, + packetDigest: subjectPacket.digest, + }, + sourceBindings: [ + { + sourceRef: SOURCE_REF, + revision: SOURCE_REVISION, + digest: resolvedSourceDigest, + }, + ], + construction: { + profile: declaredArtifact(constructionDescriptors.profile, constructionRefs.profile), + tool: declaredArtifact(constructionDescriptors.tool, constructionRefs.tool), + runtime: declaredArtifact(constructionDescriptors.runtime, constructionRefs.runtime), + }, + constructedAt: '2026-09-08T07:00:00Z', + declaredTransformations: [ + { + transformationId: 'select-and-structure-bounded-request', + description: 'Selected declared source material and structured it into the bounded task-packet fields.', + inputRefs: [SOURCE_REF], + outputRefs: [`packet-digest:${subjectPacket.digest.value}`], + }, + ], + limitations: [ + 'Fixture authentication uses a public test key and establishes no production trust.', + 'The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.', + 'ConstructedAt is a signed declaration, not a trusted timestamp.', + ], + boundaryStatement: BOUNDARY_STATEMENT, + }; + + const valid = finalizeAttestation(unsigned, trustedPrivateKey, TRUSTED_KEY_ID); + + const sourceSubstitution = structuredClone(valid); + sourceSubstitution.sourceBindings[0].revision = 'git:0000000000000000000000000000000000000000'; + const sourceSubstitutionSigned = finalizeAttestation( + sourceSubstitution, + trustedPrivateKey, + TRUSTED_KEY_ID, + ); + + const packetMismatch = structuredClone(valid); + packetMismatch.packetBinding.packetDigest.value = 'a'.repeat(64); + const packetMismatchSigned = finalizeAttestation(packetMismatch, trustedPrivateKey, TRUSTED_KEY_ID); + + const selfAssertedIdentity = structuredClone(valid); + selfAssertedIdentity.builder = { + principalId: 'principal://example.invalid/self-asserted-builder', + authenticationMethod: AUTHENTICATION_METHOD, + verificationProfileRef: TRUST_PROFILE_REF, + keyId: 'key://example.invalid/self-asserted-builder/untrusted-1', + }; + const selfAssertedIdentitySigned = finalizeAttestation( + selfAssertedIdentity, + untrustedPrivateKey, + selfAssertedIdentity.builder.keyId, + ); + + const signatureMismatch = structuredClone(valid); + const firstCharacter = signatureMismatch.cryptographicBinding.signature[0]; + signatureMismatch.cryptographicBinding.signature = `${firstCharacter === 'A' ? 'B' : 'A'}${signatureMismatch.cryptographicBinding.signature.slice(1)}`; + + const changedClaim = structuredClone(valid); + changedClaim.declaredTransformations[0].description = 'Undeclared post-signature construction change.'; + + const constructionArtifactMismatch = structuredClone(valid); + constructionArtifactMismatch.construction.tool.artifactDigest.value = 'b'.repeat(64); + const constructionArtifactMismatchSigned = finalizeAttestation( + constructionArtifactMismatch, + trustedPrivateKey, + TRUSTED_KEY_ID, + ); + + const files = new Map([ + ['construction/profile.json', constructionDescriptors.profile], + ['construction/tool.json', constructionDescriptors.tool], + ['construction/runtime.json', constructionDescriptors.runtime], + ['verification-context.json', verificationContext], + ['valid/packet-assembly-attestation.valid.json', valid], + ['invalid/source-revision-substitution.invalid.json', sourceSubstitutionSigned], + ['invalid/packet-digest-mismatch.invalid.json', packetMismatchSigned], + ['invalid/self-asserted-identity.invalid.json', selfAssertedIdentitySigned], + ['invalid/signature-mismatch.invalid.json', signatureMismatch], + ['invalid/changed-claim.invalid.json', changedClaim], + ['invalid/construction-artifact-mismatch.invalid.json', constructionArtifactMismatchSigned], + ]); + + const manifest = { + schema: 'hacp.packet_assembly_attestation.conformance_manifest.v1', + status: 'experimental_draft', + sourceIssue: 'https://github.com/joefeser/hacp/issues/69', + hacpEvidenceBase: '671791f3e7568a57b8e97bea5ac7f8c9e7e74b06', + candidatePackageModified: false, + verificationContext: 'verification-context.json', + expectedValid: [ + { + path: 'valid/packet-assembly-attestation.valid.json', + expectedCodes: [], + }, + ], + expectedInvalid: [ + { + path: 'invalid/source-revision-substitution.invalid.json', + expectedCodes: ['SOURCE_REVISION_MISMATCH'], + }, + { + path: 'invalid/packet-digest-mismatch.invalid.json', + expectedCodes: ['PACKET_DIGEST_MISMATCH'], + }, + { + path: 'invalid/self-asserted-identity.invalid.json', + expectedCodes: ['UNTRUSTED_BUILDER_PRINCIPAL'], + }, + { + path: 'invalid/signature-mismatch.invalid.json', + expectedCodes: ['SIGNATURE_INVALID'], + }, + { + path: 'invalid/changed-claim.invalid.json', + expectedCodes: ['CLAIM_DIGEST_MISMATCH'], + }, + { + path: 'invalid/construction-artifact-mismatch.invalid.json', + expectedCodes: ['CONSTRUCTION_ARTIFACT_MISMATCH'], + }, + ], + boundaryStatement: 'Passing these fixtures proves bounded harness behavior only. It does not authenticate a production builder or prove correct packet construction.', + }; + files.set('manifest.json', manifest); + return { files, manifest, verificationContext }; +} + +function diagnostic(code, message) { + return { code, message }; +} + +export async function validateAttestation(attestation, verificationContext, validateSchema) { + if (!validateSchema(attestation)) { + return [diagnostic( + 'SCHEMA_VALIDATION_FAILED', + validateSchema.errors?.map((item) => `${item.instancePath || '/'} ${item.message}`).join('; ') || 'Schema validation failed.', + )]; + } + + const expectedClaimDigest = digestClaim(attestation); + if (!equalDigest(attestation.claimDigest, expectedClaimDigest)) { + return [diagnostic('CLAIM_DIGEST_MISMATCH', 'Construction claim digest does not match its canonical preimage.')]; + } + + if (attestation.builder.keyId !== attestation.cryptographicBinding.keyId + || attestation.builder.verificationProfileRef !== verificationContext.profileRef) { + return [diagnostic('UNTRUSTED_BUILDER_PRINCIPAL', 'Attestation cannot select or change its own trust binding.')]; + } + const trustedBuilder = verificationContext.trustedBuilders.find((entry) => ( + entry.status === 'active' + && entry.principalId === attestation.builder.principalId + && entry.authenticationMethod === attestation.builder.authenticationMethod + && entry.keyId === attestation.builder.keyId + )); + if (!trustedBuilder) { + return [diagnostic('UNTRUSTED_BUILDER_PRINCIPAL', 'No active verification-profile entry binds the declared principal, method, and key.')]; + } + + const signatureValid = verify( + null, + canonicalClaimInput(attestation), + createPublicKey({ key: trustedBuilder.publicKeyJwk, format: 'jwk' }), + Buffer.from(attestation.cryptographicBinding.signature, 'base64url'), + ); + if (!signatureValid) { + return [diagnostic('SIGNATURE_INVALID', 'Cryptographic binding does not verify under the admitted builder key.')]; + } + + const subjectPacket = await readJson(path.join(repoRoot, verificationContext.subjectPacket.path)); + if (!equalDigest(subjectPacket.digest, candidatePacketDigest(subjectPacket))) { + return [diagnostic('PACKET_DIGEST_MISMATCH', 'Subject packet digest is not reproducible.')]; + } + if (attestation.packetBinding.packetId !== subjectPacket.packetId + || attestation.packetBinding.packetSchemaVersion !== subjectPacket.schemaVersion + || !equalDigest(attestation.packetBinding.packetDigest, subjectPacket.digest)) { + return [diagnostic('PACKET_DIGEST_MISMATCH', 'Attestation does not bind the exact packet under review.')]; + } + + for (const source of attestation.sourceBindings) { + const resolution = verificationContext.sourceResolutions.find( + (entry) => entry.sourceRef === source.sourceRef, + ); + if (!resolution || resolution.revision !== source.revision) { + return [diagnostic('SOURCE_REVISION_MISMATCH', 'Declared source revision does not match trusted resolution context.')]; + } + const resolvedDigest = sourceDigest(await readFile(path.join(repoRoot, resolution.path))); + if (source.digest.algorithm !== resolvedDigest.algorithm + || source.digest.representation !== resolvedDigest.representation + || source.digest.value !== resolvedDigest.value) { + return [diagnostic('SOURCE_DIGEST_MISMATCH', 'Resolved source bytes do not match the declared source digest.')]; + } + } + + for (const [kind, artifact] of Object.entries(attestation.construction)) { + const resolution = verificationContext.constructionArtifactResolutions.find( + (entry) => entry.kind === kind && entry.artifactRef === artifact.artifactRef, + ); + if (!resolution) { + return [diagnostic('CONSTRUCTION_ARTIFACT_MISMATCH', `No trusted resolution exists for the declared ${kind} artifact.`)]; + } + const descriptorBytes = await readFile(path.join(repoRoot, resolution.path)); + const descriptor = JSON.parse(descriptorBytes); + const resolvedDigest = sourceDigest(descriptorBytes); + if (descriptor.id !== artifact.id + || descriptor.version !== artifact.version + || artifact.artifactDigest.algorithm !== resolvedDigest.algorithm + || artifact.artifactDigest.representation !== resolvedDigest.representation + || artifact.artifactDigest.value !== resolvedDigest.value) { + return [diagnostic('CONSTRUCTION_ARTIFACT_MISMATCH', `Declared ${kind} identity does not match resolved artifact bytes.`)]; + } + } + return []; +} + +async function writeCorpus(corpus) { + for (const [relativePath, value] of corpus.files) { + const output = path.join(fixtureRoot, relativePath); + await mkdir(path.dirname(output), { recursive: true }); + await writeFile(output, stableJson(value)); + } +} + +async function checkCorpus(corpus) { + const validateSchema = await schemaValidator(); + const cases = [...corpus.manifest.expectedValid, ...corpus.manifest.expectedInvalid]; + for (const [relativePath, expected] of corpus.files) { + const committed = await readFile(path.join(fixtureRoot, relativePath), 'utf8'); + if (committed !== stableJson(expected)) { + throw new Error(`Committed fixture is stale: ${relativePath}`); + } + } + for (const item of cases) { + const record = await readJson(path.join(fixtureRoot, item.path)); + const actualCodes = (await validateAttestation( + record, + corpus.verificationContext, + validateSchema, + )).map((entry) => entry.code); + if (JSON.stringify(actualCodes) !== JSON.stringify(item.expectedCodes)) { + throw new Error(`${item.path}: expected ${item.expectedCodes.join(', ') || 'valid'}, got ${actualCodes.join(', ') || 'valid'}`); + } + } + return { + schema: 'hacp.packet_assembly_attestation.validation_result.v1', + status: 'passed', + expectedValid: corpus.manifest.expectedValid.length, + expectedInvalid: corpus.manifest.expectedInvalid.length, + candidatePackageModified: false, + }; +} + +async function main() { + const writeMode = process.argv.includes('--write'); + const checkMode = process.argv.includes('--check') || !writeMode; + const corpus = await buildCorpus(); + if (writeMode) await writeCorpus(corpus); + if (checkMode) process.stdout.write(`${stableJson(await checkCorpus(corpus))}`); +} + +const isMain = process.argv[1] + && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url; +if (isMain) { + main().catch((error) => { + process.stderr.write(`${error.stack || error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/validate-packet-assembly-attestation.test.mjs b/scripts/validate-packet-assembly-attestation.test.mjs new file mode 100644 index 0000000..860bcbc --- /dev/null +++ b/scripts/validate-packet-assembly-attestation.test.mjs @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { test } from 'node:test'; +import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; +import { + buildCorpus, + digestClaim, + validateAttestation, +} from './validate-packet-assembly-attestation.mjs'; + +const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); +const packageRoot = path.join(repoRoot, 'schemas/experimental/packet-assembly-attestation/v0.1-draft'); +const schema = JSON.parse(await readFile( + path.join(packageRoot, 'packet-assembly-attestation.schema.json'), + 'utf8', +)); +const ajv = new Ajv2020({ allErrors: true, strict: true }); +addFormats(ajv); +const validateSchema = ajv.compile(schema); +const corpus = await buildCorpus(); +const valid = corpus.files.get('valid/packet-assembly-attestation.valid.json'); + +test('valid fixture authenticates the construction claim under supplied fixture trust', async () => { + assert.deepEqual( + await validateAttestation(valid, corpus.verificationContext, validateSchema), + [], + ); +}); + +test('all negative fixtures fail with their exact declared diagnostic', async () => { + for (const item of corpus.manifest.expectedInvalid) { + const record = corpus.files.get(item.path); + const actual = (await validateAttestation( + record, + corpus.verificationContext, + validateSchema, + )).map((entry) => entry.code); + assert.deepEqual(actual, item.expectedCodes, item.path); + } +}); + +test('a bare createdBy assertion is rejected by the closed record shape', async () => { + const record = { ...structuredClone(valid), createdBy: 'agent-claimed-builder' }; + assert.deepEqual( + (await validateAttestation(record, corpus.verificationContext, validateSchema)).map((entry) => entry.code), + ['SCHEMA_VALIDATION_FAILED'], + ); +}); + +test('the signature covers role separation and non-authority boundaries', async () => { + for (const mutate of [ + (record) => { record.builder.principalId = 'principal://example.invalid/other'; }, + (record) => { record.boundaryStatement = 'builder also approves'; }, + (record) => { record.limitations = ['claims correct construction']; }, + ]) { + const record = structuredClone(valid); + mutate(record); + assert.notDeepEqual(record.claimDigest, digestClaim(record)); + assert.deepEqual( + (await validateAttestation(record, corpus.verificationContext, validateSchema)).map((entry) => entry.code), + record.boundaryStatement === valid.boundaryStatement + ? ['CLAIM_DIGEST_MISMATCH'] + : ['SCHEMA_VALIDATION_FAILED'], + ); + } +}); + +test('the published v0.3-candidate task-packet schema remains unchanged in meaning', async () => { + const candidate = JSON.parse(await readFile( + path.join(repoRoot, 'schemas/v0.3-candidate/task-packet.schema.json'), + 'utf8', + )); + assert.equal(candidate.properties.recordKind.const, 'hacp.v0_3_candidate.task_packet'); + assert.equal(candidate.properties.schemaVersion.const, 'hacp-0.3-candidate'); + assert.equal(Object.hasOwn(candidate.properties, 'builder'), false); + assert.equal(Object.hasOwn(candidate.properties, 'packetAssemblyAttestation'), false); +}); From 200c615b638af7f1ae169966865bd11ba041bd71 Mon Sep 17 00:00:00 2001 From: Joe Feser Date: Tue, 8 Sep 2026 02:09:05 -0500 Subject: [PATCH 2/2] Address packet attestation review findings --- rfcs/0010-packet-assembly-attestation.md | 5 +- schemas/README.md | 6 +- .../v0.1-draft/README.md | 9 +- .../v0.1-draft/fixtures/manifest.json | 8 ++ ...t-assembly-attestation.git-blob.valid.json | 94 +++++++++++++++++++ ...ly-attestation.json-rfc8785-jcs.valid.json | 94 +++++++++++++++++++ .../packet-assembly-attestation.schema.json | 2 +- .../validate-packet-assembly-attestation.mjs | 81 +++++++++++++--- ...idate-packet-assembly-attestation.test.mjs | 17 ++-- 9 files changed, 291 insertions(+), 25 deletions(-) create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.git-blob.valid.json create mode 100644 schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.json-rfc8785-jcs.valid.json diff --git a/rfcs/0010-packet-assembly-attestation.md b/rfcs/0010-packet-assembly-attestation.md index c2b2d8d..79c5e47 100644 --- a/rfcs/0010-packet-assembly-attestation.md +++ b/rfcs/0010-packet-assembly-attestation.md @@ -183,8 +183,9 @@ evidence. ## Conformance Evidence -The experimental package includes a positive signed fixture and negative -fixtures for source-revision substitution, packet-digest mismatch, +The experimental package includes positive signed fixtures for raw-byte, +Git-blob, and RFC 8785 canonical-JSON content digests, plus negative fixtures +for source-revision substitution, packet-digest mismatch, self-asserted identity under an untrusted key, signature corruption, and post-signature claim mutation. The included keys and identities are synthetic, public test material and MUST NOT be trusted outside the fixture harness. diff --git a/schemas/README.md b/schemas/README.md index 9837083..0496e60 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -30,8 +30,10 @@ Need to choose a concrete stop reason? See - `https://hacp.example/schemas/v0.2-draft/` - `https://hacp.example/schemas/v0.3-candidate/` for executable candidate interworking contracts; exact digest domains remain owner review gates. - - `https://hacp.io/schemas/experimental/packet-assembly-attestation/v0.1-draft/` - for the separate experimental construction-provenance record family. + - `https://hacp.example/schemas/experimental/packet-assembly-attestation/v0.1-draft/` + for the separate experimental construction-provenance record family. This + deliberately non-routable identifier does not imply publication by hacp.io; + use the repository copy of the schema. Versioning for future phases should preserve backward-readable draft history rather than rewriting old identifiers. diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md b/schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md index 53f9382..a091919 100644 --- a/schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/README.md @@ -24,8 +24,10 @@ npm run hacp:packet-assembly-attestation The validator checks the closed schema, RFC 8785 claim digest, Ed25519 fixture signature, independently supplied synthetic trust binding, exact packet -binding, and source revision/digest resolution. All fixture identities and keys -are public test material with no production authority. +binding, and source revision/digest resolution. Positive fixtures exercise +raw-byte, Git-blob, and RFC 8785 canonical-JSON representations for both source +and construction-artifact digests. All fixture identities and keys are public +test material with no production authority. ## Files @@ -35,7 +37,8 @@ are public test material with no production authority. builder trust and source-resolution context for the harness. - `fixtures/construction/`: exact synthetic profile, tool, and runtime identity descriptors bound by the valid attestation. -- `fixtures/valid/`: one authenticated construction claim. +- `fixtures/valid/`: authenticated construction claims covering every declared + content-digest representation. - `fixtures/invalid/`: source substitution, packet mismatch, self-asserted identity, signature corruption, changed-claim, and construction-artifact mismatch cases. diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json index f965abf..71aee9b 100644 --- a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/manifest.json @@ -9,6 +9,14 @@ { "path": "valid/packet-assembly-attestation.valid.json", "expectedCodes": [] + }, + { + "path": "valid/packet-assembly-attestation.git-blob.valid.json", + "expectedCodes": [] + }, + { + "path": "valid/packet-assembly-attestation.json-rfc8785-jcs.valid.json", + "expectedCodes": [] } ], "expectedInvalid": [ diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.git-blob.valid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.git-blob.valid.json new file mode 100644 index 0000000..bece960 --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.git-blob.valid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_git_blob", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "git-blob", + "value": "e122d39bcd7f5800b0828eebff9b2cc206b86f733ed06fdf24bd9481032a12cd" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "git-blob", + "value": "d4fde57820c1c491d89e1fd4832fa6f9c0ff38c733b037275801aec769e8af30" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "git-blob", + "value": "56b9239e86e3c18361a3f08b309ca2764d6f9c8ffd7e09584c61995a7004132d" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "git-blob", + "value": "8d98009642a09a13df2e52574486cdaf90f7acb793f09a8be1e56f3770252a15" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "64ecab944bb8930cf56a112a81a0d69cc7b07f05218f5b8782ebe1566b1a704f" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "6Vvi7t1H4K8oskBOH4rwVT5EUlX6MRVmxB6nG1v2sglkVvdaJc1_6_OS2il12o77ohFrGoUi1VxsCnIjghJiBw" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.json-rfc8785-jcs.valid.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.json-rfc8785-jcs.valid.json new file mode 100644 index 0000000..b324aaa --- /dev/null +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/fixtures/valid/packet-assembly-attestation.json-rfc8785-jcs.valid.json @@ -0,0 +1,94 @@ +{ + "recordKind": "hacp.experimental.packet_assembly_attestation", + "schemaVersion": "hacp-packet-assembly-attestation-0.1-draft", + "attestationId": "assembly_attestation_fixture_canonical_json", + "builder": { + "principalId": "principal://example.invalid/packet-builder", + "authenticationMethod": "profile_bound_ed25519_key", + "verificationProfileRef": "profile://example.invalid/packet-builder-test/v0.1", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1" + }, + "packetBinding": { + "packetId": "taskpkt_example_continue_review_001", + "packetSchemaVersion": "hacp-0.3-candidate", + "packetDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.task-packet.v0.3-candidate", + "value": "c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + } + }, + "sourceBindings": [ + { + "sourceRef": "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json", + "revision": "git:671791f3e7568a57b8e97bea5ac7f8c9e7e74b06", + "digest": { + "algorithm": "sha256", + "representation": "json-rfc8785-jcs", + "value": "26aacda7b607ced00008c54e0bc099ef3fd13bb0198a4e9d30a2d4e68fe3a5fd" + } + } + ], + "construction": { + "profile": { + "id": "org.hacp.fixture.packet-assembly", + "version": "0.1-test", + "artifactRef": "fixture://packet-assembly/construction/profile.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "json-rfc8785-jcs", + "value": "92f287d769d98b3e775864e24bb0316cbbdc260dc196cd1aa771b3605457e447" + } + }, + "tool": { + "id": "fixture-packet-builder", + "version": "1.0.0-test", + "artifactRef": "fixture://packet-assembly/construction/tool.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "json-rfc8785-jcs", + "value": "428919bedceda96fc6abf594abb73909e8ed4204f1a94275f12233729e6a7aae" + } + }, + "runtime": { + "id": "nodejs-fixture-runtime", + "version": "24.14.0-test", + "artifactRef": "fixture://packet-assembly/construction/runtime.json", + "artifactDigest": { + "algorithm": "sha256", + "representation": "json-rfc8785-jcs", + "value": "7c8e34fff5f48ddb0d7ff5429ea7e6f4076f5f93228f133d18a0d20b754f1297" + } + } + }, + "constructedAt": "2026-09-08T07:00:00Z", + "declaredTransformations": [ + { + "transformationId": "select-and-structure-bounded-request", + "description": "Selected declared source material and structured it into the bounded task-packet fields.", + "inputRefs": [ + "repo://joefeser/hacp/schemas/v0.3-candidate/task-packet.schema.json" + ], + "outputRefs": [ + "packet-digest:c330d201bd02c7176542740377ea5be9f9b0621b3ea67f7e8e520464a10d58a4" + ] + } + ], + "limitations": [ + "Fixture authentication uses a public test key and establishes no production trust.", + "The attestation does not prove source completeness, source authenticity, correct interpretation, or correct construction.", + "ConstructedAt is a signed declaration, not a trusted timestamp." + ], + "boundaryStatement": "This attestation binds an authenticated builder claim under an independently selected verification profile. It does not prove correct construction, source authenticity, human approval, runtime admission, execution, completion, or external effects.", + "claimDigest": { + "algorithm": "sha256", + "canonicalization": "json-rfc8785-jcs", + "digestDomain": "io.hacp.packet-assembly-attestation-claim.v0.1-draft", + "value": "462336451e9e6298e02888d30fe8bafa77111918126311558e3c8bce13142f00" + }, + "cryptographicBinding": { + "scheme": "ed25519", + "keyId": "key://example.invalid/packet-builder/fixture-ed25519-1", + "signature": "HcGukRSkCthkCT5owbb2b_lB53pEXSw6fsrLzy8uZL2eJ89ia-WRQVHfrouYyRZBWN6JaGcHbL86A3xrGxXAAA" + } +} diff --git a/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json b/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json index 99ca3d2..0885114 100644 --- a/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json +++ b/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://hacp.io/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json", + "$id": "https://hacp.example/schemas/experimental/packet-assembly-attestation/v0.1-draft/packet-assembly-attestation.schema.json", "title": "HACP Experimental Packet Assembly Attestation v0.1 Draft", "type": "object", "additionalProperties": false, diff --git a/scripts/validate-packet-assembly-attestation.mjs b/scripts/validate-packet-assembly-attestation.mjs index 49f8738..15c93d6 100644 --- a/scripts/validate-packet-assembly-attestation.mjs +++ b/scripts/validate-packet-assembly-attestation.mjs @@ -60,20 +60,38 @@ function privateKeyFromSeed(seedHex) { }); } -function sourceDigest(bytes) { +export function contentDigest(bytes, representation = 'raw-bytes') { + let digestInput; + if (representation === 'raw-bytes') { + digestInput = bytes; + } else if (representation === 'git-blob') { + digestInput = Buffer.concat([ + Buffer.from(`blob ${bytes.length}\0`, 'utf8'), + bytes, + ]); + } else if (representation === 'json-rfc8785-jcs') { + const serialized = canonicalize(JSON.parse(bytes.toString('utf8'))); + if (serialized === undefined) throw new Error('RFC 8785 content serialization failed.'); + digestInput = Buffer.from(serialized, 'utf8'); + } else { + throw new Error(`Unsupported digest representation: ${representation}`); + } return { algorithm: 'sha256', - representation: 'raw-bytes', - value: createHash('sha256').update(bytes).digest('hex'), + representation, + value: createHash('sha256').update(digestInput).digest('hex'), }; } -function declaredArtifact(descriptor, artifactRef) { +function declaredArtifact(descriptor, artifactRef, representation = 'raw-bytes') { return { id: descriptor.id, version: descriptor.version, artifactRef, - artifactDigest: sourceDigest(Buffer.from(stableJson(descriptor), 'utf8')), + artifactDigest: contentDigest( + Buffer.from(stableJson(descriptor), 'utf8'), + representation, + ), }; } @@ -144,7 +162,7 @@ export async function buildCorpus() { const trustedPublicKey = createPublicKey(trustedPrivateKey).export({ format: 'jwk' }); const subjectPacket = await readJson(subjectPacketPath); const sourceBytes = await readFile(sourceSchemaPath); - const resolvedSourceDigest = sourceDigest(sourceBytes); + const resolvedSourceDigest = contentDigest(sourceBytes); if (!equalDigest(subjectPacket.digest, candidatePacketDigest(subjectPacket)) || subjectPacket.digest.value !== EXPECTED_PACKET_DIGEST) { @@ -258,6 +276,22 @@ export async function buildCorpus() { const valid = finalizeAttestation(unsigned, trustedPrivateKey, TRUSTED_KEY_ID); + const validForRepresentation = (representation, suffix) => { + const variant = structuredClone(unsigned); + variant.attestationId = `assembly_attestation_fixture_${suffix}`; + variant.sourceBindings[0].digest = contentDigest(sourceBytes, representation); + for (const [kind, descriptor] of Object.entries(constructionDescriptors)) { + variant.construction[kind] = declaredArtifact( + descriptor, + constructionRefs[kind], + representation, + ); + } + return finalizeAttestation(variant, trustedPrivateKey, TRUSTED_KEY_ID); + }; + const validGitBlob = validForRepresentation('git-blob', 'git_blob'); + const validCanonicalJson = validForRepresentation('json-rfc8785-jcs', 'canonical_json'); + const sourceSubstitution = structuredClone(valid); sourceSubstitution.sourceBindings[0].revision = 'git:0000000000000000000000000000000000000000'; const sourceSubstitutionSigned = finalizeAttestation( @@ -304,6 +338,8 @@ export async function buildCorpus() { ['construction/runtime.json', constructionDescriptors.runtime], ['verification-context.json', verificationContext], ['valid/packet-assembly-attestation.valid.json', valid], + ['valid/packet-assembly-attestation.git-blob.valid.json', validGitBlob], + ['valid/packet-assembly-attestation.json-rfc8785-jcs.valid.json', validCanonicalJson], ['invalid/source-revision-substitution.invalid.json', sourceSubstitutionSigned], ['invalid/packet-digest-mismatch.invalid.json', packetMismatchSigned], ['invalid/self-asserted-identity.invalid.json', selfAssertedIdentitySigned], @@ -324,6 +360,14 @@ export async function buildCorpus() { path: 'valid/packet-assembly-attestation.valid.json', expectedCodes: [], }, + { + path: 'valid/packet-assembly-attestation.git-blob.valid.json', + expectedCodes: [], + }, + { + path: 'valid/packet-assembly-attestation.json-rfc8785-jcs.valid.json', + expectedCodes: [], + }, ], expectedInvalid: [ { @@ -415,9 +459,16 @@ export async function validateAttestation(attestation, verificationContext, vali if (!resolution || resolution.revision !== source.revision) { return [diagnostic('SOURCE_REVISION_MISMATCH', 'Declared source revision does not match trusted resolution context.')]; } - const resolvedDigest = sourceDigest(await readFile(path.join(repoRoot, resolution.path))); + let resolvedDigest; + try { + resolvedDigest = contentDigest( + await readFile(path.join(repoRoot, resolution.path)), + source.digest.representation, + ); + } catch { + return [diagnostic('SOURCE_DIGEST_MISMATCH', 'Resolved source content cannot be represented as declared.')]; + } if (source.digest.algorithm !== resolvedDigest.algorithm - || source.digest.representation !== resolvedDigest.representation || source.digest.value !== resolvedDigest.value) { return [diagnostic('SOURCE_DIGEST_MISMATCH', 'Resolved source bytes do not match the declared source digest.')]; } @@ -431,12 +482,20 @@ export async function validateAttestation(attestation, verificationContext, vali return [diagnostic('CONSTRUCTION_ARTIFACT_MISMATCH', `No trusted resolution exists for the declared ${kind} artifact.`)]; } const descriptorBytes = await readFile(path.join(repoRoot, resolution.path)); - const descriptor = JSON.parse(descriptorBytes); - const resolvedDigest = sourceDigest(descriptorBytes); + let descriptor; + let resolvedDigest; + try { + descriptor = JSON.parse(descriptorBytes); + resolvedDigest = contentDigest( + descriptorBytes, + artifact.artifactDigest.representation, + ); + } catch { + return [diagnostic('CONSTRUCTION_ARTIFACT_MISMATCH', `Resolved ${kind} artifact cannot be represented as declared.`)]; + } if (descriptor.id !== artifact.id || descriptor.version !== artifact.version || artifact.artifactDigest.algorithm !== resolvedDigest.algorithm - || artifact.artifactDigest.representation !== resolvedDigest.representation || artifact.artifactDigest.value !== resolvedDigest.value) { return [diagnostic('CONSTRUCTION_ARTIFACT_MISMATCH', `Declared ${kind} identity does not match resolved artifact bytes.`)]; } diff --git a/scripts/validate-packet-assembly-attestation.test.mjs b/scripts/validate-packet-assembly-attestation.test.mjs index 860bcbc..5b829f9 100644 --- a/scripts/validate-packet-assembly-attestation.test.mjs +++ b/scripts/validate-packet-assembly-attestation.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; import Ajv2020 from 'ajv/dist/2020.js'; import addFormats from 'ajv-formats'; import { @@ -10,7 +11,7 @@ import { validateAttestation, } from './validate-packet-assembly-attestation.mjs'; -const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const packageRoot = path.join(repoRoot, 'schemas/experimental/packet-assembly-attestation/v0.1-draft'); const schema = JSON.parse(await readFile( path.join(packageRoot, 'packet-assembly-attestation.schema.json'), @@ -22,11 +23,15 @@ const validateSchema = ajv.compile(schema); const corpus = await buildCorpus(); const valid = corpus.files.get('valid/packet-assembly-attestation.valid.json'); -test('valid fixture authenticates the construction claim under supplied fixture trust', async () => { - assert.deepEqual( - await validateAttestation(valid, corpus.verificationContext, validateSchema), - [], - ); +test('valid fixtures authenticate every advertised content representation', async () => { + for (const item of corpus.manifest.expectedValid) { + const record = corpus.files.get(item.path); + assert.deepEqual( + await validateAttestation(record, corpus.verificationContext, validateSchema), + [], + item.path, + ); + } }); test('all negative fixtures fail with their exact declared diagnostic', async () => {