@zanii/certify 0.0.0-stage → 0.5.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.
Files changed (133) hide show
  1. package/LICENSE +159 -0
  2. package/README.md +112 -2
  3. package/dist/appliance-execution.d.ts +18 -0
  4. package/dist/appliance-execution.js +12 -0
  5. package/dist/appliance.d.ts +135 -0
  6. package/dist/appliance.js +253 -0
  7. package/dist/assurance-status.d.ts +58 -0
  8. package/dist/assurance-status.js +86 -0
  9. package/dist/audit-renewal.d.ts +39 -0
  10. package/dist/audit-renewal.js +62 -0
  11. package/dist/audit.d.ts +69 -0
  12. package/dist/audit.js +112 -0
  13. package/dist/beacon-client.d.ts +38 -0
  14. package/dist/beacon-client.js +52 -0
  15. package/dist/beacon.d.ts +84 -0
  16. package/dist/beacon.js +106 -0
  17. package/dist/bitstring-status.d.ts +56 -0
  18. package/dist/bitstring-status.js +135 -0
  19. package/dist/board.d.ts +218 -0
  20. package/dist/board.js +428 -0
  21. package/dist/bundle.d.ts +92 -0
  22. package/dist/bundle.js +109 -0
  23. package/dist/certificate.d.ts +161 -0
  24. package/dist/certificate.js +345 -0
  25. package/dist/cli-messages.json +148 -0
  26. package/dist/cli.d.ts +16 -0
  27. package/dist/cli.js +369 -0
  28. package/dist/current-issuer-vc.d.ts +20 -0
  29. package/dist/current-issuer-vc.js +60 -0
  30. package/dist/evidence-share.d.ts +89 -0
  31. package/dist/evidence-share.js +479 -0
  32. package/dist/federation.d.ts +103 -0
  33. package/dist/federation.js +240 -0
  34. package/dist/feed-snapshot.d.ts +97 -0
  35. package/dist/feed-snapshot.js +186 -0
  36. package/dist/fingerprint.d.ts +23 -0
  37. package/dist/fingerprint.js +48 -0
  38. package/dist/formats/status-v2.schema.json +31 -0
  39. package/dist/governance.d.ts +242 -0
  40. package/dist/governance.js +411 -0
  41. package/dist/identity.d.ts +79 -0
  42. package/dist/identity.js +146 -0
  43. package/dist/impartiality.d.ts +47 -0
  44. package/dist/impartiality.js +85 -0
  45. package/dist/index.d.ts +58 -0
  46. package/dist/index.js +59 -0
  47. package/dist/issuer-authorization.d.ts +4 -0
  48. package/dist/issuer-authorization.js +52 -0
  49. package/dist/issuer-resolution.d.ts +25 -0
  50. package/dist/issuer-resolution.js +89 -0
  51. package/dist/licence-owner-entry.d.ts +20 -0
  52. package/dist/licence-owner-entry.js +33 -0
  53. package/dist/licence-owner.d.ts +67 -0
  54. package/dist/licence-owner.js +180 -0
  55. package/dist/licence.d.ts +174 -0
  56. package/dist/licence.js +400 -0
  57. package/dist/live-evidence.d.ts +135 -0
  58. package/dist/live-evidence.js +210 -0
  59. package/dist/nitro-roots.d.ts +3 -0
  60. package/dist/nitro-roots.js +20 -0
  61. package/dist/nitro-x509.d.ts +16 -0
  62. package/dist/nitro-x509.js +141 -0
  63. package/dist/nitro.d.ts +74 -0
  64. package/dist/nitro.js +329 -0
  65. package/dist/normalization.d.ts +8 -0
  66. package/dist/normalization.js +10 -0
  67. package/dist/pq.d.ts +32 -0
  68. package/dist/pq.js +69 -0
  69. package/dist/profile.d.ts +20 -0
  70. package/dist/profile.js +161 -0
  71. package/dist/register.d.ts +79 -0
  72. package/dist/register.js +134 -0
  73. package/dist/rotation.d.ts +49 -0
  74. package/dist/rotation.js +124 -0
  75. package/dist/royalty-reconciliation.d.ts +24 -0
  76. package/dist/royalty-reconciliation.js +88 -0
  77. package/dist/runtime-client.d.ts +38 -0
  78. package/dist/runtime-client.js +185 -0
  79. package/dist/runtime-gateway.d.ts +24 -0
  80. package/dist/runtime-gateway.js +167 -0
  81. package/dist/runtime-knowledge.d.ts +28 -0
  82. package/dist/runtime-knowledge.js +75 -0
  83. package/dist/runtime-nitro.d.ts +30 -0
  84. package/dist/runtime-nitro.js +57 -0
  85. package/dist/runtime.d.ts +226 -0
  86. package/dist/runtime.js +254 -0
  87. package/dist/sandbox-delegation.d.ts +38 -0
  88. package/dist/sandbox-delegation.js +61 -0
  89. package/dist/schema-dialect.d.ts +1 -0
  90. package/dist/schema-dialect.js +97 -0
  91. package/dist/schemas/clause-map.schema.json +52 -0
  92. package/dist/schemas/profile.schema.json +80 -0
  93. package/dist/schemas/scenarios.schema.json +573 -0
  94. package/dist/schemas/suite.schema.json +138 -0
  95. package/dist/scheme-authority.d.ts +5 -0
  96. package/dist/scheme-authority.js +15 -0
  97. package/dist/scheme-entry.d.ts +25 -0
  98. package/dist/scheme-entry.js +85 -0
  99. package/dist/scheme-history.d.ts +5 -0
  100. package/dist/scheme-history.js +87 -0
  101. package/dist/scheme-log.d.ts +98 -0
  102. package/dist/scheme-log.js +266 -0
  103. package/dist/scheme-merkle.d.ts +2 -0
  104. package/dist/scheme-merkle.js +20 -0
  105. package/dist/scheme-text.d.ts +25 -0
  106. package/dist/scheme-text.js +46 -0
  107. package/dist/scheme-timing.d.ts +16 -0
  108. package/dist/scheme-timing.js +22 -0
  109. package/dist/scheme-transition.d.ts +47 -0
  110. package/dist/scheme-transition.js +101 -0
  111. package/dist/scoring.d.ts +35 -0
  112. package/dist/scoring.js +50 -0
  113. package/dist/shared.d.ts +16 -0
  114. package/dist/shared.js +20 -0
  115. package/dist/signer.d.ts +18 -0
  116. package/dist/signer.js +49 -0
  117. package/dist/signing-identity.d.ts +1 -0
  118. package/dist/signing-identity.js +11 -0
  119. package/dist/status.d.ts +113 -0
  120. package/dist/status.js +204 -0
  121. package/dist/succession.d.ts +21 -0
  122. package/dist/succession.js +67 -0
  123. package/dist/suite.d.ts +257 -0
  124. package/dist/suite.js +441 -0
  125. package/dist/vc.d.ts +29 -0
  126. package/dist/vc.js +96 -0
  127. package/dist/verification-bounds.d.ts +5 -0
  128. package/dist/verification-bounds.js +76 -0
  129. package/dist/verification-policy.d.ts +32 -0
  130. package/dist/verification-policy.js +85 -0
  131. package/dist/witness.d.ts +19 -0
  132. package/dist/witness.js +80 -0
  133. package/package.json +57 -3
