hachure 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -236,6 +236,7 @@ a profile requires no changes to core record shapes or the status function.
236
236
  |---|---|---|
237
237
  | in-toto interop | [interop-in-toto.md](interop-in-toto.md) | Wrapping a TrustBundle as a signed in-toto Statement v1 / DSSE envelope. |
238
238
  | Verification endpoint | [verification-endpoint.md](verification-endpoint.md) | Producer-served HTTP endpoint for receivers to fetch post-export event deltas. |
239
+ | Assurance | [assurance.md](assurance.md) | Signing as a dial: L0/L1/L2 assurance levels, identity presentation, consumer policy, and human signing ceremony. |
239
240
 
240
241
  ---
241
242
 
package/assurance.md ADDED
@@ -0,0 +1,230 @@
1
+ # Assurance — Extension Profile
2
+
3
+ **Profile type:** OPTIONAL extension
4
+ **Status:** draft
5
+ **Namespace:** `hachure.org/v1`
6
+ **Depends on:** core record shapes, [interop-in-toto.md](interop-in-toto.md), [verification-endpoint.md](verification-endpoint.md)
7
+
8
+ ---
9
+
10
+ ## Principle: signing is a dial, not a gate
11
+
12
+ Unsigned records are valid core records. Forever. This profile defines how
13
+ consumers express and verify *higher* assurance, and how policies weigh the
14
+ levels against one another. It is never a prerequisite for producing or
15
+ consuming a TrustBundle.
16
+
17
+ The assurance model exists because some environments want non-repudiation, audit
18
+ trails that survive producer outages, or cryptographic evidence that a specific
19
+ human exercised a specific authority. Signing is the mechanism that provides
20
+ those properties — but it is layered on top of an already-valid record, not a
21
+ precondition for record validity. A bundle with no signatures is not a
22
+ degraded bundle; it is an L0 bundle.
23
+
24
+ ---
25
+
26
+ ## Assurance levels
27
+
28
+ Three levels are defined. Higher levels are a strict superset of lower levels:
29
+ an L2 record satisfies any policy that accepts L1 or L0.
30
+
31
+ ### L0 — producer-asserted (default)
32
+
33
+ The record is unsigned. Assurance derives entirely from the trustworthiness of
34
+ the transport channel and the producer identity declared in the bundle's
35
+ `source` field. All TrustBundles that do not carry a DSSE envelope are L0.
36
+ This is the default; no annotation or flag is required.
37
+
38
+ ### L1 — identity-signed (keyless / ephemeral)
39
+
40
+ The record is wrapped in a DSSE envelope (per
41
+ [interop-in-toto.md](interop-in-toto.md)). The signing key is short-lived and
42
+ bound to an identity asserted by an OIDC provider at signing time — a CI
43
+ workflow identity (`sub: repo:org/repo:ref:refs/heads/main`) or a human account
44
+ (`sub: user@example.com`). The certificate binds the ephemeral public key to
45
+ that OIDC identity for the lifetime of the signing event.
46
+
47
+ An L1 signature MAY be submitted to a public transparency log (Rekor or
48
+ equivalent). When it is, the resulting log entry UUID SHOULD be stored in the
49
+ bundle's `proof` block as an `IntegrityAnchor` of kind `transparency_log`.
50
+ Transparency-log inclusion is encouraged but not required for L1 conformance.
51
+
52
+ The OIDC issuer and subject carried in the signing certificate are the
53
+ authoritative identity for the signing event. Implementations that verify L1
54
+ signatures MUST surface a human-readable identity derived from those fields (see
55
+ [Identity presentation](#identity-presentation) below).
56
+
57
+ ### L2 — held-key (org-controlled KMS/HSM)
58
+
59
+ The record is wrapped in a DSSE envelope and signed with a long-lived key held
60
+ in an org-controlled key-management system (a KMS or HSM). The key is not
61
+ ephemeral and is not tied to a per-operation OIDC token. Key rotation and
62
+ revocation are producer policy; this profile imposes no rotation schedule or
63
+ revocation mechanism.
64
+
65
+ L2 is appropriate for environments that cannot use public OIDC issuers or
66
+ public transparency logs — air-gapped deployments, highly regulated environments
67
+ with internal PKI requirements, or organisations that maintain their own CA.
68
+
69
+ Because L2 keys are long-lived, verifiers must obtain the producer's public key
70
+ through an out-of-band channel (internal PKI, a published key-distribution
71
+ endpoint, a previously-trusted bundle). This profile does not specify that
72
+ channel.
73
+
74
+ ---
75
+
76
+ ## What gets signed
77
+
78
+ Producers that adopt this profile MAY sign the following record types. Signing
79
+ any of them does not change the record's schema; the DSSE envelope wraps the
80
+ serialised record and is carried alongside it, not embedded in it.
81
+
82
+ ### TrustBundles
83
+
84
+ A full TrustBundle, including release bundles, is the primary signing target.
85
+ Signing a bundle asserts that the named producer attests to the entire contents
86
+ at the moment the envelope was created. The in-toto Statement `subject` array
87
+ SHOULD identify the bundle's `integrityAnchor.value` so downstream verifiers can
88
+ correlate the envelope with a specific bundle revision.
89
+
90
+ ### Attestations
91
+
92
+ A human's `ReviewOutcome` record carrying an `authorizing` block (ADR 0004) is
93
+ an identity-signed attestation. When the human's OIDC identity signs that
94
+ record at the moment the authority is exercised (see
95
+ [Human signing ceremony](#human-signing-ceremony)), the result is non-repudiable
96
+ evidence that a specific identity made a specific decision. This signed
97
+ attestation is the appropriate content for the `identityEvidence` field on a
98
+ Claim: it collapses "someone in this org approved this" into "this OIDC-verified
99
+ identity approved this, cryptographically."
100
+
101
+ ### Verification-endpoint responses
102
+
103
+ A producer serving a verification-endpoint response (see
104
+ [verification-endpoint.md](verification-endpoint.md)) MAY wrap the response
105
+ bundle in a DSSE envelope before returning it. A signed response upgrades the
106
+ assurance level of that endpoint from L0 to L1 or L2 according to the key type
107
+ used. Receivers SHOULD apply the same identity-presentation rules to a signed
108
+ response as to any other signed record.
109
+
110
+ ---
111
+
112
+ ## Identity presentation
113
+
114
+ Verifiers MUST surface a human-readable identity for every signed record they
115
+ display or log. Raw key fingerprints are not an acceptable primary display.
116
+
117
+ Derive the display identity from the DSSE envelope's signing certificate:
118
+
119
+ - For L1 (OIDC-backed): present the OIDC issuer and subject in a
120
+ human-readable form. Examples:
121
+ - CI workflow: `"signed by github.com/org/repo workflow .github/workflows/release.yml"`
122
+ - Human account: `"signed by alice@example.com (GitHub)"`
123
+ - For L2 (held key): present the key's subject DN or the KMS key alias as
124
+ configured by the producer. Example: `"signed by CN=kontour-release-key, O=Kontour AI"`
125
+
126
+ Displaying a fingerprint alongside the human-readable identity is permitted and
127
+ encouraged for debugging. It is not a substitute for the identity string as the
128
+ primary display element.
129
+
130
+ ---
131
+
132
+ ## Consumer policy
133
+
134
+ Assurance requirements are policy, not wire format. A consumer expresses its
135
+ requirements through a `VerificationPolicy` that declares a minimum assurance
136
+ level among its `acceptanceCriteria`. Mismatches between a record's actual
137
+ level and the consumer's required level are **transparency gaps** — structured
138
+ annotations that feed the status function's inputs and surface as `proposed` or
139
+ `assumed` status rather than silent hard failures.
140
+
141
+ This mirrors the admissibility doctrine in ADR 0004 §4: heuristics emit issues
142
+ for human review; they do not silently decide. A bundle whose assurance level
143
+ falls below a consumer's requirement is recorded and flagged; a human or an
144
+ automated policy layer decides whether to accept, escalate, or re-request.
145
+
146
+ Concretely:
147
+
148
+ - An L0 bundle that satisfies all policy evidence requirements reaches
149
+ `verified` normally. If the consumer policy also requires L1, the consumer
150
+ annotates the gap; the status function sees the annotation as a transparency
151
+ gap and surfaces the claim at `proposed` pending a signed replacement.
152
+ - An L1 or L2 bundle that satisfies all policy evidence requirements and meets
153
+ the assurance requirement reaches `verified` without annotation.
154
+ - A signed record with an unverifiable or expired certificate does not silently
155
+ downgrade to L0. The verification failure is itself a transparency gap,
156
+ flagged for the attention queue.
157
+
158
+ ---
159
+
160
+ ## Human signing ceremony
161
+
162
+ When a human exercises authority — authoring a policy-change attestation,
163
+ signing off a release bundle, or countersigning a ReviewOutcome — the signing
164
+ gesture MUST be interactive and co-located in time with the authorizing act.
165
+
166
+ The ceremony is an interactive OIDC browser flow: the same gesture as
167
+ sign-in-with-Google or a Stripe payment confirmation. The human authenticates
168
+ to the OIDC provider, the provider issues a short-lived certificate bound to
169
+ the authenticated identity, and that certificate signs the record. The entire
170
+ flow completes in the same session as the authorizing act; signing a record
171
+ retroactively from a different session is not a conforming ceremony.
172
+
173
+ **Recommended mechanism for highest-assurance human ceremonies:** WebAuthn /
174
+ passkey with user-verification (UV=1). A device-local authenticator satisfies
175
+ three independent assurance legs simultaneously:
176
+
177
+ - **Presence** — the passkey gesture (biometric or PIN) proves the enrolled
178
+ device is physically present; biometric data never leaves the device.
179
+ - **Identity** — the hardware-backed assertion composes with the OIDC
180
+ certificate: the certificate binds the ephemeral key to the OIDC subject;
181
+ the passkey attests the device-local identity enrolled at registration time.
182
+ - **Engagement** (non-normative) — oversight metrics such as session duration
183
+ or interaction events can accompany the record as non-normative evidence;
184
+ they are not part of the conformance criteria.
185
+
186
+ Because the three legs are independent, losing one (e.g., no biometric sensor
187
+ on the device) does not forfeit the others. An OIDC-only ceremony without a
188
+ passkey assertion is still a conforming L1 ceremony; the passkey upgrade is
189
+ RECOMMENDED where the platform supports it.
190
+
191
+ Design intent: friction lands only where authority is exercised. Routine
192
+ read-only operations, status queries, and bundle consumption carry no signing
193
+ requirement. The ceremony surfaces once, at the moment of consequence.
194
+
195
+ ## Collection channels and media evidence
196
+
197
+ Any collection channel defined in the reference implementation (see ADR 0004 in
198
+ the reference implementation) MAY attach media evidence — for example,
199
+ capture-provenance imagery conforming to C2PA — as ordinary evidence entries
200
+ with integrity anchors. Three rules govern such attachments:
201
+
202
+ 1. **Channels do not replace admissible testimony kinds.** A photo or video of
203
+ a signing gesture is attached evidence, not a substitute for an `exchange` or
204
+ `authorized-action` block. The admissible testimony kinds in ADR 0004 remain
205
+ the authoritative record; media is corroborating context.
206
+ 2. **Capture provenance determines evidence grade.** Media accompanied by a
207
+ valid C2PA manifest (or equivalent capture-provenance credential) is treated
208
+ as integrity-anchored evidence at the level its anchor supports. Media
209
+ without capture provenance is ordinary L0 evidence: present, but carrying no
210
+ additional assurance weight beyond the transport channel.
211
+ 3. **Integrity anchors are required for non-L0 media claims.** If a consumer
212
+ policy requires L1 or higher evidence grade for media, the media item MUST
213
+ carry an `IntegrityAnchor` referencing the capture-provenance credential.
214
+
215
+ ---
216
+
217
+ ## Non-goals
218
+
219
+ - **Mandating a CA or transparency log.** This profile does not require a
220
+ specific certificate authority, a specific OIDC issuer, or submission to any
221
+ particular log. L1 signing with a private OIDC issuer and no public log is
222
+ valid L1.
223
+ - **Key custody prescriptions.** How a producer acquires, stores, rotates, or
224
+ revokes L2 keys is producer policy. This profile does not specify HSM vendor,
225
+ key-rotation schedule, or revocation-distribution mechanism.
226
+ - **Core-format changes.** No core record schema is altered by this profile.
227
+ Adopting the assurance profile requires no schema migration and no changes to
228
+ the status derivation function.
229
+ - **Signing every record.** Signing is always optional. Producers that sign
230
+ nothing are fully conformant with the core specification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hachure",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Hachure \u2014 canonical distribution of the open trust format: normative JSON schemas, conformance test vectors, and spec constants.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -16,7 +16,8 @@
16
16
  "README.md",
17
17
  "status-function.md",
18
18
  "interop-in-toto.md",
19
- "verification-endpoint.md"
19
+ "verification-endpoint.md",
20
+ "assurance.md"
20
21
  ],
21
22
  "scripts": {
22
23
  "test": "node --test test/index.test.mjs"
@@ -36,4 +37,4 @@
36
37
  "type": "git",
37
38
  "url": "git+https://github.com/hachure-org/spec.git"
38
39
  }
39
- }
40
+ }
@@ -24,9 +24,34 @@
24
24
  "collectedBy": { "type": "string" },
25
25
  "integrityRef": { "type": "string" },
26
26
  "integrityAnchor": { "$ref": "#/$defs/integrityAnchor" },
27
+ "passing": {
28
+ "type": "boolean",
29
+ "description": "Whether the check that produced this evidence passed. Omitted when the evidence is not the output of a pass/fail check."
30
+ },
31
+ "blocking": {
32
+ "type": "boolean",
33
+ "description": "Whether a non-passing result should block (e.g. trigger disputed status under a policy conflict rule)."
34
+ },
35
+ "execution": {
36
+ "$ref": "#/$defs/execution",
37
+ "description": "How the evidence was produced: the command or tool invocation, exit code, and duration — making artifacts self-describing without requiring the producer's adapter config."
38
+ },
27
39
  "metadata": { "type": "object" }
28
40
  },
