@synoi/sraid 0.2.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/PROJECTION_SPEC.md +245 -0
- package/README.md +149 -0
- package/SPEC.md +216 -0
- package/dist/attestation.d.ts +92 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +155 -0
- package/dist/attestation.js.map +1 -0
- package/dist/authority.d.ts +351 -0
- package/dist/authority.d.ts.map +1 -0
- package/dist/authority.js +563 -0
- package/dist/authority.js.map +1 -0
- package/dist/canonicalize.d.ts +70 -0
- package/dist/canonicalize.d.ts.map +1 -0
- package/dist/canonicalize.js +151 -0
- package/dist/canonicalize.js.map +1 -0
- package/dist/ed25519.d.ts +26 -0
- package/dist/ed25519.d.ts.map +1 -0
- package/dist/ed25519.js +53 -0
- package/dist/ed25519.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/base64.d.ts +28 -0
- package/dist/internal/base64.d.ts.map +1 -0
- package/dist/internal/base64.js +45 -0
- package/dist/internal/base64.js.map +1 -0
- package/dist/internal/key-cache.d.ts +47 -0
- package/dist/internal/key-cache.d.ts.map +1 -0
- package/dist/internal/key-cache.js +77 -0
- package/dist/internal/key-cache.js.map +1 -0
- package/dist/lineage.d.ts +104 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +0 -0
- package/dist/lineage.js.map +1 -0
- package/dist/mldsa.d.ts +41 -0
- package/dist/mldsa.d.ts.map +1 -0
- package/dist/mldsa.js +119 -0
- package/dist/mldsa.js.map +1 -0
- package/dist/oid.d.ts +109 -0
- package/dist/oid.d.ts.map +1 -0
- package/dist/oid.js +140 -0
- package/dist/oid.js.map +1 -0
- package/dist/sensitivity.d.ts +104 -0
- package/dist/sensitivity.d.ts.map +1 -0
- package/dist/sensitivity.js +117 -0
- package/dist/sensitivity.js.map +1 -0
- package/dist/signature.d.ts +46 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +89 -0
- package/dist/signature.js.map +1 -0
- package/dist/types.d.ts +313 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +88 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +339 -0
- package/dist/validate.js.map +1 -0
- package/package.json +69 -0
- package/src/attestation.ts +204 -0
- package/src/authority.ts +849 -0
- package/src/canonicalize.ts +167 -0
- package/src/ed25519.ts +60 -0
- package/src/index.ts +118 -0
- package/src/internal/base64.ts +44 -0
- package/src/internal/key-cache.ts +79 -0
- package/src/lineage.ts +0 -0
- package/src/mldsa.ts +131 -0
- package/src/oid.ts +146 -0
- package/src/sensitivity.ts +154 -0
- package/src/signature.ts +119 -0
- package/src/types.ts +351 -0
- package/src/validate.ts +402 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SynOI Inc. / Foundation X, LLC
|
|
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.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# SRAID CDRO OID Projection — Normative Specification
|
|
2
|
+
|
|
3
|
+
Status: Normative
|
|
4
|
+
Version: 1.0
|
|
5
|
+
Date: 2026-07-05
|
|
6
|
+
Authority: ADR_019 (L0 Normative Contract Unification), decisions 1-3.
|
|
7
|
+
License: CC0-1.0 (public domain). Non-TypeScript SDKs MUST derive from this
|
|
8
|
+
document; the TypeScript reference implementation is `@synoi/sraid`
|
|
9
|
+
(`src/oid.ts`, `src/canonicalize.ts`).
|
|
10
|
+
|
|
11
|
+
This document is the SINGLE NORMATIVE SOURCE of prose for:
|
|
12
|
+
|
|
13
|
+
1. the CDRO OID content-core projection (the field-set removed before hashing),
|
|
14
|
+
2. the number rule (finite integers only), and
|
|
15
|
+
3. the RFC 8785 (JCS) rules SRAID relies on.
|
|
16
|
+
|
|
17
|
+
Any implementation (TS, Python, Rust, Go, or other) that produces a byte
|
|
18
|
+
string different from the reference for the same input is non-conformant. The
|
|
19
|
+
cross-language ABI is enforced by the conformance vectors generated from the
|
|
20
|
+
reference implementation; this prose exists so a new SDK can be DERIVED from
|
|
21
|
+
one written source instead of re-inventing a divergent copy. There have been
|
|
22
|
+
four divergent projections and two number rules across the stack; this spec
|
|
23
|
+
collapses them to one.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. The OID
|
|
28
|
+
|
|
29
|
+
A CDRO's OID (Object IDentifier) is:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
OID = "sha256:" + lowercase_hex( SHA-256( canonicalize( cdroContentCore(object) ) ) )
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- The hash is SHA-256 (FIPS 180-4). It stays quantum-relevant only via Grover,
|
|
36
|
+
which halves preimage resistance to 2^128 — acceptable.
|
|
37
|
+
- The digest is emitted as exactly 64 lowercase hexadecimal characters.
|
|
38
|
+
- The prefix is the literal ASCII string `sha256:`. It is NEVER truncated. A
|
|
39
|
+
bare hex string with no algorithm prefix is NOT a conformant OID.
|
|
40
|
+
- `canonicalize` is the RFC 8785 subset defined in §3.
|
|
41
|
+
- `cdroContentCore` is the projection defined in §2.
|
|
42
|
+
|
|
43
|
+
The OID is content-addressed: any byte-level change to the canonical content
|
|
44
|
+
core yields a different OID. Signatures are computed over the SAME canonical
|
|
45
|
+
bytes and attach AFTER hashing, so signing (or rotating a signature) never
|
|
46
|
+
changes the OID.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 2. The content-core projection (`cdroContentCore`)
|
|
51
|
+
|
|
52
|
+
`cdroContentCore(object)` returns `object` with EXACTLY the following six
|
|
53
|
+
top-level fields REMOVED, and every other field KEPT unchanged:
|
|
54
|
+
|
|
55
|
+
| Field | Why it is removed |
|
|
56
|
+
| --------------------- | -------------------------------------------------------- |
|
|
57
|
+
| `oid` | The projection OUTPUT; it cannot be an input to itself. |
|
|
58
|
+
| `signature` | Legacy hybrid `SignatureEnvelope`; attaches after hash. |
|
|
59
|
+
| `ml_dsa_signature` | Detached ML-DSA-65 signature; produced by the signer. |
|
|
60
|
+
| `signature_key_id` | Signer-stamped key id; produced by the signer. |
|
|
61
|
+
| `signature_algorithm` | Signer-stamped algorithm id; produced by the signer. |
|
|
62
|
+
| `attestation` | DSSE `AttestationEnvelope`; attaches after hash. |
|
|
63
|
+
|
|
64
|
+
### 2.1 The strip-set is SEMANTIC, not a per-surface enumeration
|
|
65
|
+
|
|
66
|
+
The removed set is defined by ONE rule: **remove every field the signer
|
|
67
|
+
produces AFTER canonicalization, plus the OID output itself.** The six names
|
|
68
|
+
above are the concrete realization of that rule for the current object shape.
|
|
69
|
+
An implementation MUST NOT maintain its own independent hand-listed strip-set;
|
|
70
|
+
it derives the set from this rule and this table. A field that a future signer
|
|
71
|
+
stamps post-hash is added here, in this one place, and nowhere else.
|
|
72
|
+
|
|
73
|
+
### 2.2 Everything else is KEPT and hashed into identity
|
|
74
|
+
|
|
75
|
+
In particular the following are IN the content core and therefore IN the OID:
|
|
76
|
+
|
|
77
|
+
- `gap_version` — IN identity so a protocol downgrade is OID-detectable. A
|
|
78
|
+
receipt claiming `gap_version: "1.0"` cannot be silently re-presented as a
|
|
79
|
+
different protocol version without changing its OID.
|
|
80
|
+
- `supersedes` — IN identity because the SRAID Merkle-DAG "head proves history"
|
|
81
|
+
property requires every lineage edge to be inside the hash. Superseding mints
|
|
82
|
+
a NEW object; it never mutates the superseded object's bytes, so keeping the
|
|
83
|
+
pointer inside identity is safe and makes the lineage edge tamper-evident. An
|
|
84
|
+
edge cannot be added, removed, or re-pointed without changing the OID.
|
|
85
|
+
- `prev`, `links` — L3 lineage edges, same rationale as `supersedes`.
|
|
86
|
+
- `authority` — the L4 authorized-axis block. IN identity so it cannot be
|
|
87
|
+
stripped or swapped without breaking the OID and the signature.
|
|
88
|
+
- `sensitivity` — the coarse, opaque propagating tier. IN identity so a
|
|
89
|
+
classification cannot be silently downgraded.
|
|
90
|
+
- `type`, `sraid_version`, `tenant_id`, `created_at_ms`, `created_by`, `body`,
|
|
91
|
+
and every other content field.
|
|
92
|
+
|
|
93
|
+
### 2.3 Pre- vs post-attestation invariance (the keystone)
|
|
94
|
+
|
|
95
|
+
This is the property the entire "portable, independently verifiable receipt"
|
|
96
|
+
thesis depends on: **`cdroOid(object)` yields the SAME OID whether `object` is
|
|
97
|
+
pre-attestation (no signature fields) or post-attestation (signature fields
|
|
98
|
+
attached).** Because §2 strips the six detached fields, a third party who
|
|
99
|
+
recomputes the OID of a SIGNED receipt gets the byte-identical value the signer
|
|
100
|
+
stamped. A projection that hashes `attestation` (or any of the six) into the
|
|
101
|
+
OID breaks this: the third party recomputes a different OID and wrongly
|
|
102
|
+
concludes a valid receipt was tampered.
|
|
103
|
+
|
|
104
|
+
Reference conformance vector (from `test/oid.test.ts`, `baseCdro`):
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
cdroOid(pre-attestation) == cdroOid(post-attestation)
|
|
108
|
+
== sha256:d1d5d5c51d2d5f80470089ff10b8b642e19fc76db6be298f4f346616528a087a
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
where the object is
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"type": "gap:decision_receipt",
|
|
116
|
+
"sraid_version": "2.0",
|
|
117
|
+
"gap_version": "1.0",
|
|
118
|
+
"tenant_id": "tenant-x",
|
|
119
|
+
"created_at_ms": 1720000000000,
|
|
120
|
+
"created_by": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
121
|
+
"body": { "decision": "allow", "amount_minor": 1299 },
|
|
122
|
+
"authority": { "grant_oid": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "decision": "allow" },
|
|
123
|
+
"supersedes": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The post-attestation form is the same object with `oid`, `attestation`,
|
|
128
|
+
`signature`, `ml_dsa_signature`, `signature_key_id`, and `signature_algorithm`
|
|
129
|
+
added; its `cdroOid` is byte-identical to the pre-attestation form's.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 3. The number rule and the JCS rules (`canonicalize`)
|
|
134
|
+
|
|
135
|
+
`canonicalize(value)` is a STRICT SUBSET of RFC 8785 (JCS). It produces a
|
|
136
|
+
deterministic UTF-8 string, byte-identical across conformant implementations
|
|
137
|
+
for the same input.
|
|
138
|
+
|
|
139
|
+
### 3.1 Number rule — finite integers ONLY (ADR_019 decision 2)
|
|
140
|
+
|
|
141
|
+
A number is legal **iff it is a finite integer.** All of the following are
|
|
142
|
+
REJECTED with a typed error BEFORE any hashing:
|
|
143
|
+
|
|
144
|
+
- `NaN`, `+Infinity`, `-Infinity` (RFC 8785 already forbids non-finite).
|
|
145
|
+
- Any non-integer number (float), e.g. `1.5`, `0.1`, `2e-3`, `9.99`.
|
|
146
|
+
|
|
147
|
+
Rationale: the product is settlement-grade signed receipts. All money is
|
|
148
|
+
represented as integer minor units (e.g. cents); all timestamps are integer
|
|
149
|
+
milliseconds. No conformant SRAID object legitimately carries a float.
|
|
150
|
+
Forbidding floats removes the single hardest RFC 8785 cross-language trap
|
|
151
|
+
(shortest-round-trip float serialization) from a signed byte string, with
|
|
152
|
+
near-zero blast radius.
|
|
153
|
+
|
|
154
|
+
Notes:
|
|
155
|
+
|
|
156
|
+
- `-0` is an integer and serializes as `"0"` (`JSON.stringify(-0) === "0"`).
|
|
157
|
+
There is NO bespoke `-0` special-case in the reference implementation; the
|
|
158
|
+
integer path handles it. Implementations MUST NOT add a separate `-0` branch
|
|
159
|
+
(it is a divergence source and cannot fire on legal input differently).
|
|
160
|
+
- A source token like `56.0` that a JSON parser collapses to the integer `56`
|
|
161
|
+
is legal (it IS the integer 56 after parsing). The rule constrains the parsed
|
|
162
|
+
numeric VALUE, not the source lexeme.
|
|
163
|
+
- Legal integers serialize via the ECMAScript number-to-string of RFC 8785
|
|
164
|
+
§3.2.2.3, which for integers is the plain decimal form (`1000`, `-42`, `0`).
|
|
165
|
+
|
|
166
|
+
### 3.2 JCS rules SRAID relies on (MUST NOT regress)
|
|
167
|
+
|
|
168
|
+
- **Object key ordering:** keys are sorted in ascending UTF-16 code-unit order
|
|
169
|
+
(RFC 8785 §3.2.3 for UTF-16 host languages; this is JS `Array.prototype.sort`
|
|
170
|
+
default order). This is verified byte-for-byte against the RFC 8785 reference
|
|
171
|
+
vectors (`arrays`, `french`, `structures`, `unicode`, `weird`).
|
|
172
|
+
- **String escaping:** minimal JSON string escaping — only the characters JSON
|
|
173
|
+
requires be escaped are escaped, using the shortest form. No locale, no
|
|
174
|
+
Unicode normalization (the input's code points are emitted as-is; e.g. an
|
|
175
|
+
unnormalized `A` + combining ring is NOT normalized).
|
|
176
|
+
- **No whitespace:** separators are bare `,` and `:`; no spaces or newlines.
|
|
177
|
+
- **`null`:** emitted as the literal `null`. Booleans as `true` / `false`.
|
|
178
|
+
- **Arrays:** element order preserved; each element recursively canonicalized.
|
|
179
|
+
|
|
180
|
+
### 3.3 Reject-loud domain (out-of-domain values throw, never coerce)
|
|
181
|
+
|
|
182
|
+
A typed error is thrown, never a silent wrong/invalid form, for:
|
|
183
|
+
|
|
184
|
+
- non-integer / non-finite numbers (§3.1),
|
|
185
|
+
- `undefined` / function / symbol / bigint as any value, INCLUDING an array
|
|
186
|
+
element (which would otherwise produce invalid JSON like `[1,,2]`),
|
|
187
|
+
- a sparse array hole,
|
|
188
|
+
- any object exposing a `toJSON()` method (e.g. `Date`, `Buffer`, decimal
|
|
189
|
+
libraries) — convert to a JSON value (e.g. an ISO string) first.
|
|
190
|
+
|
|
191
|
+
Object properties whose value is `undefined` are OMITTED (matching the JSON
|
|
192
|
+
data model). An `undefined` ARRAY element throws (omitting it would shift
|
|
193
|
+
indices and silently change meaning).
|
|
194
|
+
|
|
195
|
+
### 3.4 Relationship to full RFC 8785
|
|
196
|
+
|
|
197
|
+
SRAID's number domain is a DOCUMENTED NARROWING of RFC 8785: the RFC permits
|
|
198
|
+
non-integer numbers with a defined serialization; SRAID forbids them. SRAID is
|
|
199
|
+
therefore RFC 8785 conformant on ordering and string escaping (proven against
|
|
200
|
+
the reference vectors), and INTENTIONALLY stricter on numbers. A general-purpose
|
|
201
|
+
RFC 8785 float vector (e.g. the reference `values` vector) is expected to be
|
|
202
|
+
REJECTED by a conformant SRAID canonicalizer, not to round-trip a float.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 4. Firewalls — schemes that are NOT this projection
|
|
207
|
+
|
|
208
|
+
Two adjacent schemes look similar and MUST NOT be merged into this normative
|
|
209
|
+
CDRO projection. Confusing them is signature confusion, which is fatal.
|
|
210
|
+
|
|
211
|
+
### 4.1 The v1 flat-scalar receipt projection (legacy, separate)
|
|
212
|
+
|
|
213
|
+
The gateway's legacy v1 flat signer uses a DIFFERENT strip-set (its
|
|
214
|
+
`GAP_SIGNING_EXCLUDED` set additionally excludes `gap_version` and
|
|
215
|
+
`supersedes`, and folds the body to flat scalars). That is a SEPARATE, LEGACY
|
|
216
|
+
signing shape gated behind the `receipt_scheme` discriminator. It is NOT the
|
|
217
|
+
CDRO content core and MUST NOT be used to recompute a CDRO OID. The live v2
|
|
218
|
+
signer uses THIS projection (`@synoi/sraid`'s `cdroContentCore`); the v1 flat
|
|
219
|
+
scheme is retained only for objects minted before v2 and is never confused for
|
|
220
|
+
a content core.
|
|
221
|
+
|
|
222
|
+
### 4.2 The Vault L1 binary-TLV canonical hash (separate identity contract)
|
|
223
|
+
|
|
224
|
+
The Vault L1 `canonical_hash` (a binary TLV encoding, `@synoi/vault`) is a
|
|
225
|
+
DISTINCT, deliberately-frozen identity contract governing a different layer. It
|
|
226
|
+
is explicitly OUT OF SCOPE and firewalled — NOT merged with this JSON/JCS
|
|
227
|
+
projection. A Vault L1 identity and a CDRO OID are computed by different rules
|
|
228
|
+
by design.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 5. Conformance
|
|
233
|
+
|
|
234
|
+
An implementation is conformant iff, for every generated conformance vector:
|
|
235
|
+
|
|
236
|
+
1. its `cdroContentCore` removes exactly the six §2 fields and keeps all others,
|
|
237
|
+
2. `cdroOid(pre-attestation) == cdroOid(post-attestation)` (§2.3),
|
|
238
|
+
3. every float-bearing input is REJECTED (§3.1),
|
|
239
|
+
4. changing `supersedes` or `gap_version` changes the OID (§2.2),
|
|
240
|
+
5. its `canonicalize` matches the RFC 8785 reference ordering/escaping vectors
|
|
241
|
+
byte-for-byte (§3.2).
|
|
242
|
+
|
|
243
|
+
The reference vectors are generated from `@synoi/sraid` and consumed
|
|
244
|
+
byte-for-byte by all SDKs, the gateway signer, and the verifiers in CI. A
|
|
245
|
+
divergent strip-set, number rule, or canonicalize turns a vector red.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @synoi/sraid
|
|
2
|
+
|
|
3
|
+
Reference TypeScript implementation of **SRAID (Self-Routing Addressable
|
|
4
|
+
Identity Data)** — L0 of the SynOI SRAID Stack. MIT-licensed.
|
|
5
|
+
|
|
6
|
+
SRAID defines:
|
|
7
|
+
|
|
8
|
+
- a content-addressed object envelope, the **CDRO** (Canonical Data-Rich Object),
|
|
9
|
+
- a deterministic canonical object serializer used as input to hashing and signing,
|
|
10
|
+
- a content-derived **OID** (`sha256:` followed by 64 hex chars) over the canonical bytes,
|
|
11
|
+
- a hybrid **Ed25519 + ML-DSA-65** signature envelope,
|
|
12
|
+
- an **L3 Merkle-DAG lineage** layer (`prev` edge + typed `links[]`), hashed into the OID so a head id commits its whole reachable history, and
|
|
13
|
+
- a supersession record, the **SRO** (Supersession Record Object), that links a successor CDRO to its predecessor.
|
|
14
|
+
|
|
15
|
+
The three legacy supersession mechanisms — the self-asserted `supersedes`
|
|
16
|
+
string, the standalone SRO, and the `prev` edge — are unified by the lineage
|
|
17
|
+
helpers (`lineageLinks`, `supersededOids`, `latestWins`). `latestWins` resolves
|
|
18
|
+
a set of versions to its single head by following the identity-bound `prev`/
|
|
19
|
+
`links` edges, giving a verifier a **latest-wins / monotone** rule instead of an
|
|
20
|
+
unwitnessed pointer. Given the complete version set, `latestWins` will not name
|
|
21
|
+
a superseded object as head; full rollback-replay resistance additionally
|
|
22
|
+
requires that the superseding object cannot be withheld, which is a Resolver /
|
|
23
|
+
transparency-log property (DESIGN, not yet deployed). The `set_complete` field
|
|
24
|
+
on `LatestWinsResult` is the local signal that lets a caller detect an
|
|
25
|
+
incomplete (potentially withheld) set.
|
|
26
|
+
|
|
27
|
+
Higher layers — Vault/Resolver (L1), Inference Broker/Resonance (L2), GAP (L3)
|
|
28
|
+
— are built on SRAID objects. This package is intentionally tiny and has no
|
|
29
|
+
storage, no HTTP surface, and no governance logic.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @synoi/sraid
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Minimal example
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import {
|
|
41
|
+
canonicalize,
|
|
42
|
+
oidOf,
|
|
43
|
+
verifySignature,
|
|
44
|
+
validateCdro,
|
|
45
|
+
type CDRO,
|
|
46
|
+
type SignatureEnvelope,
|
|
47
|
+
} from '@synoi/sraid'
|
|
48
|
+
import { ed25519 } from '@noble/curves/ed25519'
|
|
49
|
+
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js'
|
|
50
|
+
import { randomBytes } from 'node:crypto'
|
|
51
|
+
|
|
52
|
+
// 1. Build a CDRO body and derive its OID.
|
|
53
|
+
const body = { capability: 'door.unlock', risk_class: 'B' }
|
|
54
|
+
const oid = oidOf(body) // sha256:<64 hex chars>
|
|
55
|
+
|
|
56
|
+
const cdro: CDRO<typeof body> = {
|
|
57
|
+
oid,
|
|
58
|
+
type: 'gap:capability_declaration',
|
|
59
|
+
sraid_version: '2.0',
|
|
60
|
+
tenant_id: 't-home',
|
|
61
|
+
created_at_ms: Date.now(),
|
|
62
|
+
created_by: 'actor:skill:demo',
|
|
63
|
+
body,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(validateCdro(cdro)) // { ok: true, errors: [] }
|
|
67
|
+
|
|
68
|
+
// 2. Sign the canonical bytes with both Ed25519 and ML-DSA-65.
|
|
69
|
+
const canonical = canonicalize(cdro.body)
|
|
70
|
+
const message = new TextEncoder().encode(canonical)
|
|
71
|
+
|
|
72
|
+
const edPriv = new Uint8Array(randomBytes(32))
|
|
73
|
+
const edPub = ed25519.getPublicKey(edPriv)
|
|
74
|
+
const mlKeys = ml_dsa65.keygen(new Uint8Array(randomBytes(32)))
|
|
75
|
+
|
|
76
|
+
const envelope: SignatureEnvelope = {
|
|
77
|
+
ed25519: Buffer.from(ed25519.sign(message, edPriv)).toString('base64'),
|
|
78
|
+
ml_dsa_65: Buffer.from(ml_dsa65.sign(message, mlKeys.secretKey)).toString('base64'),
|
|
79
|
+
signer_kid: 'synoi-demo-2026-05',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Verify.
|
|
83
|
+
const v = verifySignature({
|
|
84
|
+
canonical,
|
|
85
|
+
envelope,
|
|
86
|
+
ed25519_pub: edPub,
|
|
87
|
+
ml_dsa_pub: mlKeys.publicKey,
|
|
88
|
+
})
|
|
89
|
+
console.log(v) // { valid: true, reasons: [] }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Surface
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
canonicalize(value: unknown): string
|
|
96
|
+
oidOf(canonical: unknown): string
|
|
97
|
+
oidOfCanonical(canonical: string | Uint8Array): string
|
|
98
|
+
|
|
99
|
+
verifySignature(args: {
|
|
100
|
+
canonical: string | Uint8Array
|
|
101
|
+
envelope: SignatureEnvelope
|
|
102
|
+
ed25519_pub: Uint8Array
|
|
103
|
+
ml_dsa_pub: Uint8Array
|
|
104
|
+
}): { valid: boolean; reasons: string[] }
|
|
105
|
+
|
|
106
|
+
validateCdro(x: unknown): { ok: boolean; errors: string[] }
|
|
107
|
+
validateSro(x: unknown): { ok: boolean; errors: string[] }
|
|
108
|
+
validateSignatureEnvelope(x: unknown): { ok: boolean; errors: string[] }
|
|
109
|
+
|
|
110
|
+
// Types
|
|
111
|
+
interface CDRO<TBody> { /* envelope */ }
|
|
112
|
+
interface SRO { /* supersession record */ }
|
|
113
|
+
interface SignatureEnvelope { ed25519: string; ml_dsa_65: string; signer_kid: string }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Canonical form
|
|
117
|
+
|
|
118
|
+
The canonical form is recursive JCS-lite JSON:
|
|
119
|
+
|
|
120
|
+
- primitives via `JSON.stringify`,
|
|
121
|
+
- objects emit keys in lexicographic order,
|
|
122
|
+
- `undefined` properties are omitted,
|
|
123
|
+
- arrays preserve order,
|
|
124
|
+
- no whitespace.
|
|
125
|
+
|
|
126
|
+
This canonical form is a wire contract. **Any byte-level change to the
|
|
127
|
+
serializer changes every OID and invalidates every previously-created
|
|
128
|
+
signature.** Treat it as frozen for SRAID v1.0; evolve it only via a new,
|
|
129
|
+
explicitly versioned canonical profile.
|
|
130
|
+
|
|
131
|
+
## OID format
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
OID = "sha256:" + lowercase_hex( sha256( canonicalize(content) ) )
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The hash input is the OBJECT MINUS its own `oid` and `signature` fields, so
|
|
138
|
+
signature rotation does not change the OID.
|
|
139
|
+
|
|
140
|
+
## Why hybrid signatures
|
|
141
|
+
|
|
142
|
+
SRAID objects are signed with both Ed25519 and ML-DSA-65. The classical
|
|
143
|
+
Ed25519 path is fast and ubiquitously verifiable today; the post-quantum
|
|
144
|
+
ML-DSA-65 path future-proofs receipts against quantum attacks. `verifySignature`
|
|
145
|
+
in this package returns `valid: true` only when BOTH succeed.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT — see [`LICENSE`](./LICENSE).
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# SRAID - Self-Routing Addressable Identity Data, Core Object Specification
|
|
2
|
+
|
|
3
|
+
**Core Specification, Version 1.0**
|
|
4
|
+
|
|
5
|
+
> To the extent possible under law, SynOI Inc. has waived all copyright and related rights to this specification under the Creative Commons CC0 1.0 Universal Public Domain Dedication (https://creativecommons.org/publicdomain/zero/1.0/). The reference implementation `@synoi/sraid` is MIT-licensed; the conformance suite `@synoi/conformance` is Apache-2.0-licensed.
|
|
6
|
+
|
|
7
|
+
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Overview
|
|
12
|
+
|
|
13
|
+
SRAID defines a **canonical, content-addressed object** (serialized in a canonical object format) and its **object identifier (OID)**. The goals are:
|
|
14
|
+
|
|
15
|
+
- **Determinism.** The same content yields the same canonical bytes and the same OID on any implementation.
|
|
16
|
+
- **Content addressing.** An object's identity is derived from its content, so identical content deduplicates and any party can recompute and check an OID.
|
|
17
|
+
- **Independent verification.** An object carries its own signatures, so a third party can verify authenticity offline, without contacting the issuer.
|
|
18
|
+
|
|
19
|
+
SRAID Core is intentionally small. It specifies the object envelope, the canonical form, the OID, and the signature requirement. Higher-level concerns (storage, retrieval, access control) are out of scope and are layered above SRAID by other specifications.
|
|
20
|
+
|
|
21
|
+
## 2. The SRAID object
|
|
22
|
+
|
|
23
|
+
A SRAID object (the Canonical Data Record Object, or **CDRO**) is a map with the following members:
|
|
24
|
+
|
|
25
|
+
| Member | Required | Meaning |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `oid` | yes | The object's content identifier (Section 4). `"sha256:" + lowercase_hex(SHA-256(canonical bytes))`. Excluded from the OID input. |
|
|
28
|
+
| `type` | yes | Object type discriminator (a non-empty string, for example `"gap:capability_grant"` or `"sraid:sro"`). |
|
|
29
|
+
| `sraid_version` | yes | SRAID protocol version. The only defined value is the string `"2.0"`. |
|
|
30
|
+
| `tenant_id` | yes | Non-empty string identifying the tenant that owns the object. |
|
|
31
|
+
| `created_at_ms` | yes | Unix-millisecond creation timestamp (a positive integer). |
|
|
32
|
+
| `created_by` | yes | OID or other non-empty string identifying the actor (skill, user, device) that created the object. |
|
|
33
|
+
| `body` | yes | The type-specific payload. May be any JSON value, but the member MUST be present. |
|
|
34
|
+
| `supersedes` | optional | If present, a canonical OID (`sha256:` followed by exactly 64 lowercase hex characters) of the previous version this object supersedes. |
|
|
35
|
+
| `sensitivity` | optional | If present, a **coarse, opaque sensitivity tier** (Section 7): one of `"s0"`, `"s1"`, `"s2"`, `"s3"`, `"s4"` (lowest to highest). It MUST NOT be a literal content category (for example `"phi"` or `"financial"`). Part of the canonical content, so it is hashed into the OID and cannot be silently stripped or downgraded. |
|
|
36
|
+
| `attestation` | optional | If present, a detached **DSSE attestation envelope** (Section 6) carrying one or more signatures over the object's PAE-encoded canonical bytes. This is the preferred signing form. May be absent on draft or unsigned objects. |
|
|
37
|
+
| `signature` | optional | LEGACY. If present, a single detached hybrid signature envelope over the object's bare canonical bytes (Section 6.4). Retained for back-compat; new producers SHOULD emit `attestation` instead. May be absent on draft or unsigned objects. |
|
|
38
|
+
|
|
39
|
+
The `oid`, `attestation`, and `signature` members are excluded from the OID input (Section 4) so that the same content produces the same OID and re-signing never changes identity. All other members are part of the canonical content and therefore contribute to the OID.
|
|
40
|
+
|
|
41
|
+
**Lineage fields are independent.** An object MAY carry `supersedes`, `prev`, and one or more `links[]` simultaneously. `supersedes` asserts a version-replacement relationship (this object is the authoritative successor of the referenced OID); `prev` is the hash-chain backpointer for ordered append; `links[]` are typed semantic edges (L3). Each is validated for canonical-OID format in isolation. The validator does NOT enforce agreement among them (for example it does not require `supersedes === prev`), nor does it resolve or fetch the referenced objects. Higher layers (Vault/Resolver, GAP) are responsible for any cross-field lineage semantics. The signed supersession receipt (SRO, type `sraid:sro`) is distinct from the self-asserted `supersedes` field on a CDRO: the SRO is an independently signed object authored by a governing actor; `supersedes` is a producer-asserted claim inside the envelope. Both fields are format-validated but neither implies the other.
|
|
42
|
+
|
|
43
|
+
## 3. Canonical form
|
|
44
|
+
|
|
45
|
+
SRAID v1.0 defines one normative canonical object format, identified as **`sraid/json`** (strict RFC 8785 JCS):
|
|
46
|
+
|
|
47
|
+
1. Take the value to canonicalize. For OID derivation this is the CDRO with its `oid` and `signature` members removed (Section 4); for signing it is the same value the signer hashed.
|
|
48
|
+
2. Serialize it as JSON deterministically per RFC 8785:
|
|
49
|
+
- **Objects** emit their keys sorted in ascending lexicographic (UTF-16 code-unit) order. Each member is `key:value` with no whitespace; members are joined by a bare `,`.
|
|
50
|
+
- Object members whose value is `undefined` are OMITTED entirely (an absent member and a member set to `undefined` canonicalize identically).
|
|
51
|
+
- **Arrays** preserve their element order; each element is recursively canonicalized and joined by a bare `,`.
|
|
52
|
+
- **Strings** are emitted with standard JSON string escaping.
|
|
53
|
+
- **Numbers** use the ECMAScript number-to-string serialization defined by RFC 8785 §3.2.2.3. An implementation MUST reject (throw an error for) any number value that is `NaN`, positive `Infinity`, or negative `Infinity` — these values are not representable in JSON and MUST NOT be silently coerced to `null` or any other value. The value `-0` MUST serialize as `"0"` (the positive-zero form) per RFC 8785 §3.2.2.3.
|
|
54
|
+
- `null`, `true`, and `false` are emitted literally.
|
|
55
|
+
- No insignificant whitespace appears anywhere; the only separators are bare `,` and `:`.
|
|
56
|
+
3. Encode the result as UTF-8. The resulting byte string is the object's **canonical bytes**.
|
|
57
|
+
|
|
58
|
+
The precise canonicalization is pinned by the test vectors in `@synoi/conformance` (Section 8); an implementation is canonical-correct if and only if it reproduces those vectors.
|
|
59
|
+
|
|
60
|
+
A binary profile, **`sraid/cbor`** (CBOR per RFC 8949 with COSE per RFC 9052 for signatures), is reserved for a future version. An implementation MUST declare which profile it produces; a v1.0 producer MUST produce `sraid/json`.
|
|
61
|
+
|
|
62
|
+
**Serialization format decision (closed, 2026-06-27).** `sraid/json` is the one normative format for v1.0. The choice is settled for this version; it is not under deliberation. Rationale: (a) RFC 8785 JCS specifies a single, deterministic output with no schema dependency, which is required because `body` may be any JSON value (Section 2); (b) the conformance vectors (Section 8) already pin the byte-exact output, establishing a concrete interoperability floor; (c) JSON tooling is universally available across the implementation environments SRAID targets. `sraid/cbor` is explicitly deferred to a post-v1.0 additive binary profile. When introduced, it MUST yield the same OID-bearing content model as `sraid/json` (identical member set and semantics), not a replacement format. A `sraid/cbor` producer is therefore still bound by all CDRO structural rules in Section 2.
|
|
63
|
+
|
|
64
|
+
## 4. The Object Identifier (OID)
|
|
65
|
+
|
|
66
|
+
> The OID is the single normative identifier of SRAID v1.0.
|
|
67
|
+
|
|
68
|
+
The OID is computed over the canonical bytes of the CDRO with its `oid`, `attestation`, and `signature` members excluded from the input (so signing or re-signing never changes the OID):
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
OID = "sha256:" + lowercase_hex( SHA-256( canonical_bytes ) )
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- An OID MUST match the regular expression `^sha256:[0-9a-f]{64}$`.
|
|
75
|
+
- The OID is both the object's content identity and its deduplication key: two CDROs whose members are identical after removing `oid`, `attestation`, and `signature` MUST yield identical canonical bytes and therefore an identical OID.
|
|
76
|
+
- Deduplication, supersession, and any trust or truth decision MUST use the full OID. An implementation MUST NOT substitute a truncation of the hash for these purposes.
|
|
77
|
+
- The 64-hex `sha256:` OID is the sole normative object identifier. The class/schema-prefixed `[4+12]` short form of Section 5 is a non-normative routing prefix only and is never an object identity.
|
|
78
|
+
|
|
79
|
+
SHA-256 provides approximately 128-bit pre-image resistance against a quantum adversary (Grover) and approximately 128-bit collision resistance, so the OID does not require migration for post-quantum reasons. Algorithm agility (a self-describing multihash form) is reserved for a future version and, if introduced, MUST be a distinct, versioned change rather than a silent one.
|
|
80
|
+
|
|
81
|
+
## 5. Routing prefix (non-normative)
|
|
82
|
+
|
|
83
|
+
Implementations MAY derive a short, fixed-size **routing prefix** from an object for type-partitioned placement or fast bucketing. A routing prefix begins with class/schema bytes followed by a prefix of the object's SHA-256.
|
|
84
|
+
|
|
85
|
+
- A routing prefix MUST NOT be used as a security identity, nor for deduplication, supersession, or trust decisions; those use the full OID (Section 4). A routing-prefix collision is a placement event, resolved by comparing full OIDs.
|
|
86
|
+
- The exact byte layout of the routing prefix is not fixed by SRAID v1.0 and is reserved as a separate, non-normative profile.
|
|
87
|
+
|
|
88
|
+
## 6. Attestation (signatures)
|
|
89
|
+
|
|
90
|
+
An object MAY carry a detached **attestation envelope** that authenticates its canonical bytes. SRAID v1.0 defines a **hybrid signature suite** combining a classical and a post-quantum algorithm, BOTH required:
|
|
91
|
+
|
|
92
|
+
- **Ed25519** (RFC 8032), and
|
|
93
|
+
- **ML-DSA-65** (FIPS 204).
|
|
94
|
+
|
|
95
|
+
The preferred form is a **DSSE** (Dead Simple Signing Envelope) JSON-profile envelope in the optional `attestation` member (Section 6.1–6.3). A legacy bare-bytes envelope (the `signature` member) is retained for back-compat only (Section 6.4). The `attestation` and `signature` members are not part of the OID (Section 4), so an object may be re-signed without changing its identity.
|
|
96
|
+
|
|
97
|
+
### 6.1 The DSSE attestation envelope
|
|
98
|
+
|
|
99
|
+
The `attestation` member has three members:
|
|
100
|
+
|
|
101
|
+
- `payloadType` — a non-empty media-type string identifying the payload kind (for example `application/vnd.synoi.sraid+json`, or `application/vnd.in-toto+json` for an in-toto/SLSA supply-chain attestation),
|
|
102
|
+
- `payload` — the canonical UTF-8 string that is attested (the CDRO content core per Section 3–4, i.e. the object minus its `oid`, `attestation`, and `signature` members), and
|
|
103
|
+
- `signatures` — an array of signature entries, each with members `alg` (the algorithm identifier, e.g. `"ed25519"` or `"ml-dsa-65"`), `sig` (the base64-encoded signature bytes), and an optional `keyid` (an opaque string identifying the keypair).
|
|
104
|
+
|
|
105
|
+
### 6.2 Pre-Authentication Encoding (PAE) — type binding
|
|
106
|
+
|
|
107
|
+
Each signature in `attestation.signatures` is computed over the **PAE** of the envelope, NOT over the bare payload:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
PAE(payloadType, payload) =
|
|
111
|
+
"DSSEv1" SP LEN(payloadType) SP payloadType SP LEN(payload) SP payload
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
where `SP` is a single ASCII space (0x20), `LEN(x)` is the ASCII-decimal byte length of `x` in its UTF-8 encoding, and `payloadType` and `payload` are their raw UTF-8 bytes. Because `payloadType` (and both lengths) are inside the signed bytes, the payload TYPE is structurally bound into every signature. A signature minted for one `payloadType` therefore CANNOT verify against an envelope that claims a different `payloadType`, even if the `payload` bytes are identical. This closes the cross-type / confused-deputy hazard inherent in signing bare canonical bytes (where all object kinds are signed identically).
|
|
115
|
+
|
|
116
|
+
### 6.3 The both-required (AND) policy
|
|
117
|
+
|
|
118
|
+
DSSE is, by itself, an OR-of-signatures envelope. SRAID layers a stricter rule on top: a conformant SRAID attestation MUST carry both an `ed25519` and an `ml-dsa-65` entry over the same PAE, and a verifier MUST require BOTH to verify (against the supplied public keys) before the object is considered signed. The presence of additional `signatures` entries with other algorithms is permitted and ignored by a SRAID hybrid verifier. Signatures are base64-encoded (standard alphabet, with `=` padding).
|
|
119
|
+
|
|
120
|
+
### 6.4 Legacy bare-bytes envelope (`signature`) — deprecated
|
|
121
|
+
|
|
122
|
+
For back-compat, an object MAY instead carry the legacy `signature` member with three members `ed25519`, `ml_dsa_65` (both base64-encoded signatures), and `signer_kid` (an opaque non-empty string). In the legacy form both signatures are computed over the bare canonical bytes (the CDRO minus its `oid`, `attestation`, and `signature` members, per Section 4) with NO payload-type binding. A verifier MUST require BOTH signatures to verify. New producers SHOULD NOT emit the legacy form; it lacks the PAE type binding of Section 6.2 and is retained only so objects minted before DSSE adoption keep verifying.
|
|
123
|
+
|
|
124
|
+
### 6.5 Binary profile
|
|
125
|
+
|
|
126
|
+
The `sraid/cbor` binary profile (Section 3) reserves **COSE** (RFC 9052) as the CBOR-profile attestation form. It is reserved, not defined by SRAID v1.0.
|
|
127
|
+
|
|
128
|
+
## 7. Envelope metadata privacy
|
|
129
|
+
|
|
130
|
+
The envelope members other than `body` (notably `type`, `tenant_id`, `created_by`, `created_at_ms`, and `supersedes`) are part of the canonical, signed, NON-encrypted envelope. They are in the clear so that the system can route, deduplicate, and govern an object without decrypting its `body`.
|
|
131
|
+
|
|
132
|
+
Because those members are visible to anyone who can see the object, an implementation SHOULD treat the envelope as a public surface and keep semantically sensitive content inside `body` (which higher layers MAY encrypt) rather than in envelope members. In particular, the `type` discriminator SHOULD identify the structural object kind (for example `sraid:sro`) and SHOULD NOT encode a semantic content category (for example a literal `health` or `financial` label), so that the envelope does not leak the nature of the encrypted `body`.
|
|
133
|
+
|
|
134
|
+
### 7.1 The sensitivity tier — coarse and opaque
|
|
135
|
+
|
|
136
|
+
SRAID v1.0 defines an optional `sensitivity` member: a **coarse, opaque** tier drawn from the ordered, fixed ladder `"s0" < "s1" < "s2" < "s3" < "s4"` (lowest to highest). The tier is OPAQUE by construction. Because the envelope is a public surface, the member MUST NOT carry a literal content category (for example `"phi"`, `"health"`, or `"financial"`): such a label would leak the nature of an encrypted `body` to anyone who can see the object. The mapping from a real-world classification (PHI, PII, secret, …) to a tier is a higher-layer POLICY concern that is kept OFF the wire; the envelope only ever exposes the opaque level. An implementation MUST reject a `sensitivity` value that is not one of the five defined tiers.
|
|
137
|
+
|
|
138
|
+
The member is part of the canonical content (Section 4), so it is hashed into the OID. A producer therefore cannot strip it or downgrade it without changing the OID and invalidating any signature.
|
|
139
|
+
|
|
140
|
+
**Monotone carry-forward (`max`).** Sensitivity propagates monotonically through derivation. When an object is derived from one or more sources (for example a consolidated or summarized memory record built from raw records, linked by `consolidated_from`/`derived_from`), its tier MUST be at least the **maximum** tier among those sources. In other words a summary of higher-tier inputs MUST NOT be assigned a lower tier than its highest input. This is the standard lattice-based information-flow rule (Denning 1976): the join (`max`) of the source labels is the floor for the derived label. The reference implementation provides `sensitivityCarryForward` and `sensitivityMonotoneCheck` for this.
|
|
141
|
+
|
|
142
|
+
> **Note (reserved).** SRAID v1.0 does NOT define a commitment-based selective-disclosure scheme that would CONCEAL the tier itself; the reference implementation carries none. Concealing the opaque tier (so that even the level is hidden) is reserved for a future version (Section 9) and MUST NOT be assumed by a v1.0 implementation. The v1.0 tier is opaque as to CATEGORY, not hidden as to LEVEL.
|
|
143
|
+
|
|
144
|
+
## 8. Delegation chains
|
|
145
|
+
|
|
146
|
+
A **delegation chain** is an ordered sequence of capability grant CDROs of type `gap:capability_grant` (or any type that carries a `body.capability_scopes` array) that traces authority from a trusted root principal down to a leaf grantee. Delegation verification is OFFLINE (verify-only): the verifier makes NO live-revocation or existence claim. Those remain resolver-dependent.
|
|
147
|
+
|
|
148
|
+
### 8.1 Chain shape
|
|
149
|
+
|
|
150
|
+
A chain MUST contain at least one grant. The ordered sequence is `links = [leaf, ancestor_0, ..., root]`, where `links[i]` is the direct child of `links[i+1]` and `links[links.length - 1]` is the terminal root grant. A chain MUST NOT exceed `MAX_DELEGATION_DEPTH = 8` links; an over-depth chain MUST be rejected before any hashing or signature work (cheap DoS guard).
|
|
151
|
+
|
|
152
|
+
### 8.2 OID honesty
|
|
153
|
+
|
|
154
|
+
Every link's `oid` MUST equal `sha256: + lowercase_hex(SHA-256(canonicalize(cdroContentCore(link))))`. A link whose claimed OID does not match the recomputed value MUST be rejected.
|
|
155
|
+
|
|
156
|
+
### 8.3 Per-hop structure
|
|
157
|
+
|
|
158
|
+
For each child `links[i]` under parent `links[i+1]`:
|
|
159
|
+
|
|
160
|
+
**(a) Linkage.** `child.body.granted_by` MUST equal `parent.body.grantee.actor_oid`. A mismatch means the chain is broken and MUST be rejected.
|
|
161
|
+
|
|
162
|
+
**(b) Attenuation.** Every capability in `child.body.capability_scopes` MUST be covered by at least one capability in `parent.body.capability_scopes` (segment-boundary wildcard match, Section 6 of the capability matching spec). A child grant with an **empty `capability_scopes` array MUST be rejected** — an empty scope set is not vacuously attenuated and grants nothing. A child scope that widens beyond all parent scopes (not covered by any parent scope) MUST be rejected.
|
|
163
|
+
|
|
164
|
+
**(c) Expiry monotone narrowing.** A `null` parent expiry is unbounded; a `null` child expiry under a bounded parent is widening and MUST be rejected. When both are non-null, `child.body.expires_at_ms` MUST NOT exceed `parent.body.expires_at_ms`.
|
|
165
|
+
|
|
166
|
+
### 8.4 Hybrid DSSE attestation, every link
|
|
167
|
+
|
|
168
|
+
Every link MUST carry a valid hybrid DSSE attestation envelope (Section 6.1) over `PAE(payloadType, payload)` (Section 6.2) where `payload = canonicalize(cdroContentCore(link))`. Both Ed25519 AND ML-DSA-65 signatures are required (Section 6.3). A missing attestation or an attestation whose payload does not match the link's content core MUST cause the chain to fail.
|
|
169
|
+
|
|
170
|
+
### 8.5 Root anchor (caller-asserted)
|
|
171
|
+
|
|
172
|
+
The terminal link's issuer key MUST deep-equal the caller-supplied `rootPubkeys`. A mismatch means the chain does not anchor to a known trusted root and MUST be rejected.
|
|
173
|
+
|
|
174
|
+
### 8.6 Action coverage (optional)
|
|
175
|
+
|
|
176
|
+
When the verifier is called with an optional `action` string, the leaf grant's `capability_scopes` MUST cover that action (using the same segment-boundary wildcard match as Section 8.3(b)). An uncovered action MUST fail closed (`authorized: false`). When no `action` is supplied, coverage of a specific action is not checked (GATE 5 skipped; `action_checked: false`).
|
|
177
|
+
|
|
178
|
+
### 8.7 No live-state claims
|
|
179
|
+
|
|
180
|
+
Delegation verification MUST NOT assert revocation status or grant existence. Implementations MUST return literal `false` (not a computed boolean) for `revocation_checked`, `not_revoked`, and `existence_checked`. Those remain RESOLVER-DEPENDENT and are out of scope for offline verification.
|
|
181
|
+
|
|
182
|
+
## 9. Conformance
|
|
183
|
+
|
|
184
|
+
An implementation conforms to SRAID Core v1.0 if it:
|
|
185
|
+
|
|
186
|
+
1. Produces `sraid/json` canonical bytes that match the published vectors,
|
|
187
|
+
2. Computes OIDs per Section 4 that match the published vectors, and
|
|
188
|
+
3. Verifies signatures per Section 6.
|
|
189
|
+
|
|
190
|
+
The conformance vectors in `@synoi/conformance` are the normative test of conformance.
|
|
191
|
+
|
|
192
|
+
## 10. Reserved for future versions
|
|
193
|
+
|
|
194
|
+
The following are reserved by name so that independent implementations do not adopt incompatible forms:
|
|
195
|
+
|
|
196
|
+
- **`sraid/cbor`** - a CBOR + COSE binary profile (Section 3). Reserved as a future additive binary profile only. The v1.0 serialization format is settled as `sraid/json` (see Section 3 decision note, 2026-06-27); `sraid/cbor` is not a candidate for v1.0 and MUST NOT be interpreted as an open question for this version.
|
|
197
|
+
- **routing-prefix layout** - the exact byte layout of the non-normative routing prefix (Section 5).
|
|
198
|
+
- **algorithm agility** - a self-describing (multihash-style) OID form and additional signature suites (Sections 4, 6).
|
|
199
|
+
- **sensitivity concealment** - a commitment-based selective-disclosure scheme that CONCEALS the opaque sensitivity tier itself (Section 7.1). The coarse, opaque tier member and its monotone `max` carry-forward ARE defined in v1.0 (Section 7.1); only the mechanism to hide the level is reserved.
|
|
200
|
+
|
|
201
|
+
## 11. References
|
|
202
|
+
|
|
203
|
+
- RFC 2119 - Key words for use in RFCs.
|
|
204
|
+
- RFC 8032 - Edwards-Curve Digital Signature Algorithm (EdDSA), Ed25519.
|
|
205
|
+
- FIPS 204 - Module-Lattice-Based Digital Signature Standard (ML-DSA).
|
|
206
|
+
- RFC 8949 - Concise Binary Object Representation (CBOR).
|
|
207
|
+
- RFC 9052 - CBOR Object Signing and Encryption (COSE): Structures and Process.
|
|
208
|
+
- FIPS 180-4 - Secure Hash Standard (SHA-256).
|
|
209
|
+
|
|
210
|
+
## Appendix A. Closed design decisions
|
|
211
|
+
|
|
212
|
+
This appendix records decisions that were implicit in the spec text and have been explicitly closed. It exists so that future readers and implementers do not treat settled questions as still open.
|
|
213
|
+
|
|
214
|
+
| ID | Date | Decision | Status |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| DD-1 | 2026-06-27 | Serialization format for v1.0 is `sraid/json` (RFC 8785 JCS). `sraid/cbor` is deferred to a post-v1.0 additive binary profile. See Section 3 for rationale. | CLOSED |
|