@@ -0,0 +1,253 @@
1
+ // spec/appliance.md: the self-hosted lab appliance. The central lab enrols an appliance key; the
2
+ // appliance exports a run as a signed bundle with its platform attestation, the evidence chain
3
+ // heads and the transcripts sealed to the central lab (@zanii/pq X25519 + ML-KEM-768); offline
4
+ // update bundles are signed by the lab release key.
5
+ import { verifyAttestation } from "@zanii/attest";
6
+ import { canonicalBytes, jcsHash, verifySignedObject } from "@zanii/core";
7
+ import { hybridOpen, hybridSeal } from "@zanii/pq";
8
+ import { MAX_SKEW_SECONDS } from "./beacon.js";
9
+ import { isText } from "./board.js";
10
+ import { buildApplianceExportBundle, bundleHash } from "./bundle.js";
11
+ import { sha256Bytes } from "./fingerprint.js";
12
+ import { isDid, isInt, isIso, isObj, isSha, ms, sortedUnique } from "./shared.js";
13
+ import { signerDid, signWith } from "./signer.js";
14
+ const COUNTRY = /^[A-Z]{2}$/;
15
+ const PLATFORM = /^[a-z0-9][a-z0-9-]{0,31}$/;
16
+ const MEASUREMENT = /^[A-Za-z0-9:._+/=-]{1,256}$/;
17
+ const CHAIN = /^[A-Za-z0-9._:-]{1,128}$/;
18
+ const HEX = /^[0-9a-f]+$/;
19
+ const VERSION = /^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$/;
20
+ function enrolmentError(e) {
21
+ const c = isObj(e.customer) ? e.customer : {};
22
+ if (!isDid(e.appliance))
23
+ return "appliance must be a did:key";
24
+ if (!isObj(e.customer) || Object.keys(c).length !== 2 || typeof c.name !== "string" || c.name.length < 1 || c.name.length > 200 || typeof c.country !== "string" || !COUNTRY.test(c.country))
25
+ return "customer needs a name (1-200) and an ISO 3166-1 alpha-2 country";
26
+ const m = e.measurements;
27
+ if (!Array.isArray(m) || m.length < 1 || m.length > 32 || !m.every((x) => typeof x === "string" && MEASUREMENT.test(x)) || sortedUnique(m).join("\n") !== m.join("\n"))
28
+ return "measurements must be 1 to 32 sorted, distinct measurements";
29
+ if (!isIso(e.enrolled_at) || !isIso(e.valid_until) || !(ms(e.valid_until) > ms(e.enrolled_at)))
30
+ return "enrolled_at and valid_until must be ISO timestamps, valid_until after enrolled_at";
31
+ return null;
32
+ }
33
+ /** Section 1: the central lab enrols an appliance key and the code measurements it may run. */
34
+ export async function buildApplianceEnrolment(f, signer) {
35
+ const unsigned = {
36
+ v: 1,
37
+ type: "certify.appliance-enrolment",
38
+ appliance: f.appliance,
39
+ customer: { name: f.customer?.name, country: f.customer?.country },
40
+ measurements: Array.isArray(f.measurements) ? sortedUnique(f.measurements) : f.measurements,
41
+ enrolled_at: f.enrolledAt,
42
+ valid_until: f.validUntil,
43
+ };
44
+ const err = enrolmentError(unsigned);
45
+ if (err)
46
+ throw new Error(err);
47
+ return signWith(unsigned, signer);
48
+ }
49
+ /** Section 2: the hash of the central lab's public key, `{x25519, mlkem}` hex. */
50
+ export const labKeyId = (key) => jcsHash({ x25519: key.x25519, mlkem: key.mlkem });
51
+ /** Section 2: seal the transcripts (any JSON value) to the central lab: its canonical bytes, hybrid sealed. */
52
+ export function sealTranscripts(transcripts, labKey) {
53
+ return { recipient: labKeyId(labKey), sealed: hybridSeal(canonicalBytes(transcripts), labKey) };
54
+ }
55
+ /** Section 2: the report data the appliance's attestation quote binds. */
56
+ export const applianceChallenge = (f) => jcsHash({ v: 1, type: "certify.appliance-challenge", appliance: f.appliance, bundle: f.bundle, evidence: f.evidence, transcripts: jcsHash(f.transcripts), exported_at: f.exportedAt });
57
+ const sealedOk = (t) => {
58
+ if (!isObj(t) || Object.keys(t).length !== 2 || !isSha(t.recipient) || !isObj(t.sealed))
59
+ return false;
60
+ const s = t.sealed;
61
+ const hex = (v, n) => typeof v === "string" && HEX.test(v) && (n === undefined || v.length === n);
62
+ return Object.keys(s).length === 7 && s.v === 1 && s.kem === "x25519+mlkem768" && hex(s.ephemeral, 64) && hex(s.mlkemCipherText, 2176) && hex(s.iv, 24) && hex(s.tag, 32) && (s.ct === "" || hex(s.ct));
63
+ };
64
+ const headOk = (h) => isObj(h) && Object.keys(h).length === 3 && typeof h.chain === "string" && CHAIN.test(h.chain) && isSha(h.head) && isInt(h.size, 1, Number.MAX_SAFE_INTEGER);
65
+ function exportError(x) {
66
+ if (!isObj(x.bundle) || ![1, 2].includes(x.bundle.v) || x.bundle.type !== "certify.result-bundle")
67
+ return "bundle must be a result bundle";
68
+ const e = x.evidence;
69
+ if (!Array.isArray(e) || e.length > 10_000 || !e.every(headOk) || e.some((h, i) => i > 0 && !(h.chain > e[i - 1].chain)))
70
+ return "evidence must be sorted, distinct {chain, head, size}";
71
+ if (!sealedOk(x.transcripts))
72
+ return "transcripts must be {recipient, sealed} sealed with x25519+mlkem768";
73
+ if (x.bundle.v === 2) {
74
+ const b = x.bundle;
75
+ if (Object.keys(b).length !== 15 || b.phase !== "undecided" || !isObj(b.staff) || Object.keys(b.staff).length !== 3 || b.staff.reviewer !== null || b.staff.decider !== null)
76
+ return "appliance bundle must be explicitly undecided (v2)";
77
+ try {
78
+ const input = b;
79
+ const rebuilt = buildApplianceExportBundle({ scheme: input.scheme, profile: input.profile, level: input.level, fingerprint: input.fingerprint, plan: input.plan, results: input.results, evidence: input.evidence, suite_snapshot: input.suite_snapshot, testers: input.staff.testers, execution: input.execution, started_at: input.started_at, ended_at: input.ended_at });
80
+ if (jcsHash(rebuilt) !== jcsHash(b))
81
+ return "appliance bundle fields or scores are not canonical (v2)";
82
+ const identity = rebuilt.execution, heads = new Map(e.map(head => [head.chain, head.head]));
83
+ if (identity.appliance !== x.appliance || identity.recipient !== x.transcripts.recipient || !heads.has(identity.source_run) || heads.size !== rebuilt.evidence.length + 1 || rebuilt.evidence.some(head => heads.get(`${identity.source_run}:${head.case_id}`) !== head.chain_head))
84
+ return "appliance bundle execution identity does not bind this export";
85
+ }
86
+ catch {
87
+ return "appliance bundle is malformed (v2)";
88
+ }
89
+ }
90
+ if (!isIso(x.exported_at))
91
+ return "exported_at must be an ISO timestamp";
92
+ return null;
93
+ }
94
+ function attestationOk(a, x) {
95
+ if (!isObj(a) || Object.keys(a).length !== 6 || a.v !== 1)
96
+ return false;
97
+ const challenge = applianceChallenge({
98
+ appliance: String(x.appliance),
99
+ bundle: bundleHash(x.bundle),
100
+ evidence: x.evidence,
101
+ transcripts: x.transcripts,
102
+ exportedAt: String(x.exported_at),
103
+ });
104
+ return (typeof a.platform === "string" && PLATFORM.test(a.platform) &&
105
+ typeof a.measurement === "string" && MEASUREMENT.test(a.measurement) &&
106
+ isSha(a.quote_hash) && a.nonce === challenge && isIso(a.attested_at) &&
107
+ Math.abs(ms(a.attested_at) - ms(x.exported_at)) <= MAX_SKEW_SECONDS * 1000);
108
+ }
109
+ /** Section 2: the export, signed by the appliance key (`signer`, the enrolled appliance). */
110
+ export async function buildApplianceExport(f, signer) {
111
+ const appliance = signerDid(signer);
112
+ if (!isObj(f.enrolment) || f.enrolment.appliance !== appliance)
113
+ throw new Error("the enrolment is for another appliance");
114
+ const unsigned = {
115
+ v: 1,
116
+ type: "certify.appliance-export",
117
+ appliance,
118
+ enrolment: f.enrolment,
119
+ attestation: f.attestation,
120
+ bundle: f.bundle,
121
+ evidence: Array.isArray(f.evidence)
122
+ ? f.evidence.map((h) => ({ chain: h?.chain, head: h?.head, size: h?.size })).sort((a, b) => (a.chain < b.chain ? -1 : a.chain > b.chain ? 1 : 0))
123
+ : f.evidence,
124
+ transcripts: f.transcripts,
125
+ exported_at: f.exportedAt,
126
+ };
127
+ const err = exportError(unsigned);
128
+ if (err)
129
+ throw new Error(err);
130
+ if (!attestationOk(unsigned.attestation, unsigned))
131
+ throw new Error("attestation is malformed or does not bind this export's applianceChallenge");
132
+ return signWith(unsigned, signer);
133
+ }
134
+ /**
135
+ * Section 3: signed by its appliance, enrolled by a trusted central lab (`labs`) for the time of
136
+ * export, attested for this export with an enrolled measurement, sealed to `labKey` when given, and
137
+ * (with `verifyQuote`) the attestation quote verified by the injected platform verifier.
138
+ */
139
+ export async function verifyApplianceExport(doc, opts = {}) {
140
+ const x = isObj(doc) ? doc : {};
141
+ if (x.v !== 1 || x.type !== "certify.appliance-export" || Object.keys(x).length !== 11 || !isDid(x.appliance))
142
+ return { ok: false, reasons: ["not an appliance export (v1)"], attested: null };
143
+ const reasons = [];
144
+ if (!verifySignedObject(x) || x.signer !== x.appliance)
145
+ reasons.push("bad appliance export signature");
146
+ const err = exportError(x);
147
+ if (err)
148
+ return { ok: false, reasons: [...reasons, `appliance export is malformed: ${err}`], attested: null };
149
+ const e = isObj(x.enrolment) ? x.enrolment : {};
150
+ if (e.v !== 1 || e.type !== "certify.appliance-enrolment" || Object.keys(e).length !== 9 || !verifySignedObject(e) || enrolmentError(e))
151
+ reasons.push("the enrolment is not a signed appliance enrolment");
152
+ else {
153
+ if (e.appliance !== x.appliance)
154
+ reasons.push("the enrolment is for another appliance");
155
+ if (!opts.labs)
156
+ reasons.push("no trusted central lab supplied: the enrolment was NOT checked");
157
+ else if (!opts.labs.includes(e.signer))
158
+ reasons.push("the enrolment is not signed by a trusted central lab");
159
+ if (!(ms(e.enrolled_at) <= ms(x.exported_at) && ms(x.exported_at) <= ms(e.valid_until)))
160
+ reasons.push("the export is outside the appliance's enrolment");
161
+ }
162
+ const a = x.attestation;
163
+ const bound = attestationOk(a, x);
164
+ if (!bound)
165
+ reasons.push("appliance attestation is malformed or does not bind the export");
166
+ else if (!Array.isArray(e.measurements) || !e.measurements.includes(a.measurement))
167
+ reasons.push("appliance attestation measurement is not an enrolled measurement");
168
+ if (opts.labKey !== undefined && x.transcripts.recipient !== labKeyId(opts.labKey))
169
+ reasons.push("transcripts are not sealed to this lab key");
170
+ const now = opts.now ? ms(opts.now) : Date.now();
171
+ if (ms(x.exported_at) > now + MAX_SKEW_SECONDS * 1000)
172
+ reasons.push("appliance export exported_at is in the future");
173
+ let attested = null;
174
+ if (!bound)
175
+ attested = null;
176
+ else if (!opts.verifyQuote)
177
+ reasons.push("no quote verifier supplied: the appliance attestation was NOT verified");
178
+ else {
179
+ const q = a;
180
+ const r = await verifyAttestation({ v: 1, platform: q.platform, measurement: q.measurement, quoteHash: q.quote_hash, nonce: q.nonce, attestedAt: q.attested_at }, { verifyQuote: opts.verifyQuote });
181
+ attested = r.quoteVerified === true;
182
+ if (!attested)
183
+ reasons.push("appliance attestation quote verification failed");
184
+ }
185
+ return { ok: reasons.length === 0, reasons, attested };
186
+ }
187
+ /** Section 4: the central lab opens the transcripts with its secret key; throws on a wrong key or tampering. */
188
+ export function openApplianceExport(doc, secretKey) {
189
+ if (!isObj(doc) || !sealedOk(doc.transcripts))
190
+ throw new Error("not an appliance export (v1)");
191
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(hybridOpen(doc.transcripts.sealed, secretKey)));
192
+ }
193
+ function updateError(u) {
194
+ if (typeof u.version !== "string" || !VERSION.test(u.version))
195
+ return "version must be <major>.<minor>.<patch>";
196
+ if (!isSha(u.sha256))
197
+ return "sha256 must be the update file hash";
198
+ if (!isSha(u.enrolment))
199
+ return "enrolment must be the hash of the enrolment of the appliance the update is for";
200
+ if (!isIso(u.released_at))
201
+ return "released_at must be an ISO timestamp";
202
+ if (!isText(u.notes))
203
+ return "notes need en and ar text (1-4000)";
204
+ return null;
205
+ }
206
+ /** Section 5: an offline update bundle's manifest for one enrolled appliance (`enrolment`, the jcsHash of its enrolment), signed by the lab release key. */
207
+ export async function buildApplianceUpdate(f, signer) {
208
+ const unsigned = {
209
+ v: 1, type: "certify.appliance-update", version: f.version, sha256: f.sha256, enrolment: f.enrolment,
210
+ released_at: f.releasedAt, notes: { en: f.notes?.en, ar: f.notes?.ar },
211
+ };
212
+ const err = updateError(unsigned);
213
+ if (err)
214
+ throw new Error(err);
215
+ return signWith(unsigned, signer);
216
+ }
217
+ const versionParts = (v) => v.split(".").map(Number);
218
+ const newer = (a, b) => {
219
+ const [x, y] = [versionParts(a), versionParts(b)];
220
+ for (let i = 0; i < 3; i++)
221
+ if (x[i] !== y[i])
222
+ return x[i] > y[i];
223
+ return false;
224
+ };
225
+ /** Section 5: signed by a trusted release key (`keys`), for `content` when given, for the appliance whose enrolment hash is `enrolment`, and newer than `installed` when given. */
226
+ export function verifyApplianceUpdate(doc, opts = {}) {
227
+ const u = isObj(doc) ? doc : {};
228
+ if (u.v !== 1 || u.type !== "certify.appliance-update" || Object.keys(u).length !== 9)
229
+ return { ok: false, reasons: ["not an appliance update (v1)"] };
230
+ const reasons = [];
231
+ if (!verifySignedObject(u))
232
+ reasons.push("bad appliance update signature");
233
+ const err = updateError(u);
234
+ if (err)
235
+ reasons.push(`appliance update is malformed: ${err}`);
236
+ if (!opts.keys)
237
+ reasons.push("no trusted release keys supplied: the signer was NOT checked");
238
+ else if (!opts.keys.includes(u.signer))
239
+ reasons.push("appliance update is not signed by a trusted release key");
240
+ if (opts.content !== undefined && sha256Bytes(opts.content) !== u.sha256)
241
+ reasons.push("the update content is not the signed content");
242
+ if (opts.enrolment === undefined)
243
+ reasons.push("no enrolment supplied: the appliance the update is for was NOT checked");
244
+ else if (u.enrolment !== opts.enrolment)
245
+ reasons.push("appliance update is for another appliance's enrolment");
246
+ if (opts.installed !== undefined) {
247
+ if (!VERSION.test(opts.installed))
248
+ throw new Error("installed must be <major>.<minor>.<patch>");
249
+ if (err || !newer(u.version, opts.installed))
250
+ reasons.push("appliance update is not newer than the installed version");
251
+ }
252
+ return { ok: reasons.length === 0, reasons };
253
+ }
@@ -0,0 +1,58 @@
1
+ import { type Signer } from "./signer.ts";
2
+ export type AssurancePurpose = "gateway-enrolment" | "agent-binding";
3
+ interface AssuranceRef {
4
+ purpose: AssurancePurpose;
5
+ subject: string;
6
+ principal: string;
7
+ context: string;
8
+ }
9
+ export interface AssuranceRevocation extends AssuranceRef {
10
+ v: 1;
11
+ type: "certify.assurance-revocation";
12
+ reason: string;
13
+ revoked_at: string;
14
+ issued_at: string;
15
+ signer: string;
16
+ sig: string;
17
+ }
18
+ export interface AssuranceStatus extends AssuranceRef {
19
+ v: 1;
20
+ type: "certify.assurance-status";
21
+ epoch: number;
22
+ status: "active" | "revoked";
23
+ revocation: string | null;
24
+ issued_at: string;
25
+ valid_until: string;
26
+ signer: string;
27
+ sig: string;
28
+ }
29
+ export interface AssuranceAuthority {
30
+ owner: string;
31
+ domain: string;
32
+ document: unknown;
33
+ }
34
+ export interface AssuranceStatusPolicy {
35
+ document: unknown;
36
+ signers: readonly string[];
37
+ minEpoch: number;
38
+ maxAgeSeconds: number;
39
+ revocation?: unknown;
40
+ authority?: AssuranceAuthority;
41
+ }
42
+ export interface AssuranceStatusOptions {
43
+ purpose: AssurancePurpose;
44
+ artifact: unknown;
45
+ signers: readonly string[];
46
+ now: string;
47
+ minEpoch: number;
48
+ maxAgeSeconds: number;
49
+ revocation?: unknown;
50
+ authority?: AssuranceAuthority;
51
+ }
52
+ export declare function buildAssuranceRevocation(fields: Omit<AssuranceRevocation, "v" | "type" | "signer" | "sig">, signer: Signer): Promise<AssuranceRevocation>;
53
+ export declare function buildAssuranceStatus(fields: Omit<AssuranceStatus, "v" | "type" | "signer" | "sig">, signer: Signer): Promise<AssuranceStatus>;
54
+ export declare function verifyAssuranceStatus(document: unknown, opts: AssuranceStatusOptions): {
55
+ ok: boolean;
56
+ reasons: string[];
57
+ };
58
+ export {};
@@ -0,0 +1,86 @@
1
+ // Fresh, purpose-bound status for immutable enrolments/bindings; callers retain epoch/revocation watermarks.
2
+ import { jcsHash, publicKeyFromDid } from "@zanii/core";
3
+ import { issuerArtifactAuthorization } from "./issuer-authorization.js";
4
+ import { isInt, isIso, isObj, isSha, ms } from "./shared.js";
5
+ import { signatureOk, signerDid, signWith } from "./signer.js";
6
+ import { withinVerificationBudget } from "./verification-bounds.js";
7
+ const keys = (v, names) => isObj(v) && Object.keys(v).length === names.split(" ").length && names.split(" ").every((k) => Object.hasOwn(v, k));
8
+ const did = (v) => typeof v === "string" && v.length <= 69 && publicKeyFromDid(v) !== null;
9
+ const hash = (v) => isSha(v) && v.length === 71;
10
+ const ref = (v) => (v.purpose === "gateway-enrolment" || v.purpose === "agent-binding") && hash(v.subject) && did(v.principal) && hash(v.context);
11
+ const signed = (v, type) => v.v === 1 && v.type === type && did(v.signer) && typeof v.sig === "string" && v.sig.length === 136 && /^ed25519:[0-9a-f]{128}$/.test(v.sig);
12
+ const revocationShape = (v) => keys(v, "v type purpose subject principal context reason revoked_at issued_at signer sig") && signed(v, "certify.assurance-revocation") && ref(v)
13
+ && typeof v.reason === "string" && v.reason.length <= 64 && /^[A-Z][A-Z0-9_]*$/.test(v.reason) && v.reason.trim() === v.reason && isIso(v.revoked_at) && isIso(v.issued_at) && ms(v.revoked_at) <= ms(v.issued_at);
14
+ const statusShape = (v) => keys(v, "v type purpose subject principal context epoch status revocation issued_at valid_until signer sig") && signed(v, "certify.assurance-status") && ref(v)
15
+ && isInt(v.epoch, 1, Number.MAX_SAFE_INTEGER) && (v.status === "active" ? v.revocation === null : v.status === "revoked" && hash(v.revocation))
16
+ && isIso(v.issued_at) && isIso(v.valid_until) && ms(v.valid_until) > ms(v.issued_at) && ms(v.valid_until) - ms(v.issued_at) <= 300_000;
17
+ const small = (v) => withinVerificationBudget(v, { maxBytes: 16_384, maxNodes: 200, maxDepth: 8 });
18
+ const validSignature = (doc) => { const { sig, ...unsigned } = doc; return signatureOk(unsigned, sig, doc.signer); };
19
+ const placeholder = `ed25519:${"0".repeat(128)}`;
20
+ export async function buildAssuranceRevocation(fields, signer) {
21
+ if (!isObj(fields) || !small(fields) || ["v", "type", "signer", "sig"].some((k) => Object.hasOwn(fields, k)))
22
+ throw new Error("invalid assurance revocation input");
23
+ const doc = { ...fields, v: 1, type: "certify.assurance-revocation", signer: signerDid(signer), sig: placeholder };
24
+ if (!revocationShape(doc))
25
+ throw new Error("invalid assurance revocation v1");
26
+ const { sig: _, signer: __, ...unsigned } = doc;
27
+ return signWith(unsigned, signer);
28
+ }
29
+ export async function buildAssuranceStatus(fields, signer) {
30
+ if (!isObj(fields) || !small(fields) || ["v", "type", "signer", "sig"].some((k) => Object.hasOwn(fields, k)))
31
+ throw new Error("invalid assurance status input");
32
+ const doc = { ...fields, v: 1, type: "certify.assurance-status", signer: signerDid(signer), sig: placeholder };
33
+ if (!statusShape(doc))
34
+ throw new Error("invalid assurance status v1");
35
+ const { sig: _, signer: __, ...unsigned } = doc;
36
+ return signWith(unsigned, signer);
37
+ }
38
+ function artifactRef(artifact, purpose) {
39
+ const names = purpose === "gateway-enrolment" ? "v type certificate gateway attestation issued_at valid_until signer sig" : "v type applicant legal_name domain kyb agent bound_at valid_until signer sig";
40
+ if (!small(artifact) || !keys(artifact, names) || artifact.v !== 1 || artifact.type !== `certify.${purpose}`
41
+ || !did(artifact.signer) || !isIso(artifact.valid_until) || typeof artifact.sig !== "string" || artifact.sig.length !== 136 || !validSignature(artifact))
42
+ return null;
43
+ const principal = purpose === "gateway-enrolment" ? artifact.gateway : artifact.agent, context = purpose === "gateway-enrolment" ? artifact.certificate : artifact.kyb;
44
+ const at = purpose === "gateway-enrolment" ? artifact.issued_at : artifact.bound_at;
45
+ if (!did(principal) || !hash(context) || !isIso(at))
46
+ return null;
47
+ return { purpose, subject: jcsHash(artifact), principal, context, signer: artifact.signer, issued_at: at, valid_until: artifact.valid_until };
48
+ }
49
+ const sameRef = (doc, expected) => doc.purpose === expected.purpose && doc.subject === expected.subject && doc.principal === expected.principal && doc.context === expected.context;
50
+ function historicalAuthority(signer, issued_at, current, authority, at) {
51
+ if (!authority)
52
+ return signer === current ? [] : ["historical assurance signer needs authorized issuer lineage"];
53
+ return issuerArtifactAuthorization({ signer, issued_at }, current, authority.document, authority.owner, authority.domain, at);
54
+ }
55
+ export function verifyAssuranceStatus(document, opts) {
56
+ const denied = (reason) => ({ ok: false, reasons: [reason] });
57
+ if (!isObj(opts) || (opts.purpose !== "gateway-enrolment" && opts.purpose !== "agent-binding") || !small(document) || !withinVerificationBudget({ artifact: opts.artifact, authority: opts.authority ?? null, revocation: opts.revocation ?? null }, { maxBytes: 4_194_304 })
58
+ || !Array.isArray(opts.signers) || opts.signers.length < 1 || opts.signers.length > 64 || !Array.from(opts.signers).every(did) || !isIso(opts.now)
59
+ || !isInt(opts.minEpoch, 1, Number.MAX_SAFE_INTEGER) || !isInt(opts.maxAgeSeconds, 1, 300))
60
+ return denied("independent assurance pins, epoch, time and bounded evidence are required");
61
+ const expected = artifactRef(opts.artifact, opts.purpose);
62
+ if (!expected || !statusShape(document) || !sameRef(document, expected))
63
+ return denied("assurance status does not match the expected artifact and purpose");
64
+ const reasons = [];
65
+ if (!opts.signers.includes(document.signer) || !validSignature(document))
66
+ reasons.push("assurance status is not signed by a trusted current authority");
67
+ if (document.epoch < opts.minEpoch)
68
+ reasons.push("assurance status epoch is older than the retained watermark");
69
+ if (ms(document.issued_at) > ms(opts.now) || ms(document.valid_until) <= ms(opts.now) || ms(opts.now) - ms(document.issued_at) >= opts.maxAgeSeconds * 1000)
70
+ reasons.push("assurance status is stale or from the future");
71
+ if (ms(document.issued_at) < ms(expected.issued_at) || ms(expected.valid_until) <= ms(opts.now))
72
+ reasons.push("assurance artifact is not in force at the reference time");
73
+ reasons.push(...historicalAuthority(expected.signer, expected.issued_at, document.signer, opts.authority, document.issued_at));
74
+ const revocation = opts.revocation;
75
+ if (document.status === "revoked" || revocation != null) {
76
+ if (!small(revocation) || !revocationShape(revocation) || !sameRef(revocation, expected) || !validSignature(revocation) || ms(revocation.issued_at) > ms(opts.now))
77
+ reasons.push("missing or invalid assurance revocation evidence");
78
+ else {
79
+ reasons.push(...historicalAuthority(revocation.signer, revocation.issued_at, document.signer, opts.authority, document.issued_at));
80
+ if (document.status !== "revoked" || document.revocation !== jcsHash(revocation))
81
+ reasons.push("assurance status does not reference the known revocation");
82
+ }
83
+ reasons.push("assurance artifact is revoked");
84
+ }
85
+ return { ok: reasons.length === 0, reasons: [...new Set(reasons)] };
86
+ }
@@ -0,0 +1,39 @@
1
+ import { type Credential } from "@zanii/credentials";
2
+ import { type AssuranceAuthority } from "./assurance-status.ts";
3
+ import { type Signer } from "./signer.ts";
4
+ export interface AuditRenewal {
5
+ v: 1;
6
+ type: "certify.audit-renewal";
7
+ certificate: string;
8
+ original_audit: string;
9
+ audit: string;
10
+ epoch: number;
11
+ issued_at: string;
12
+ valid_until: string;
13
+ signer: string;
14
+ sig: string;
15
+ }
16
+ export interface AuditRenewalPolicy {
17
+ document: unknown;
18
+ originalAudit: unknown;
19
+ issuers: readonly string[];
20
+ minEpoch: number;
21
+ authority?: AssuranceAuthority;
22
+ originalTrustedBodies?: readonly string[];
23
+ }
24
+ export interface AuditRenewalOptions {
25
+ certificate: Credential;
26
+ originalAudit: unknown;
27
+ audit: unknown;
28
+ issuers: readonly string[];
29
+ trustedBodies: readonly string[];
30
+ originalTrustedBodies?: readonly string[];
31
+ now: string;
32
+ minEpoch: number;
33
+ authority?: AssuranceAuthority;
34
+ }
35
+ export declare function buildAuditRenewal(fields: Omit<AuditRenewal, "v" | "type" | "signer" | "sig">, signer: Signer): Promise<AuditRenewal>;
36
+ export declare function verifyAuditRenewal(document: unknown, opts: AuditRenewalOptions): {
37
+ ok: boolean;
38
+ reasons: string[];
39
+ };
@@ -0,0 +1,62 @@
1
+ // Immutable Level 3 credentials retain their original audit; a current issuer explicitly attaches its renewal.
2
+ import { jcsHash, publicKeyFromDid } from "@zanii/core";
3
+ import { credentialHash } from "@zanii/credentials";
4
+ import { verifyAuditAttestation } from "./audit.js";
5
+ import {} from "./assurance-status.js";
6
+ import { issuerArtifactAuthorization } from "./issuer-authorization.js";
7
+ import { isInt, isIso, isObj, isSha, ms } from "./shared.js";
8
+ import { signatureOk, signerDid, signWith } from "./signer.js";
9
+ import { withinVerificationBudget } from "./verification-bounds.js";
10
+ const did = (v) => typeof v === "string" && v.length <= 69 && publicKeyFromDid(v) !== null;
11
+ const shape = (v) => isObj(v) && Object.keys(v).length === 10 && "v type certificate original_audit audit epoch issued_at valid_until signer sig".split(" ").every((k) => Object.hasOwn(v, k))
12
+ && v.v === 1 && v.type === "certify.audit-renewal" && isSha(v.certificate) && isSha(v.original_audit) && isSha(v.audit) && v.audit !== v.original_audit && isInt(v.epoch, 1, Number.MAX_SAFE_INTEGER)
13
+ && isIso(v.issued_at) && isIso(v.valid_until) && ms(v.valid_until) > ms(v.issued_at) && did(v.signer) && typeof v.sig === "string" && /^ed25519:[0-9a-f]{128}$/.test(v.sig);
14
+ export async function buildAuditRenewal(fields, signer) {
15
+ if (!isObj(fields) || !withinVerificationBudget(fields, { maxBytes: 16_384, maxNodes: 200, maxDepth: 8 }) || ["v", "type", "signer", "sig"].some((k) => Object.hasOwn(fields, k)))
16
+ throw new Error("invalid audit renewal input");
17
+ const doc = { ...fields, v: 1, type: "certify.audit-renewal", signer: signerDid(signer), sig: `ed25519:${"0".repeat(128)}` };
18
+ if (!shape(doc))
19
+ throw new Error("invalid audit renewal v1");
20
+ const { sig: _, signer: __, ...unsigned } = doc;
21
+ return signWith(unsigned, signer);
22
+ }
23
+ export function verifyAuditRenewal(document, opts) {
24
+ const denied = (reason) => ({ ok: false, reasons: [reason] });
25
+ if (!withinVerificationBudget({ document, opts }, { maxBytes: 4_194_304 }) || !shape(document) || !isObj(opts) || !isIso(opts.now) || !isInt(opts.minEpoch, 1, Number.MAX_SAFE_INTEGER)
26
+ || !Array.isArray(opts.issuers) || opts.issuers.length < 1 || opts.issuers.length > 64 || !opts.issuers.every(did)
27
+ || !Array.isArray(opts.trustedBodies) || opts.trustedBodies.length < 1 || opts.trustedBodies.length > 64 || !opts.trustedBodies.every(did)
28
+ || (opts.originalTrustedBodies !== undefined && (!Array.isArray(opts.originalTrustedBodies) || opts.originalTrustedBodies.length > 64 || !opts.originalTrustedBodies.every(did))))
29
+ return denied("audit renewal needs bounded evidence, current issuer/body pins, retained epoch and time");
30
+ const c = opts.certificate, a = opts.audit, original = opts.originalAudit;
31
+ if (!isObj(c) || c.v !== 1 || c.type !== "credential" || Object.keys(c).length !== 10 || !isObj(c.claim) || c.claim.level !== 3 || !did(c.issuer) || !isIso(c.issued_at) || !isIso(c.exp) || typeof c.sig !== "string" || !isObj(a) || !isObj(original))
32
+ return denied("audit renewal needs its original Level 3 certificate and both audit attestations");
33
+ const reasons = [];
34
+ const { sig, ...unsigned } = c;
35
+ if (!signatureOk(unsigned, sig, c.issuer))
36
+ reasons.push("bad original certificate signature");
37
+ if (document.certificate !== credentialHash(c) || document.original_audit !== c.claim.audit || document.original_audit !== jcsHash(original) || document.audit !== jcsHash(a))
38
+ reasons.push("audit renewal references a different certificate or audit");
39
+ if (!opts.issuers.includes(document.signer))
40
+ reasons.push("audit renewal is not signed by a trusted current issuer");
41
+ const { sig: renewalSig, ...renewalUnsigned } = document;
42
+ if (!signatureOk(renewalUnsigned, renewalSig, document.signer))
43
+ reasons.push("bad audit renewal signature");
44
+ if (opts.authority)
45
+ reasons.push(...issuerArtifactAuthorization({ signer: c.issuer, issued_at: c.issued_at }, document.signer, opts.authority.document, opts.authority.owner, opts.authority.domain, document.issued_at));
46
+ else if (c.issuer !== document.signer)
47
+ reasons.push("historical certificate signer needs authorized issuer lineage");
48
+ reasons.push(...verifyAuditAttestation(original, { certificate: c, trustedBodies: opts.originalTrustedBodies ?? opts.trustedBodies, now: c.issued_at }).reasons);
49
+ reasons.push(...verifyAuditAttestation(a, { trustedBodies: opts.trustedBodies, now: opts.now }).reasons);
50
+ const applicant = isObj(c.claim.applicant) ? c.claim.applicant : {}, ap = isObj(a.applicant) ? a.applicant : {};
51
+ if (a.holder !== c.holder || ap.name !== applicant.name || ap.country !== applicant.country)
52
+ reasons.push("renewed audit names another agent or applicant");
53
+ const scope = isObj(a.scope) ? a.scope : {}, previousScope = isObj(original.scope) ? original.scope : {};
54
+ if (a.standard !== original.standard || scope.en !== previousScope.en || scope.ar !== previousScope.ar || !Array.isArray(a.controls) || !Array.isArray(original.controls) || !original.controls.every((control) => a.controls.includes(control)))
55
+ reasons.push("renewed audit changes scope, standard or drops original controls; a new certificate is required");
56
+ if (!isIso(a.issued_at) || !isIso(a.audited_at) || !isIso(original.issued_at) || ms(original.issued_at) > ms(c.issued_at) || ms(a.issued_at) > ms(document.issued_at) || ms(a.audited_at) <= ms(original.audited_at) || ms(document.issued_at) < ms(c.issued_at) || ms(document.issued_at) > ms(opts.now)
57
+ || ms(document.valid_until) <= ms(opts.now) || ms(document.valid_until) > ms(a.valid_until) || ms(document.valid_until) > ms(c.exp) || ms(a.valid_until) <= ms(opts.now))
58
+ reasons.push("audit renewal is expired, future-dated or outside certificate/audit validity");
59
+ if (document.epoch < opts.minEpoch)
60
+ reasons.push("audit renewal epoch is older than the retained watermark");
61
+ return { ok: reasons.length === 0, reasons: [...new Set(reasons)] };
62
+ }
@@ -0,0 +1,69 @@
1
+ import type { Credential } from "@zanii/credentials";
2
+ import { type Signer } from "./signer.ts";
3
+ export declare const AUDIT_OUTCOMES: readonly ["conforming", "minor_nonconformities", "major_nonconformities"];
4
+ export type AuditOutcome = (typeof AUDIT_OUTCOMES)[number];
5
+ /** A clause or Annex A control id, by number only (`8.2`, `A.6.2.4`). */
6
+ export declare const CONTROL: RegExp;
7
+ export interface AuditAttestation {
8
+ v: 1;
9
+ type: "certify.audit-attestation";
10
+ /** The certification body's did:key; equals `signer`. */
11
+ body: string;
12
+ /** The audited agent (the certificate holder). */
13
+ holder: string;
14
+ applicant: {
15
+ name: string;
16
+ country: string;
17
+ };
18
+ scope: {
19
+ en: string;
20
+ ar: string;
21
+ };
22
+ standard: string;
23
+ controls: string[];
24
+ audited_at: string;
25
+ valid_until: string;
26
+ outcome: AuditOutcome;
27
+ issued_at: string;
28
+ signer: string;
29
+ sig: string;
30
+ }
31
+ export interface AuditInput {
32
+ holder: string;
33
+ applicant: {
34
+ name: string;
35
+ country: string;
36
+ };
37
+ scope: {
38
+ en: string;
39
+ ar: string;
40
+ };
41
+ standard: string;
42
+ controls: readonly string[];
43
+ auditedAt: string;
44
+ validUntil: string;
45
+ outcome: AuditOutcome;
46
+ issuedAt: string;
47
+ }
48
+ /** Sign an audit attestation as the certification body (`signer` holds the body's key). */
49
+ export declare function buildAuditAttestation(fields: AuditInput, signer: Signer): Promise<AuditAttestation>;
50
+ export interface AuditCheck {
51
+ ok: boolean;
52
+ reasons: string[];
53
+ }
54
+ /**
55
+ * Verify an audit attestation: signed by its body, well formed, from a body in `trustedBodies`,
56
+ * still valid at `now`, not a major nonconformity, and (with `certificate`) for that certificate's
57
+ * holder and applicant, referenced by its `claim.audit` and dated no later than its issue.
58
+ */
59
+ export declare function verifyAuditAttestation(att: unknown, opts?: {
60
+ trustedBodies?: readonly string[];
61
+ now?: string;
62
+ certificate?: Credential;
63
+ }): AuditCheck;
64
+ /**
65
+ * Section 3: has the audit attestation of a Level 3 certificate lapsed at `now`? Surveillance then
66
+ * suspends the certificate with reason AUDIT_EXPIRED. False below Level 3; throws when `audit` is
67
+ * not the attestation the certificate references.
68
+ */
69
+ export declare function auditLapsed(certificate: Credential, audit: AuditAttestation, now: string): boolean;