29
41
  "$defs": {
42
+ "execution": {
43
+ "type": "object",
44
+ "required": ["runner", "label"],
45
+ "properties": {
46
+ "runner": { "enum": ["bash", "mcp"] },
47
+ "label": { "type": "string" },
48
+ "exitCode": { "type": "integer" },
49
+ "isError": { "type": "boolean" },
50
+ "durationMs": { "type": "number" },
51
+ "metadata": { "type": "object" }
52
+ },
53
+ "additionalProperties": false
54
+ },
30
55
  "integrityAnchor": {
31
56
  "type": "object",
32
57
  "required": ["id", "kind", "algorithm", "value", "sourceRef"],
@@ -30,7 +30,7 @@ Producers supporting this profile expose a verification endpoint at a well-known
30
30
  path on the same host that issued the bundle:
31
31
 
32
32
  ```
33
- GET https://<producer-host>/.well-known/hachure/verify?ref=<integrityRef>[&ref=...]
33
+ GET https://<producer-host>/.well-known/hachure/verify?ref=<ref>[&ref=...]
34
34
  ```
35
35
 
36
36
  Multiple refs may be passed as repeated `ref` query parameters. For large sets,
@@ -40,17 +40,39 @@ a POST variant is also allowed:
40
40
  POST https://<producer-host>/.well-known/hachure/verify
41
41
  Content-Type: application/json
42
42
 
43
- { "refs": ["<integrityRef>", ...] }
43
+ { "refs": ["<ref>", ...] }
44
44
  ```
45
45
 
46
- `integrityRef` is the integrity anchor value carried on a claim or bundle
47
- (`claim.integrityRef` or a bundle-level `integrityAnchor.value`).
48
-
49
46
  Producers MAY require authentication before serving a response. The response
50
47
  semantics defined in this profile are unchanged by the presence or absence of
51
48
  authentication; the receiver simply may not be able to reach the endpoint
52
49
  without valid credentials.
53
50
 
51
+ ### Ref resolution semantics
52
+
53
+ A `ref` value in the request MAY be any of the following:
54
+
55
+ - **(a) A claim `id`** — the stable identifier on a claim object (`claim.id`).
56
+ - **(b) A claim's current integrity ref** — the `currentIntegrityRef` string field
57
+ on a claim (`claim.currentIntegrityRef`, per `schemas/claim.schema.json`).
58
+ Note: the analogous full anchor object is `claim.currentIntegrityAnchor`; when
59
+ resolving by integrity value, match against `currentIntegrityRef` or
60
+ `currentIntegrityAnchor.value`.
61
+ - **(c) A bundle-level integrity anchor value** — the `.value` field of an
62
+ `integrityAnchor` object carried on an `authorityTrace` or `evidence` record
63
+ within a bundle (`schemas/trust-bundle.schema.json` `$defs/integrityAnchor`
64
+ → `value`).
65
+
66
+ Producers MUST support lookup by (a) and (b). Producers MAY support lookup by (c).
67
+ Receivers SHOULD prefer (a) for stability; (b) and (c) are useful when only an
68
+ integrity value is available from a prior bundle snapshot.
69
+
70
+ **Multi-match:** a single ref value MAY match more than one claim (for example,
71
+ two claims that share the same `currentIntegrityRef`). When a ref matches multiple
72
+ claims, the response includes ALL matching claims. A ref is reported as unknown
73
+ (in `unknownRefs`) only when it matches nothing — zero claims, evidence records,
74
+ or authority traces.
75
+
54
76
  ---
55
77
 
56
78
  ## Response shape
@@ -60,8 +82,9 @@ scoped to the records matching the requested refs, plus a `metadata` extension
60
82
  block. The bundle carries:
61
83
 
62
84
  - **`source`** — the producer identifier, matching `source` in the original bundle.
63
- - **`claims`** — all claims whose `integrityRef` was among the requested refs.
64
- Each claim is returned in its current form as the producer knows it.
85
+ - **`claims`** — all claims that matched the requested refs (by id, currentIntegrityRef,
86
+ or anchor value as described above). When a ref matched multiple claims, all are
87
+ included. Each claim is returned in its current form as the producer knows it.
65
88
  - **`evidence`** and **`events`** — the delta since issuance where the producer
66
89
  can supply it: new evidence items, new verification events, revocation events,
67
90
  dispute events. Producers that do not track a delta MAY return the full current
@@ -69,20 +92,35 @@ block. The bundle carries:
69
92
  means it was never present in the original bundle.
70
93
  - **`authorityTrace`** — current authority traces relevant to the matched claims,
71
94
  including any that have been revoked since the original bundle was issued.
72
- - **`metadata`** — a free-form object on the bundle. This profile defines four
95
+ - **`metadata`** — a free-form object on the bundle. This profile defines five
73
96
  reserved keys within `metadata`:
74
97
 
75
98
  | Key | Type | Required | Meaning |
76
99
  |---|---|---|---|
77
100
  | `respondedAt` | ISO 8601 string | yes | The timestamp at which the producer assembled this response. |
78
- | `statusFunctionVersion` | string | yes | The status function version active at the producer at response time. |
101
+ | `statusFunctionVersion` | string | yes | The status function version the producer's current evaluation pipeline uses — i.e. the value the implementation exports (e.g. `statusFunctionVersion` from the reference implementation), independent of any version values stored inside served bundles. |
79
102
  | `requestedRefs` | string[] | yes | The full list of refs from the request, in order. |
80
- | `unknownRefs` | string[] | yes | Refs from the request that the producer does not recognise. Must be present even if empty. |
103
+ | `unknownRefs` | string[] | yes | Refs from the request that matched nothing. Must be present even if empty. |
104
+ | `evaluatedAt` | `"response"` \| `"generation"` | no | When omitted or `"response"`, the producer evaluated claims dynamically at response time. `"generation"` indicates the producer cannot evaluate dynamically (e.g. static file serving) and the `statusFunctionVersion` reflects the version recorded when the bundle was originally generated. |
81
105
 
82
106
  Unknown refs are reported in `unknownRefs` honestly. A producer MUST NOT silently
83
107
  omit a ref it does not recognise; it MUST include it in `unknownRefs` so the
84
108
  receiver can distinguish "no changes" from "not found."
85
109
 
110
+ **`statusFunctionVersion` source:** producers report the version their current
111
+ evaluation pipeline uses — the value their implementation exports at response time.
112
+ This is independent of any `statusFunctionVersion` fields stored inside the claims
113
+ or events within served bundles. Producers that serve static bundles without
114
+ dynamic evaluation MUST report the version that was active at generation time and
115
+ MUST set `evaluatedAt: "generation"` in metadata.
116
+
117
+ **`integrityAnchor` shape:** integrity anchor objects in responses follow the
118
+ `integrityAnchor` definition in `schemas/claim.schema.json` (`$defs/integrityAnchor`)
119
+ and `schemas/trust-bundle.schema.json` (`$defs/integrityAnchor`). Required fields
120
+ are `id`, `kind`, `algorithm`, `value`, and `sourceRef`. On claims the anchor is
121
+ carried as `currentIntegrityAnchor`; on authority-trace and evidence records it
122
+ is carried as `integrityAnchor`.
123
+
86
124
  ---
87
125
 
88
126
  ## Assurance levels
@@ -150,3 +188,19 @@ unsigned responses are valid.
150
188
  specified here.
151
189
  - **Key management.** Public-key distribution, rotation, and revocation are out
152
190
  of scope. See the ADR 0004 backlog note above.
191
+
192
+ ---
193
+
194
+ ## Changelog
195
+
196
+ **Amended after first independent implementation — hachure.org site function.**
197
+ Five ambiguities resolved: (1) corrected claim integrity field names to schema
198
+ truth (`currentIntegrityRef`, `currentIntegrityAnchor`) and noted authority-trace
199
+ and evidence fields (`integrityRef`, `integrityAnchor`); (2) defined ref resolution
200
+ semantics — producers MUST support id and currentIntegrityRef lookups, MAY support
201
+ anchor-value lookups; (3) clarified multi-match: a ref matching multiple claims
202
+ returns all of them, unknown only when nothing matches; (4) clarified
203
+ `statusFunctionVersion` source as the producer's current pipeline export, added
204
+ optional `evaluatedAt` metadata key (`"response"` | `"generation"`) for static
205
+ producers; (5) added normative cross-reference to `integrityAnchor` schema
206
+ definition in `schemas/claim.schema.json` and `schemas/trust-bundle.schema.json`.