hachure 0.7.0 → 0.8.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/LICENSE +21 -0
- package/README.md +71 -1
- package/SECURITY.md +180 -0
- package/assurance.md +1 -0
- package/conformance/README.md +12 -0
- package/conformance/manifest.json +71 -0
- package/index.mjs +22 -1
- package/interop-in-toto.md +1 -0
- package/merge.md +43 -13
- package/package.json +3 -2
- package/schemas/claim.schema.json +2 -2
- package/schemas/derivation-rule.schema.json +2 -2
- package/schemas/evidence.schema.json +2 -2
- package/schemas/inquiry-record.schema.json +2 -2
- package/schemas/trust-bundle.schema.json +2 -2
- package/schemas/trust-report.schema.json +2 -2
- package/schemas/verification-event.schema.json +2 -2
- package/schemas/verification-policy.schema.json +2 -2
- package/status-function.md +1 -0
- package/verification-endpoint.md +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kontour AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -86,6 +86,19 @@ every bundle valid at `schemaVersion` `3` remains valid; only the deriver
|
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
+
## Conformance language
|
|
90
|
+
|
|
91
|
+
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
|
|
92
|
+
**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this
|
|
93
|
+
document and in every other normative document in this repository
|
|
94
|
+
(`merge.md`, `assurance.md`, `verification-endpoint.md`,
|
|
95
|
+
`status-function.md`, `interop-in-toto.md`, `SECURITY.md`) are to be
|
|
96
|
+
interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)
|
|
97
|
+
and clarified by [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) (BCP 14),
|
|
98
|
+
only when they appear in all capitals, as shown here.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
89
102
|
## Scope: core record shapes
|
|
90
103
|
|
|
91
104
|
This specification covers the following record types. Each is a first-class concept
|
|
@@ -260,6 +273,42 @@ a profile requires no changes to core record shapes or the status function.
|
|
|
260
273
|
|
|
261
274
|
---
|
|
262
275
|
|
|
276
|
+
## Relationship to W3C Verifiable Credentials
|
|
277
|
+
|
|
278
|
+
Hachure and the [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/)
|
|
279
|
+
data model both represent claims-with-evidence, and it is a fair question why
|
|
280
|
+
this format does not simply build on VC instead of defining its own record
|
|
281
|
+
shapes.
|
|
282
|
+
|
|
283
|
+
**The short answer:** DID-based issuer identity is the dominant convention
|
|
284
|
+
in the VC ecosystem — a resolvable, typically key-based identifier scheme —
|
|
285
|
+
though the VC data model itself permits any URL as an issuer identifier.
|
|
286
|
+
Hachure treats signing as an opt-in [Assurance](assurance.md) dial (L0
|
|
287
|
+
unsigned by default, L1/L2 signed on request), not a precondition for a
|
|
288
|
+
record to exist.
|
|
289
|
+
Requiring DIDs for `producerId` ([merge.md](merge.md) §2) would collapse that
|
|
290
|
+
layered design into "every producer needs key infrastructure just to be
|
|
291
|
+
namespaced for merge" — a strictly higher bar than merge, or basic claim
|
|
292
|
+
authorship, actually needs. `producerId` is deliberately at the same trust
|
|
293
|
+
level as the existing `source` field: free-text, unsigned, always available,
|
|
294
|
+
upgradable to a cryptographically verifiable identity only when a consumer's
|
|
295
|
+
policy requires it.
|
|
296
|
+
|
|
297
|
+
This is not a rejection of DIDs or VCs — an implementation that wants
|
|
298
|
+
DID-backed producer identity can express it today via Assurance L1/L2's OIDC-
|
|
299
|
+
or held-key-backed signing (`assurance.md` §"Identity presentation"), and
|
|
300
|
+
nothing here prevents a future profile from mapping Hachure records into a VC
|
|
301
|
+
envelope for interop with VC-native ecosystems. It is a statement that Hachure
|
|
302
|
+
does not *require* DID infrastructure just to produce a valid, useful record —
|
|
303
|
+
consistent with the "signing is a dial, not a gate" principle that runs
|
|
304
|
+
through the whole Assurance profile.
|
|
305
|
+
|
|
306
|
+
See [merge.md](merge.md) §10 "Prior art" for the fuller technical rationale,
|
|
307
|
+
and [assurance.md](assurance.md) for how signed identity is layered on top
|
|
308
|
+
when a consumer needs it.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
263
312
|
## Out of scope: future extension profiles
|
|
264
313
|
|
|
265
314
|
The following producer domains are explicitly out of scope for this core specification.
|
|
@@ -286,7 +335,28 @@ fixed `now`. The test at `tests/spec-conformance.test.ts` loads every test vecto
|
|
|
286
335
|
asserts that the reference implementation derives the expected statuses, making this
|
|
287
336
|
specification executable.
|
|
288
337
|
|
|
289
|
-
See [conformance/README.md](conformance/README.md) for the test vector inventory
|
|
338
|
+
See [conformance/README.md](conformance/README.md) for the test vector inventory, and
|
|
339
|
+
[conformance/manifest.json](conformance/manifest.json) (also exported as
|
|
340
|
+
`conformanceManifest`) for a machine-readable index of what an implementation
|
|
341
|
+
must pass to claim conformance at each level (L1 schema-valid records, L2
|
|
342
|
+
status-derivation vectors, L3 merge vectors).
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Project documents
|
|
347
|
+
|
|
348
|
+
- **[SECURITY.md](SECURITY.md)** — the format's honest trust boundaries:
|
|
349
|
+
source/producerId spoofing, verification-endpoint replay risk, and
|
|
350
|
+
whole-bundle substitution, and which [Assurance](assurance.md) level
|
|
351
|
+
mitigates each.
|
|
352
|
+
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — how to propose a change, when a
|
|
353
|
+
design writeup is expected, and the conformance-vector requirement for
|
|
354
|
+
behavior changes. *(Draft — see the banner in that file.)*
|
|
355
|
+
- **[GOVERNANCE.md](GOVERNANCE.md)** — who currently has decision authority,
|
|
356
|
+
and what "neutral governance" is expected to mean when the project moves
|
|
357
|
+
toward it. Expands the "Governance intent" paragraph above; does not
|
|
358
|
+
contradict it. *(Draft — see the banner in that file.)*
|
|
359
|
+
- **[LICENSE](LICENSE)** — MIT, matching `package.json`'s `"license"` field.
|
|
290
360
|
|
|
291
361
|
---
|
|
292
362
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Security Considerations
|
|
2
|
+
|
|
3
|
+
This document describes the trust boundaries of the Hachure core format and
|
|
4
|
+
its optional profiles, and points at the mechanism each risk is mitigated by.
|
|
5
|
+
It is a description of the format's own honest limits, not a claim that these
|
|
6
|
+
risks are solved by default — most of them are solved only when a consumer
|
|
7
|
+
opts into a higher [Assurance](assurance.md) level.
|
|
8
|
+
|
|
9
|
+
Reporting a vulnerability in the schemas, conformance vectors, or prose in
|
|
10
|
+
this repository: open an issue at
|
|
11
|
+
[hachure-org/spec](https://github.com/hachure-org/spec/issues). This repo
|
|
12
|
+
ships specification artifacts (JSON Schemas, test vectors, markdown), not
|
|
13
|
+
runtime code; there is no server or service to report an exploit against
|
|
14
|
+
here. Vulnerabilities in the reference implementation (`@kontourai/surface`)
|
|
15
|
+
should be reported in that repository.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Threat model summary
|
|
20
|
+
|
|
21
|
+
The core format (schemas, `merge.md`, `status-function.md`) makes **no
|
|
22
|
+
cryptographic guarantee by default.** Every TrustBundle is valid — and fully
|
|
23
|
+
conformant — with zero signatures. This is a deliberate design choice
|
|
24
|
+
(`assurance.md` §"Principle: signing is a dial, not a gate"), not an
|
|
25
|
+
oversight, but it means a receiver's default trust posture for an unsigned
|
|
26
|
+
bundle is "as trustworthy as the channel it arrived over," never higher.
|
|
27
|
+
|
|
28
|
+
The three risk classes below are the direct, honest consequences of that
|
|
29
|
+
default. All three share the same mitigation dial: [assurance.md](assurance.md)'s
|
|
30
|
+
L0/L1/L2 levels. **Signing is opt-in, not a precondition for validity** — a
|
|
31
|
+
consumer that needs a mitigation below must pull the dial itself, by
|
|
32
|
+
requiring L1/L2 in its `VerificationPolicy`; the format does not do it for
|
|
33
|
+
them.
|
|
34
|
+
|
|
35
|
+
### 1. Source / producerId spoofing (L0)
|
|
36
|
+
|
|
37
|
+
`TrustBundle.source` (`schemas/trust-bundle.schema.json`) is free text, and
|
|
38
|
+
the optional `TrustBundle.producerId` (`merge.md` §2) is an unsigned string.
|
|
39
|
+
Both are **self-asserted**: nothing in the core format cryptographically
|
|
40
|
+
binds either field to the system that actually produced the bundle. A
|
|
41
|
+
receiver of an unsigned bundle has no guarantee that the `source`/`producerId`
|
|
42
|
+
values are accurate — only that they are what the sender chose to write down.
|
|
43
|
+
Below Assurance L1, this is trust-on-first-use: the receiver's confidence in
|
|
44
|
+
producer identity comes entirely from the trustworthiness of the transport
|
|
45
|
+
channel (e.g. an authenticated HTTPS connection to a known host), not from
|
|
46
|
+
anything inside the bundle itself.
|
|
47
|
+
|
|
48
|
+
`producerId` was introduced (`merge.md` §2) specifically to give merge a
|
|
49
|
+
stable, comparable identity across runs — it is explicitly documented there
|
|
50
|
+
as carrying "no cryptographic weight," an L0 fact in Assurance-profile terms.
|
|
51
|
+
Reusing it as an authorization or access-control signal without an L1/L2
|
|
52
|
+
signature over it is a misuse of the field, not a supported use case.
|
|
53
|
+
|
|
54
|
+
**Mitigation:** [assurance.md](assurance.md) L1 (OIDC-backed identity-signed)
|
|
55
|
+
or L2 (org-held-key-signed) records bind a verifiable identity to the record
|
|
56
|
+
via a DSSE envelope. A consumer that needs verified producer identity should
|
|
57
|
+
require L1 or L2 in its `VerificationPolicy`'s acceptance criteria and treat
|
|
58
|
+
a below-threshold bundle as a transparency gap (`assurance.md` §"Consumer
|
|
59
|
+
policy"), not silently accept the self-asserted `source`/`producerId` values
|
|
60
|
+
as authoritative.
|
|
61
|
+
|
|
62
|
+
### 2. Replay in the verification-endpoint profile
|
|
63
|
+
|
|
64
|
+
[verification-endpoint.md](verification-endpoint.md) defines a pull-based
|
|
65
|
+
delta-fetch channel (`GET/POST .well-known/hachure/verify`) for a receiver to
|
|
66
|
+
ask a producer "what has changed since this bundle was issued?" As of this
|
|
67
|
+
writing, that profile defines no nonce, freshness token, or monotonic cursor
|
|
68
|
+
in either the request or the response. A response is authenticated only by
|
|
69
|
+
whatever transport-level authentication the producer chooses to require
|
|
70
|
+
(profile text: "Producers MAY require authentication before serving a
|
|
71
|
+
response") and, optionally, by a DSSE signature over the response bundle
|
|
72
|
+
(`verification-endpoint.md` §"Assurance levels").
|
|
73
|
+
|
|
74
|
+
This means: an unsigned verification-endpoint response carries no
|
|
75
|
+
cryptographic freshness guarantee. A response captured and replayed later by
|
|
76
|
+
a party sitting on the transport path (or by a compromised intermediate
|
|
77
|
+
cache) is not distinguishable, at the profile level, from a fresh response —
|
|
78
|
+
the profile's own `respondedAt` metadata field is producer-asserted, not
|
|
79
|
+
verified. This is named here as an explicit, currently-unmitigated gap in the
|
|
80
|
+
profile, not as a solved problem being restated: no existing
|
|
81
|
+
freshness-handling language exists in `verification-endpoint.md` today, and
|
|
82
|
+
this document does not silently invent one.
|
|
83
|
+
|
|
84
|
+
**Mitigation:** treat every verification-endpoint response — signed or not —
|
|
85
|
+
as advisory testimony, per the profile's own framing ("It is testimony with a
|
|
86
|
+
timestamp... It is never a verdict the receiver is expected to obey,"
|
|
87
|
+
`verification-endpoint.md` §"Problem"). Receivers that need replay resistance
|
|
88
|
+
should (a) require a signed response (Assurance L1/L2) *and* (b) layer their
|
|
89
|
+
own freshness handling on top — for example, rejecting a response whose
|
|
90
|
+
`respondedAt` is older than a receiver-chosen staleness bound, or pinning
|
|
91
|
+
expected `respondedAt` monotonicity per producer host — since the core
|
|
92
|
+
profile does not provide either mechanism itself.
|
|
93
|
+
|
|
94
|
+
### 3. Whole-bundle substitution
|
|
95
|
+
|
|
96
|
+
A TrustBundle transmitted without a DSSE envelope (Assurance L0, the default)
|
|
97
|
+
has no integrity protection against wholesale substitution in transit or at
|
|
98
|
+
rest. An attacker or compromised intermediary who can replace the bytes of an
|
|
99
|
+
unsigned bundle — swap it for a different bundle, an older bundle, or a
|
|
100
|
+
bundle from a different producer — is undetectable by the receiver using
|
|
101
|
+
only the core format's own mechanisms. Nothing in `schemas/*.schema.json` or
|
|
102
|
+
`status-function.md` provides tamper-evidence; those layers assume the bytes
|
|
103
|
+
they operate on are already the intended bytes.
|
|
104
|
+
|
|
105
|
+
**Mitigation:** Assurance L1/L2 DSSE signing
|
|
106
|
+
([interop-in-toto.md](interop-in-toto.md)) wraps the serialized bundle in a
|
|
107
|
+
signed envelope; a receiver that verifies the signature before parsing the
|
|
108
|
+
bundle detects substitution (the signature will not verify over substituted
|
|
109
|
+
bytes). This is the same mechanism item 1's mitigation depends on — signing
|
|
110
|
+
the bundle simultaneously binds its producer identity and its content
|
|
111
|
+
integrity. A receiver that only checks `source`/`producerId` (item 1) without
|
|
112
|
+
also verifying the envelope (item 3) has closed neither gap: an attacker who
|
|
113
|
+
can substitute the bundle can substitute the self-asserted `source` field
|
|
114
|
+
along with it.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Integrity anchors and canonicalization
|
|
119
|
+
|
|
120
|
+
Bundle and claim records carry optional `integrityAnchor` objects
|
|
121
|
+
(`schemas/claim.schema.json` `$defs/integrityAnchor`,
|
|
122
|
+
`schemas/trust-bundle.schema.json` `$defs/integrityAnchor`) with a `kind`
|
|
123
|
+
enum that includes `"hash"`. When an implementation computes a `"hash"`-kind
|
|
124
|
+
integrity anchor over a bundle or record — or independently verifies one — it
|
|
125
|
+
**MUST** canonicalize the JSON with [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785)
|
|
126
|
+
(the JSON Canonicalization Scheme, JCS) before hashing. This is the same
|
|
127
|
+
canonicalization primitive `merge.md` §6 now ratifies as normative for its
|
|
128
|
+
deterministic tie-break rule (see `merge.md` §6/§9) — one canonicalization
|
|
129
|
+
decision for the whole format, not two independently-evolving ones.
|
|
130
|
+
Canonicalization MUST be RFC 8785 (JCS) — full stop, with no
|
|
131
|
+
sorted-key-`JSON.stringify` shortcut carve-out. Two implementations that both
|
|
132
|
+
claim RFC 8785 compliance and hash the same logical content MUST produce the
|
|
133
|
+
same digest.
|
|
134
|
+
|
|
135
|
+
> **Note (informative, not normative):** producing byte-identical RFC 8785
|
|
136
|
+
> output across languages has real cross-language pitfalls implementers
|
|
137
|
+
> should verify against, not assume away. (a) Non-ASCII string escaping —
|
|
138
|
+
> RFC 8785 (via RFC 8259) requires strings to contain literal UTF-8
|
|
139
|
+
> characters, never `\uXXXX` escapes, but several languages' default JSON
|
|
140
|
+
> serializers `\u`-escape non-ASCII by default (e.g. Python's `json.dumps`'s
|
|
141
|
+
> `ensure_ascii=True` default) and must be reconfigured to emit literal
|
|
142
|
+
> UTF-8. (b) Number serialization MUST follow the ECMAScript
|
|
143
|
+
> `Number::toString` algorithm (RFC 8785 §3.2.2.3) — native to JavaScript
|
|
144
|
+
> engines, but other languages need a compliant implementation of that exact
|
|
145
|
+
> algorithm, not their own default float-to-string routine. (c) Property
|
|
146
|
+
> sort order is by UTF-16 code unit value (RFC 8785 §3.2.3), not codepoint,
|
|
147
|
+
> byte, or locale-collation order. RFC 8785 §3.1 also does not apply Unicode
|
|
148
|
+
> normalization — strings are canonicalized exactly as they already are in
|
|
149
|
+
> memory, so two visually-identical strings that differ only in Unicode
|
|
150
|
+
> normalization form produce different canonical bytes. A hand-rolled
|
|
151
|
+
> sorted-key `JSON.stringify` can silently diverge from RFC 8785 on any of
|
|
152
|
+
> these points; there is no shortcut that is safe to assume equivalent
|
|
153
|
+
> without verifying against a conformant JCS implementation.
|
|
154
|
+
|
|
155
|
+
An integrity anchor is only as trustworthy as the channel it arrived over
|
|
156
|
+
unless it is itself covered by an Assurance L1/L2 signature (item 3 above) —
|
|
157
|
+
a `"hash"` anchor on an otherwise-unsigned L0 bundle is a self-asserted
|
|
158
|
+
checksum, useful for detecting accidental corruption, not for detecting a
|
|
159
|
+
deliberate adversary who can also recompute the hash over substituted
|
|
160
|
+
content.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Summary: the mitigation is one dial, not three
|
|
165
|
+
|
|
166
|
+
All three risk classes above, and the integrity-anchor guidance, resolve to
|
|
167
|
+
the same answer: **[assurance.md](assurance.md)'s L0/L1/L2 levels are the
|
|
168
|
+
dial a consumer pulls when any of this matters.** L0 (the default, unsigned)
|
|
169
|
+
carries none of these guarantees and is not pretending to. L1/L2 (DSSE
|
|
170
|
+
signing, per [interop-in-toto.md](interop-in-toto.md)) closes the producer-identity
|
|
171
|
+
and bundle-substitution gaps together, because they are the same mechanism.
|
|
172
|
+
The verification-endpoint replay gap additionally needs receiver-side
|
|
173
|
+
freshness handling that the format does not currently specify — named here
|
|
174
|
+
explicitly rather than left implicit.
|
|
175
|
+
|
|
176
|
+
A consumer that needs any of these properties expresses that need as a
|
|
177
|
+
`VerificationPolicy` acceptance criterion requiring a minimum assurance
|
|
178
|
+
level; a bundle that falls short is a transparency gap for a human or policy
|
|
179
|
+
layer to act on (`assurance.md` §"Consumer policy"), never a silent
|
|
180
|
+
downgrade.
|
package/assurance.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
**Status:** draft
|
|
5
5
|
**Namespace:** `hachure.org/v1`
|
|
6
6
|
**Depends on:** core record shapes, [interop-in-toto.md](interop-in-toto.md), [verification-endpoint.md](verification-endpoint.md)
|
|
7
|
+
**Conformance language:** MUST/SHOULD/MAY keywords in this document are to be interpreted per RFC 2119/BCP 14, as defined in [README.md's Conformance language section](README.md#conformance-language).
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
package/conformance/README.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
This directory contains input bundles and expected per-claim statuses that make
|
|
4
4
|
the [Status Derivation specification](../status-function.md) executable.
|
|
5
5
|
|
|
6
|
+
**Machine-readable conformance manifest:** [`manifest.json`](manifest.json)
|
|
7
|
+
(also exported as `conformanceManifest` from the package root) is a
|
|
8
|
+
structured index of what an implementation must pass to claim conformance at
|
|
9
|
+
each level (L1 schema-valid records, L2 status-derivation vectors, L3 merge
|
|
10
|
+
vectors) — distinct from, and more structured than, the raw vector inventory
|
|
11
|
+
below. Read it, or run:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { conformanceManifest } from 'hachure';
|
|
15
|
+
console.log(conformanceManifest.levels);
|
|
16
|
+
```
|
|
17
|
+
|
|
6
18
|
Each test vector is a JSON file with an `input` (a valid TrustBundle) and an `expect`
|
|
7
19
|
object listing expected per-claim statuses at a fixed `now` timestamp. The test at
|
|
8
20
|
`tests/spec-conformance.test.ts` loads every test vector and asserts that the reference
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaComment": "Machine-readable conformance manifest for the Hachure trust format. Not a JSON Schema itself — a structured index of what an implementation must pass to claim conformance at each level. Additive to, not a replacement for, index.mjs's `testVectors` export (which lists raw vectors, not levels/pass-criteria).",
|
|
3
|
+
"manifestVersion": 1,
|
|
4
|
+
"appliesTo": {
|
|
5
|
+
"schemaVersion": [2, 3, 4],
|
|
6
|
+
"statusFunctionVersion": "2"
|
|
7
|
+
},
|
|
8
|
+
"levels": [
|
|
9
|
+
{
|
|
10
|
+
"level": "L1",
|
|
11
|
+
"name": "Schema-valid records",
|
|
12
|
+
"description": "Every core record type an implementation emits or accepts validates against the corresponding normative JSON Schema in schemas/. This is the floor: an implementation that cannot produce or consume schema-valid records cannot claim any higher level.",
|
|
13
|
+
"requires": [],
|
|
14
|
+
"satisfiedBy": {
|
|
15
|
+
"kind": "schema-validation",
|
|
16
|
+
"schemaDir": "schemas",
|
|
17
|
+
"schemaFiles": [
|
|
18
|
+
"claim.schema.json",
|
|
19
|
+
"derivation-rule.schema.json",
|
|
20
|
+
"evidence.schema.json",
|
|
21
|
+
"inquiry-record.schema.json",
|
|
22
|
+
"trust-bundle.schema.json",
|
|
23
|
+
"trust-report.schema.json",
|
|
24
|
+
"verification-event.schema.json",
|
|
25
|
+
"verification-policy.schema.json"
|
|
26
|
+
],
|
|
27
|
+
"howToRun": "Validate your implementation's own TrustBundle output (and any other emitted record types) against schemas/trust-bundle.schema.json with all sibling schemas registered for $ref resolution, using an Ajv (draft 2020-12) instance or equivalent."
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"level": "L2",
|
|
32
|
+
"name": "Status derivation vectors",
|
|
33
|
+
"description": "The implementation's status-derivation function (status-function.md) produces the expected per-claim status for every single-bundle conformance vector in this directory, at the vector's fixed `now`.",
|
|
34
|
+
"requires": ["L1"],
|
|
35
|
+
"satisfiedBy": {
|
|
36
|
+
"kind": "test-vectors",
|
|
37
|
+
"vectorDir": "conformance",
|
|
38
|
+
"vectorFilePattern": "sf-*.json",
|
|
39
|
+
"vectorCount": 8,
|
|
40
|
+
"specRef": "status-function.md",
|
|
41
|
+
"howToRun": "For each vector file, call your status-derivation function with vector.input and new Date(vector.now), then assert the derived status for every claim id matches vector.expect.statusByClaimId. All vectors must pass for the statusFunctionVersion declared in appliesTo.statusFunctionVersion."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"level": "L3",
|
|
46
|
+
"name": "Multi-producer merge vectors",
|
|
47
|
+
"description": "The implementation's merge function (merge.md) produces the expected merged claim-id set, collision set, and per-claim statuses on the merged bundle for every merge conformance vector, and the merge result is identical regardless of input bundle order (merge.md §6 determinism rule).",
|
|
48
|
+
"requires": ["L1", "L2"],
|
|
49
|
+
"satisfiedBy": {
|
|
50
|
+
"kind": "test-vectors",
|
|
51
|
+
"vectorDir": "conformance/merge",
|
|
52
|
+
"vectorFilePattern": "merge-*.json",
|
|
53
|
+
"vectorCount": 4,
|
|
54
|
+
"specRef": "merge.md",
|
|
55
|
+
"howToRun": "For each vector file, call mergeBundles/mergeBundlesDetailed on vector.inputs (and on every permutation of vector.inputs, to prove order-independence per merge.md §6), then assert the merged claim ids, collisions, and per-claim statuses (via your L2 status-derivation function on the merged bundle) match vector.expect."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"canonicalization": {
|
|
60
|
+
"primitive": "RFC 8785 (JCS)",
|
|
61
|
+
"appliesTo": [
|
|
62
|
+
"merge.md §6 deterministic tie-break rule",
|
|
63
|
+
"\"hash\"-kind integrityAnchor computation (SECURITY.md)"
|
|
64
|
+
],
|
|
65
|
+
"status": "ratified"
|
|
66
|
+
},
|
|
67
|
+
"notes": [
|
|
68
|
+
"This manifest describes what passing looks like; it does not itself execute anything. hachure-org/spec ships schemas, prose, and vector fixtures — it does not depend on @kontourai/surface, so this repo's own `npm test` validates vector shape and Ajv-validates vector `input`/`inputs[]` fields against trust-bundle.schema.json, but does not run status/merge derivation itself. Algorithmic conformance (L2/L3) is proven by an implementation actually running its own deriveClaimStatus/mergeBundles against these vectors, per merge.md's \"Reference implementation notes\" table.",
|
|
69
|
+
"L2 and L3 are independent axes in principle (an implementation could theoretically support merge without an L1-conformant single-bundle deriver), but requires:['L1','L2'] on L3 reflects that merge.md's own conformance loop (merge.md's Reference implementation notes table) evaluates merged-bundle status via the same deriveClaimStatus function L2 exercises — there is no separate merge-only status function to test independently."
|
|
70
|
+
]
|
|
71
|
+
}
|
package/index.mjs
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
* vectors. Each vector has `input`, `expect`, and
|
|
12
12
|
* `now` fields; run them against your implementation
|
|
13
13
|
* to claim conformance.
|
|
14
|
+
* conformanceManifest — structured object describing conformance levels
|
|
15
|
+
* (L1 schema-valid, L2 status vectors, L3 merge
|
|
16
|
+
* vectors), which files satisfy each, and the
|
|
17
|
+
* schemaVersion/statusFunctionVersion it applies
|
|
18
|
+
* to. Parsed from conformance/manifest.json.
|
|
19
|
+
* Distinct from testVectors: this describes what
|
|
20
|
+
* passing means, testVectors is the raw fixtures.
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
23
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
@@ -50,7 +57,9 @@ function loadTestVectors() {
|
|
|
50
57
|
const conformanceDir = join(__dirname, 'conformance');
|
|
51
58
|
const vectors = [];
|
|
52
59
|
for (const file of readdirSync(conformanceDir).sort()) {
|
|
53
|
-
|
|
60
|
+
// manifest.json is structured metadata (see conformanceManifest below),
|
|
61
|
+
// not a { now, input, expect } test vector — excluded here.
|
|
62
|
+
if (!file.endsWith('.json') || file === 'manifest.json') continue;
|
|
54
63
|
const name = basename(file, '.json');
|
|
55
64
|
const vector = JSON.parse(readFileSync(join(conformanceDir, file), 'utf8'));
|
|
56
65
|
vectors.push({ name, vector });
|
|
@@ -59,3 +68,15 @@ function loadTestVectors() {
|
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
export const testVectors = loadTestVectors();
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Conformance manifest — structured levels/requirements, distinct from the
|
|
74
|
+
// raw testVectors list above. See conformance/manifest.json and
|
|
75
|
+
// conformance/README.md for the human-readable pointer.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
function loadConformanceManifest() {
|
|
78
|
+
const manifestPath = join(__dirname, 'conformance', 'manifest.json');
|
|
79
|
+
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const conformanceManifest = loadConformanceManifest();
|
package/interop-in-toto.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Module:** `@kontourai/surface` — `src/interop/in-toto.ts`
|
|
4
4
|
**Public exports:** `toInTotoStatement`, `toDsseEnvelope`, `buildPaeBytes`, `parseDssePayload`
|
|
5
|
+
**Conformance language:** MUST/SHOULD/MAY keywords in this document are to be interpreted per RFC 2119/BCP 14, as defined in [README.md's Conformance language section](README.md#conformance-language).
|
|
5
6
|
|
|
6
7
|
---
|
|
7
8
|
|
package/merge.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**Function:** `mergeBundles(bundles: TrustBundle[]) → TrustBundle` /
|
|
4
4
|
`mergeBundlesDetailed(bundles: TrustBundle[]) → { bundle: TrustBundle; collisions: MergeCollision[] }`
|
|
5
5
|
**Source of truth:** `src/merge.ts`, `src/identity.ts`, `src/canonical.ts` in `@kontourai/surface`
|
|
6
|
+
**Conformance language:** MUST/SHOULD/MAY keywords in this document are to be interpreted per RFC 2119/BCP 14, as defined in [README.md's Conformance language section](README.md#conformance-language).
|
|
6
7
|
|
|
7
8
|
---
|
|
8
9
|
|
|
@@ -194,14 +195,40 @@ content-identical, an implementation MUST:
|
|
|
194
195
|
just against the first-seen one), and report a collision for every
|
|
195
196
|
distinct-content pair.
|
|
196
197
|
2. Choose the *kept* record deterministically from content alone — not from
|
|
197
|
-
array position — using the record whose RFC 8785
|
|
198
|
-
Scheme) serialization sorts lexicographically
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
array position — using the record whose [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785)
|
|
199
|
+
(JSON Canonicalization Scheme, JCS) serialization sorts lexicographically
|
|
200
|
+
first among the distinct contents.
|
|
201
|
+
|
|
202
|
+
**Canonicalization decision (ratified):** RFC 8785/JCS is the normative
|
|
203
|
+
canonicalization primitive for this tie-break rule, and for `"hash"`-kind
|
|
204
|
+
`integrityAnchor` computation bundle-wide (`SECURITY.md` §"Integrity anchors
|
|
205
|
+
and canonicalization" states the identical rule for hashing — one decision,
|
|
206
|
+
cited from both places). This was previously a "target primitive... until
|
|
207
|
+
adopted bundle-wide" hedge; it is now ratified as MUST, not a future
|
|
208
|
+
aspiration: canonicalization MUST be RFC 8785 (JCS) — full stop, with no
|
|
209
|
+
sorted-key-`JSON.stringify` shortcut carve-out. Two implementations that both
|
|
210
|
+
claim RFC 8785 compliance and compare the same distinct contents MUST agree
|
|
211
|
+
on which one sorts first.
|
|
212
|
+
|
|
213
|
+
> **Note (informative, not normative):** producing byte-identical RFC 8785
|
|
214
|
+
> output across languages has real cross-language pitfalls implementers
|
|
215
|
+
> should verify against, not assume away. (a) Non-ASCII string escaping —
|
|
216
|
+
> RFC 8785 (via RFC 8259) requires strings to contain literal UTF-8
|
|
217
|
+
> characters, never `\uXXXX` escapes, but several languages' default JSON
|
|
218
|
+
> serializers `\u`-escape non-ASCII by default (e.g. Python's `json.dumps`'s
|
|
219
|
+
> `ensure_ascii=True` default) and must be reconfigured to emit literal
|
|
220
|
+
> UTF-8. (b) Number serialization MUST follow the ECMAScript
|
|
221
|
+
> `Number::toString` algorithm (RFC 8785 §3.2.2.3) — native to JavaScript
|
|
222
|
+
> engines, but other languages need a compliant implementation of that exact
|
|
223
|
+
> algorithm, not their own default float-to-string routine. (c) Property
|
|
224
|
+
> sort order is by UTF-16 code unit value (RFC 8785 §3.2.3), not codepoint,
|
|
225
|
+
> byte, or locale-collation order. RFC 8785 §3.1 also does not apply Unicode
|
|
226
|
+
> normalization — strings are canonicalized exactly as they already are in
|
|
227
|
+
> memory, so two visually-identical strings that differ only in Unicode
|
|
228
|
+
> normalization form produce different canonical bytes. A hand-rolled
|
|
229
|
+
> sorted-key `JSON.stringify` can silently diverge from RFC 8785 on any of
|
|
230
|
+
> these points; there is no shortcut that is safe to assume equivalent
|
|
231
|
+
> without verifying against a conformant JCS implementation.
|
|
205
232
|
|
|
206
233
|
This makes the merged bundle a pure, order-independent function of the *set*
|
|
207
234
|
of input bundles — the same guarantee `status-function.md` already gives for
|
|
@@ -301,11 +328,14 @@ claim).
|
|
|
301
328
|
Referenced descriptively in §7b/§7c (the `contradiction` gap type already
|
|
302
329
|
exists informally, per `schemas/trust-report.schema.json`'s own
|
|
303
330
|
`$comment`), but this document does not add a schema for them.
|
|
304
|
-
- **
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
(
|
|
308
|
-
|
|
331
|
+
- **A canonicalization *library* shipped by this repo.** RFC 8785/JCS is
|
|
332
|
+
ratified (§6) as the normative canonicalization primitive for both the §6
|
|
333
|
+
tie-break rule and `"hash"`-kind `integrityAnchor` computation
|
|
334
|
+
(`SECURITY.md`) — that decision is now settled, not deferred. What remains
|
|
335
|
+
out of scope here is providing a canonicalization *implementation*:
|
|
336
|
+
`hachure-org/spec` ships schemas and prose, not runtime code: it is the
|
|
337
|
+
reference implementation's (`@kontourai/surface`'s) responsibility to
|
|
338
|
+
implement RFC 8785, not this repo's.
|
|
309
339
|
- **Cryptographic producer identity (DIDs, keys, transparency-log-anchored
|
|
310
340
|
identity).** `producerId` (§2) is deliberately unsigned and unverified.
|
|
311
341
|
Where verifiable producer identity is needed, use Assurance L1/L2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hachure",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"statusFunctionVersion": "2",
|
|
5
5
|
"description": "Hachure — canonical distribution of the open trust format: normative JSON schemas, conformance test vectors, and spec constants.",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"merge.md",
|
|
20
20
|
"interop-in-toto.md",
|
|
21
21
|
"verification-endpoint.md",
|
|
22
|
-
"assurance.md"
|
|
22
|
+
"assurance.md",
|
|
23
|
+
"SECURITY.md"
|
|
23
24
|
],
|
|
24
25
|
"scripts": {
|
|
25
26
|
"test": "node --test test/*.test.mjs"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/claim.schema.json",
|
|
4
|
+
"title": "Hachure Claim",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["id", "subjectType", "subjectId", "surface", "claimType", "fieldOrBehavior", "value", "createdAt", "updatedAt"],
|
|
7
7
|
"properties": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/derivation-rule.schema.json",
|
|
4
|
+
"title": "Hachure DerivationRule",
|
|
5
5
|
"description": "A named, versioned rule composing existing claims into a derived boolean answer (ADR 0003 §5).",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["id", "version", "name", "target", "requirements", "combinator"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/evidence.schema.json",
|
|
4
|
+
"title": "Hachure Evidence",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["id", "claimId", "evidenceType", "method", "sourceRef", "excerptOrSummary", "observedAt", "collectedBy"],
|
|
7
7
|
"properties": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/inquiry-record.schema.json",
|
|
4
|
+
"title": "Hachure InquiryRecord",
|
|
5
5
|
"description": "Append-only record capturing the resolution of an Inquiry against a TrustBundle (ADR 0003 §6).",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["id", "inquiry", "outcome", "resolutionPath", "inputSnapshot", "statusFunctionVersion", "resolvedAt"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/trust-bundle.schema.json",
|
|
4
|
+
"title": "Hachure TrustBundle",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"required": ["schemaVersion", "source", "claims", "evidence", "policies", "events"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/trust-report.schema.json",
|
|
4
|
+
"title": "Hachure TrustReport",
|
|
5
5
|
"$comment": "transparencyGaps, changeRecords, subjectGroups, claimGroupRollups, summary, and evidenceRequirementsByClaimId are intentionally loosely typed pending dedicated normative sub-schemas (no existing schema for TransparencyGap/DerivationChangeRecord/SubjectGroup/ClaimGroupRollup/TrustReportSummary/EvidenceRequirement exists anywhere in the ecosystem today); only the top-level report shape and the pass-through bundle fields are strictly validated.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/verification-event.schema.json",
|
|
4
|
+
"title": "Hachure VerificationEvent",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["id", "claimId", "status", "actor", "method", "evidenceIds", "createdAt"],
|
|
7
7
|
"properties": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "https://hachure.org/schemas/verification-policy.schema.json",
|
|
4
|
+
"title": "Hachure VerificationPolicy",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["id", "claimType", "requiredEvidence", "acceptanceCriteria", "reviewAuthority", "validityRule", "stalenessTriggers", "conflictRules", "impactLevel"],
|
|
7
7
|
"properties": {
|
package/status-function.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**Function:** `status = f(claim, evidence, events, policy, authorityTrace, now)`
|
|
4
4
|
**Version constant:** `statusFunctionVersion` (currently `"2"`)
|
|
5
5
|
**Source of truth:** `src/status.ts` in `@kontourai/surface`
|
|
6
|
+
**Conformance language:** MUST/SHOULD/MAY keywords in this document are to be interpreted per RFC 2119/BCP 14, as defined in [README.md's Conformance language section](README.md#conformance-language).
|
|
6
7
|
|
|
7
8
|
---
|
|
8
9
|
|
package/verification-endpoint.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
**Status:** draft
|
|
5
5
|
**Namespace:** `hachure.org/v1`
|
|
6
6
|
**Depends on:** core record shapes, [status-function.md](status-function.md), [interop-in-toto.md](interop-in-toto.md)
|
|
7
|
+
**Conformance language:** MUST/SHOULD/MAY keywords in this document are to be interpreted per RFC 2119/BCP 14, as defined in [README.md's Conformance language section](README.md#conformance-language).
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|