@zanii/certify 0.0.0-stage → 0.6.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 +159 -0
- package/README.md +144 -2
- package/dist/appliance-execution.d.ts +18 -0
- package/dist/appliance-execution.js +12 -0
- package/dist/appliance.d.ts +135 -0
- package/dist/appliance.js +253 -0
- package/dist/assurance-status.d.ts +58 -0
- package/dist/assurance-status.js +86 -0
- package/dist/audit-renewal.d.ts +39 -0
- package/dist/audit-renewal.js +62 -0
- package/dist/audit.d.ts +69 -0
- package/dist/audit.js +112 -0
- package/dist/beacon-client.d.ts +40 -0
- package/dist/beacon-client.js +151 -0
- package/dist/beacon.d.ts +84 -0
- package/dist/beacon.js +106 -0
- package/dist/bitstring-status.d.ts +56 -0
- package/dist/bitstring-status.js +135 -0
- package/dist/board.d.ts +218 -0
- package/dist/board.js +428 -0
- package/dist/bundle.d.ts +100 -0
- package/dist/bundle.js +122 -0
- package/dist/certificate.d.ts +161 -0
- package/dist/certificate.js +346 -0
- package/dist/cli-messages.json +148 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +369 -0
- package/dist/current-issuer-vc.d.ts +20 -0
- package/dist/current-issuer-vc.js +60 -0
- package/dist/evidence-share.d.ts +89 -0
- package/dist/evidence-share.js +479 -0
- package/dist/federation.d.ts +103 -0
- package/dist/federation.js +240 -0
- package/dist/feed-snapshot.d.ts +97 -0
- package/dist/feed-snapshot.js +186 -0
- package/dist/fingerprint.d.ts +23 -0
- package/dist/fingerprint.js +48 -0
- package/dist/formats/status-v2.schema.json +31 -0
- package/dist/governance.d.ts +244 -0
- package/dist/governance.js +418 -0
- package/dist/identity.d.ts +79 -0
- package/dist/identity.js +146 -0
- package/dist/impartiality.d.ts +47 -0
- package/dist/impartiality.js +85 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +63 -0
- package/dist/issuer-authorization.d.ts +4 -0
- package/dist/issuer-authorization.js +52 -0
- package/dist/issuer-resolution.d.ts +25 -0
- package/dist/issuer-resolution.js +89 -0
- package/dist/licence-owner-entry.d.ts +20 -0
- package/dist/licence-owner-entry.js +33 -0
- package/dist/licence-owner.d.ts +67 -0
- package/dist/licence-owner.js +180 -0
- package/dist/licence.d.ts +174 -0
- package/dist/licence.js +400 -0
- package/dist/live-evidence.d.ts +135 -0
- package/dist/live-evidence.js +210 -0
- package/dist/nitro-roots.d.ts +3 -0
- package/dist/nitro-roots.js +20 -0
- package/dist/nitro-x509.d.ts +16 -0
- package/dist/nitro-x509.js +141 -0
- package/dist/nitro.d.ts +74 -0
- package/dist/nitro.js +329 -0
- package/dist/normalization.d.ts +8 -0
- package/dist/normalization.js +10 -0
- package/dist/pq.d.ts +32 -0
- package/dist/pq.js +69 -0
- package/dist/profile.d.ts +15 -0
- package/dist/profile.js +165 -0
- package/dist/register.d.ts +79 -0
- package/dist/register.js +134 -0
- package/dist/rotation.d.ts +49 -0
- package/dist/rotation.js +124 -0
- package/dist/royalty-reconciliation.d.ts +24 -0
- package/dist/royalty-reconciliation.js +88 -0
- package/dist/runtime-client.d.ts +40 -0
- package/dist/runtime-client.js +190 -0
- package/dist/runtime-gateway.d.ts +26 -0
- package/dist/runtime-gateway.js +169 -0
- package/dist/runtime-knowledge.d.ts +28 -0
- package/dist/runtime-knowledge.js +75 -0
- package/dist/runtime-nitro.d.ts +30 -0
- package/dist/runtime-nitro.js +57 -0
- package/dist/runtime.d.ts +226 -0
- package/dist/runtime.js +254 -0
- package/dist/sandbox-delegation.d.ts +38 -0
- package/dist/sandbox-delegation.js +61 -0
- package/dist/schema-dialect.d.ts +1 -0
- package/dist/schema-dialect.js +97 -0
- package/dist/schemas/clause-map.schema.json +52 -0
- package/dist/schemas/profile.schema.json +81 -0
- package/dist/schemas/scenarios.schema.json +573 -0
- package/dist/schemas/suite.schema.json +138 -0
- package/dist/schemas/trajectory.schema.json +1396 -0
- package/dist/scheme-authority.d.ts +5 -0
- package/dist/scheme-authority.js +15 -0
- package/dist/scheme-entry.d.ts +25 -0
- package/dist/scheme-entry.js +85 -0
- package/dist/scheme-history.d.ts +5 -0
- package/dist/scheme-history.js +87 -0
- package/dist/scheme-log.d.ts +98 -0
- package/dist/scheme-log.js +266 -0
- package/dist/scheme-merkle.d.ts +2 -0
- package/dist/scheme-merkle.js +20 -0
- package/dist/scheme-text.d.ts +25 -0
- package/dist/scheme-text.js +46 -0
- package/dist/scheme-timing.d.ts +16 -0
- package/dist/scheme-timing.js +22 -0
- package/dist/scheme-transition.d.ts +47 -0
- package/dist/scheme-transition.js +101 -0
- package/dist/scoring.d.ts +35 -0
- package/dist/scoring.js +50 -0
- package/dist/shared.d.ts +16 -0
- package/dist/shared.js +20 -0
- package/dist/signer.d.ts +18 -0
- package/dist/signer.js +49 -0
- package/dist/signing-identity.d.ts +1 -0
- package/dist/signing-identity.js +11 -0
- package/dist/status.d.ts +113 -0
- package/dist/status.js +204 -0
- package/dist/succession.d.ts +21 -0
- package/dist/succession.js +67 -0
- package/dist/suite.d.ts +261 -0
- package/dist/suite.js +458 -0
- package/dist/trajectory.d.ts +216 -0
- package/dist/trajectory.js +409 -0
- package/dist/transcript-provenance.d.ts +25 -0
- package/dist/transcript-provenance.js +37 -0
- package/dist/transport-key.d.ts +3 -0
- package/dist/transport-key.js +16 -0
- package/dist/vc.d.ts +29 -0
- package/dist/vc.js +96 -0
- package/dist/verification-bounds.d.ts +5 -0
- package/dist/verification-bounds.js +78 -0
- package/dist/verification-policy.d.ts +32 -0
- package/dist/verification-policy.js +85 -0
- package/dist/witness.d.ts +19 -0
- package/dist/witness.js +80 -0
- package/package.json +65 -4
- package/src/cli.ts +331 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Credential } from "@zanii/credentials";
|
|
2
|
+
import { type BoardCheck, type BodySignature } from "./board.ts";
|
|
3
|
+
import { type LicenceScope } from "./licence.ts";
|
|
4
|
+
import type { Signer } from "./signer.ts";
|
|
5
|
+
import type { SchemeLogsInput } from "./scheme-log.ts";
|
|
6
|
+
export interface RegisteredBody {
|
|
7
|
+
did: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** The accreditation body (EIAC, GAC, IAF member). */
|
|
10
|
+
accreditation_body: string;
|
|
11
|
+
certificate_number: string;
|
|
12
|
+
/** The profiles and levels the accreditation covers. */
|
|
13
|
+
scope: LicenceScope[];
|
|
14
|
+
valid_until: string;
|
|
15
|
+
}
|
|
16
|
+
export interface BodyRegisterInput {
|
|
17
|
+
seq: number;
|
|
18
|
+
prev: string | null;
|
|
19
|
+
bodies: readonly {
|
|
20
|
+
did: string;
|
|
21
|
+
name: string;
|
|
22
|
+
accreditation_body: string;
|
|
23
|
+
certificate_number: string;
|
|
24
|
+
scope: readonly {
|
|
25
|
+
profile: string;
|
|
26
|
+
levels: readonly number[];
|
|
27
|
+
}[];
|
|
28
|
+
valid_until: string;
|
|
29
|
+
}[];
|
|
30
|
+
effectiveFrom: string;
|
|
31
|
+
/** jcsHash of the board that signs (the newest board when it is appended). */
|
|
32
|
+
board: string;
|
|
33
|
+
}
|
|
34
|
+
export type BodyRegisterBody = {
|
|
35
|
+
v: 1;
|
|
36
|
+
type: "certify.body-register";
|
|
37
|
+
seq: number;
|
|
38
|
+
prev: string | null;
|
|
39
|
+
bodies: RegisteredBody[];
|
|
40
|
+
effective_from: string;
|
|
41
|
+
board: string;
|
|
42
|
+
};
|
|
43
|
+
export type BodyRegister = BodyRegisterBody & {
|
|
44
|
+
signatures: BodySignature[];
|
|
45
|
+
};
|
|
46
|
+
declare function registerError(r: Record<string, unknown>): string | null;
|
|
47
|
+
export { registerError as registerFieldError };
|
|
48
|
+
/** Section 1: the unsigned register; bodies sorted by DID, scopes normalised. */
|
|
49
|
+
export declare function bodyRegisterBody(f: BodyRegisterInput): BodyRegisterBody;
|
|
50
|
+
/** A register signed by `signers` (k members of the board it names). */
|
|
51
|
+
export declare function buildBodyRegister(f: BodyRegisterInput, signers: readonly Signer[]): Promise<BodyRegister>;
|
|
52
|
+
export declare const isBodyRegister: (r: unknown) => r is Record<string, unknown>;
|
|
53
|
+
/** Section 2: the register log: the board log verifies; each register at seq i naming i - 1, effective_from rising, signed by k of the board in force. */
|
|
54
|
+
export declare function verifyBodyRegisterLog(registers: readonly unknown[], boards: readonly unknown[], opts?: {
|
|
55
|
+
owner?: string;
|
|
56
|
+
schemeLog?: SchemeLogsInput;
|
|
57
|
+
}): BoardCheck;
|
|
58
|
+
/** Section 3: the register in force at `at` (the newest with effective_from at or before it), or null. */
|
|
59
|
+
export declare function bodyRegisterAt(registers: readonly unknown[], at: string): BodyRegister | null;
|
|
60
|
+
/** Section 3: the DIDs the register in force at `at` lists, accredited at `at` (and, given, for that profile and level). */
|
|
61
|
+
export declare function trustedBodiesAt(registers: readonly unknown[], at: string, scope?: {
|
|
62
|
+
profile?: string;
|
|
63
|
+
level?: number;
|
|
64
|
+
}): string[];
|
|
65
|
+
/** `verifyCertificate`'s `bodyRegister` option: the scheme log's board and register entries. */
|
|
66
|
+
export interface BodyRegisterOptions {
|
|
67
|
+
owner: string;
|
|
68
|
+
boards: readonly unknown[];
|
|
69
|
+
registers: readonly unknown[];
|
|
70
|
+
schemeLog?: SchemeLogsInput;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Section 3: the trusted bodies for a Level 3 certificate's audit from the register in force when
|
|
74
|
+
* it was issued, and the reasons the audit's body is not one of them.
|
|
75
|
+
*/
|
|
76
|
+
export declare function registerTrust(cert: Credential, auditBody: unknown, opts: BodyRegisterOptions): {
|
|
77
|
+
trusted: string[];
|
|
78
|
+
reasons: string[];
|
|
79
|
+
};
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// spec/body-register.md: the board-signed register of trusted certification bodies (ISO/IEC 17065
|
|
2
|
+
// or 17021 accredited), published in the scheme log, which verifiers read instead of a local list.
|
|
3
|
+
import { jcsHash } from "@zanii/core";
|
|
4
|
+
import { bodySignature, boardSignedReasons, verifyBoardLog, withSignatures } from "./board.js";
|
|
5
|
+
import { normaliseScope, scopeError } from "./licence.js";
|
|
6
|
+
import { isDid, isIso, isObj, isSha, ms } from "./shared.js";
|
|
7
|
+
import { logPositionOk } from "./status.js";
|
|
8
|
+
import { schemeAuthority } from "./scheme-authority.js";
|
|
9
|
+
const text = (v, max) => typeof v === "string" && v.length >= 1 && v.length <= max;
|
|
10
|
+
function bodyError(b) {
|
|
11
|
+
if (!isObj(b) || Object.keys(b).length !== 6 || !isDid(b.did))
|
|
12
|
+
return "each body is {did, name, accreditation_body, certificate_number, scope, valid_until}";
|
|
13
|
+
if (!text(b.name, 200) || !text(b.accreditation_body, 200))
|
|
14
|
+
return "name and accreditation_body must be 1 to 200 characters";
|
|
15
|
+
if (!text(b.certificate_number, 100))
|
|
16
|
+
return "certificate_number must be 1 to 100 characters";
|
|
17
|
+
const se = scopeError(b.scope);
|
|
18
|
+
if (se)
|
|
19
|
+
return se;
|
|
20
|
+
if (!isIso(b.valid_until))
|
|
21
|
+
return "valid_until must be an ISO timestamp";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function registerError(r) {
|
|
25
|
+
if (!logPositionOk(r.seq, r.prev))
|
|
26
|
+
return "seq and prev are not a log position";
|
|
27
|
+
const bs = r.bodies;
|
|
28
|
+
if (!Array.isArray(bs) || bs.length < 1 || bs.length > 1000)
|
|
29
|
+
return "bodies must list 1 to 1000 bodies";
|
|
30
|
+
for (const b of bs) {
|
|
31
|
+
const err = bodyError(b);
|
|
32
|
+
if (err)
|
|
33
|
+
return err;
|
|
34
|
+
}
|
|
35
|
+
if (bs.some((b, i) => i > 0 && !(b.did > bs[i - 1].did)))
|
|
36
|
+
return "bodies must be sorted by did, each body once";
|
|
37
|
+
if (!isIso(r.effective_from))
|
|
38
|
+
return "effective_from must be an ISO timestamp";
|
|
39
|
+
if (!isSha(r.board))
|
|
40
|
+
return "board must be a board hash";
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
export { registerError as registerFieldError };
|
|
44
|
+
/** Section 1: the unsigned register; bodies sorted by DID, scopes normalised. */
|
|
45
|
+
export function bodyRegisterBody(f) {
|
|
46
|
+
const bodies = Array.isArray(f.bodies)
|
|
47
|
+
? f.bodies
|
|
48
|
+
.map((b) => ({ did: b?.did, name: b?.name, accreditation_body: b?.accreditation_body, certificate_number: b?.certificate_number, scope: normaliseScope(b?.scope), valid_until: b?.valid_until }))
|
|
49
|
+
.sort((a, b) => (a.did < b.did ? -1 : a.did > b.did ? 1 : 0))
|
|
50
|
+
: f.bodies;
|
|
51
|
+
const body = { v: 1, type: "certify.body-register", seq: f.seq, prev: f.prev, bodies, effective_from: f.effectiveFrom, board: f.board };
|
|
52
|
+
const err = registerError(body);
|
|
53
|
+
if (err)
|
|
54
|
+
throw new Error(err);
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
/** A register signed by `signers` (k members of the board it names). */
|
|
58
|
+
export async function buildBodyRegister(f, signers) {
|
|
59
|
+
const body = bodyRegisterBody(f);
|
|
60
|
+
return withSignatures(body, await Promise.all(signers.map((s) => bodySignature(body, s))));
|
|
61
|
+
}
|
|
62
|
+
export const isBodyRegister = (r) => isObj(r) && r.v === 1 && r.type === "certify.body-register" && Object.keys(r).length === 8;
|
|
63
|
+
/** Section 2: the register log: the board log verifies; each register at seq i naming i - 1, effective_from rising, signed by k of the board in force. */
|
|
64
|
+
export function verifyBodyRegisterLog(registers, boards, opts = {}) {
|
|
65
|
+
if (opts.schemeLog !== undefined) {
|
|
66
|
+
const { reasons } = schemeAuthority(opts.schemeLog, opts.owner, { "certify.board": boards, "certify.body-register": registers });
|
|
67
|
+
return { ok: reasons.length === 0, reasons };
|
|
68
|
+
}
|
|
69
|
+
const reasons = [...verifyBoardLog(boards, opts).reasons];
|
|
70
|
+
if (!Array.isArray(registers) || registers.length === 0)
|
|
71
|
+
return { ok: false, reasons: [...reasons, "the body register log is empty"] };
|
|
72
|
+
const bl = Array.isArray(boards) ? boards : [];
|
|
73
|
+
registers.forEach((r, i) => {
|
|
74
|
+
if (!isBodyRegister(r)) {
|
|
75
|
+
reasons.push(`register ${i} is not a body register (v1)`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const err = registerError(r);
|
|
79
|
+
if (err) {
|
|
80
|
+
reasons.push(`register ${i} is malformed: ${err}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const prev = i > 0 ? registers[i - 1] : null;
|
|
84
|
+
if (r.seq !== i)
|
|
85
|
+
reasons.push(`register ${i} is not at position ${i}`);
|
|
86
|
+
if (r.prev !== (i > 0 ? jcsHash(prev) : null))
|
|
87
|
+
reasons.push(`register ${i} does not name the previous register`);
|
|
88
|
+
if (i > 0 && isBodyRegister(prev) && !(ms(r.effective_from) > ms(prev.effective_from)))
|
|
89
|
+
reasons.push(`register ${i} does not take effect after the previous register`);
|
|
90
|
+
reasons.push(...boardSignedReasons(r, bl, "body register").map((x) => `register ${i}: ${x}`));
|
|
91
|
+
});
|
|
92
|
+
return { ok: reasons.length === 0, reasons };
|
|
93
|
+
}
|
|
94
|
+
/** Section 3: the register in force at `at` (the newest with effective_from at or before it), or null. */
|
|
95
|
+
export function bodyRegisterAt(registers, at) {
|
|
96
|
+
let found = null;
|
|
97
|
+
for (const r of Array.isArray(registers) ? registers : [])
|
|
98
|
+
if (isBodyRegister(r) && ms(r.effective_from) <= ms(at))
|
|
99
|
+
found = r;
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
/** Section 3: the DIDs the register in force at `at` lists, accredited at `at` (and, given, for that profile and level). */
|
|
103
|
+
export function trustedBodiesAt(registers, at, scope = {}) {
|
|
104
|
+
const r = bodyRegisterAt(registers, at);
|
|
105
|
+
return (r?.bodies ?? []).filter((b) => bodyCovers(b, at, scope).length === 0).map((b) => b.did);
|
|
106
|
+
}
|
|
107
|
+
function bodyCovers(b, at, scope) {
|
|
108
|
+
const out = [];
|
|
109
|
+
if (!(ms(b.valid_until) >= ms(at)))
|
|
110
|
+
out.push("the certification body's accreditation had expired when the certificate was issued");
|
|
111
|
+
if (scope.profile !== undefined) {
|
|
112
|
+
const s = b.scope.find((e) => e.profile === scope.profile);
|
|
113
|
+
if (!s || !s.levels.includes(scope.level))
|
|
114
|
+
out.push(`the certification body's accreditation does not cover profile ${scope.profile} at level ${String(scope.level)}`);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Section 3: the trusted bodies for a Level 3 certificate's audit from the register in force when
|
|
120
|
+
* it was issued, and the reasons the audit's body is not one of them.
|
|
121
|
+
*/
|
|
122
|
+
export function registerTrust(cert, auditBody, opts) {
|
|
123
|
+
const authority = opts.schemeLog !== undefined ? schemeAuthority(opts.schemeLog, opts.owner, { "certify.board": opts.boards, "certify.body-register": opts.registers }) : null;
|
|
124
|
+
const reasons = [...(authority ? authority.reasons : verifyBodyRegisterLog(opts.registers, opts.boards, { owner: opts.owner }).reasons)];
|
|
125
|
+
const at = String(cert?.issued_at);
|
|
126
|
+
const r = reasons.length === 0 ? bodyRegisterAt(opts.registers.filter(x => !authority?.canceled.size || !authority.canceled.has(jcsHash(x))), at) : null;
|
|
127
|
+
if (reasons.length === 0 && !r)
|
|
128
|
+
reasons.push("no body register was in force when the certificate was issued");
|
|
129
|
+
const b = r?.bodies.find((x) => x.did === auditBody);
|
|
130
|
+
const claim = isObj(cert?.claim) ? cert.claim : {};
|
|
131
|
+
const covers = b ? bodyCovers(b, at, { profile: String(claim.profile), level: claim.level }) : [];
|
|
132
|
+
reasons.push(...covers);
|
|
133
|
+
return { trusted: b && covers.length === 0 ? [b.did] : [], reasons };
|
|
134
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Signer } from "./signer.ts";
|
|
2
|
+
export interface SuiteRef {
|
|
3
|
+
id: string;
|
|
4
|
+
version: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Rotation {
|
|
7
|
+
v: 1 | 2;
|
|
8
|
+
type: "certify.suite-rotation";
|
|
9
|
+
issuer: string;
|
|
10
|
+
seq: number;
|
|
11
|
+
prev: string | null;
|
|
12
|
+
active_from: string;
|
|
13
|
+
suites: SuiteRef[];
|
|
14
|
+
profiles?: string[];
|
|
15
|
+
issued_at: string;
|
|
16
|
+
signer: string;
|
|
17
|
+
sig: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RotationInput {
|
|
20
|
+
seq: number;
|
|
21
|
+
prev: string | null;
|
|
22
|
+
activeFrom: string;
|
|
23
|
+
suites: readonly SuiteRef[];
|
|
24
|
+
issuedAt: string;
|
|
25
|
+
profiles?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** Sign a rotation record as the lab issuer. `suites` is sorted here. */
|
|
28
|
+
export declare function buildRotation(fields: RotationInput, signer: Signer): Promise<Rotation>;
|
|
29
|
+
export interface RotationCheck {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
reasons: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Section 6.2: a rotation log is valid when every record is well formed and signed by `issuer`,
|
|
35
|
+
* `seq` counts from 0 with no gap, each `prev` is the hash of the record before, and neither
|
|
36
|
+
* `issued_at` nor per-lane `active_from` goes backwards.
|
|
37
|
+
*/
|
|
38
|
+
export declare function verifyRotationLog(log: readonly unknown[], opts: {
|
|
39
|
+
issuer: string;
|
|
40
|
+
}): RotationCheck;
|
|
41
|
+
/** Exact-profile override if effective, else global fallback; no profile selects global only. */
|
|
42
|
+
export declare function activeRotation<T extends {
|
|
43
|
+
seq: number;
|
|
44
|
+
active_from: string;
|
|
45
|
+
v?: number;
|
|
46
|
+
profiles?: readonly string[];
|
|
47
|
+
}>(log: readonly T[], at: string, profile?: string): T | null;
|
|
48
|
+
/** Standalone integrity validation; callers must separately authenticate the issuer policy. */
|
|
49
|
+
export declare function verifyRotationRecord(value: unknown): RotationCheck;
|
package/dist/rotation.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// spec/suites.md section 6: the suite rotation log. Each record names the held-out suite and
|
|
2
|
+
// scenario library versions plans draw from, from `active_from` on; signed by the lab issuer,
|
|
3
|
+
// chained by `prev`, append only.
|
|
4
|
+
import { jcsHash, verifySignedObject } from "@zanii/core";
|
|
5
|
+
import { isDid, isInt, isIso, isObj, isSha, ms, SLUG } from "./shared.js";
|
|
6
|
+
import { signerDid, signWith } from "./signer.js";
|
|
7
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
8
|
+
/** The section 6.1 field rules; the first broken rule, or null. `suites` must already be sorted. */
|
|
9
|
+
function fieldError(r) {
|
|
10
|
+
if (r.v === 2) {
|
|
11
|
+
const scopes = r.profiles;
|
|
12
|
+
if (!Array.isArray(scopes) || scopes.length < 1 || scopes.length > 64 || !scopes.every(p => typeof p === "string" && p === p.trim() && p.length <= 160 && /^[a-z0-9][a-z0-9-]*\/[1-9][0-9]{0,6}$/.test(p) && Number(p.split("/")[1]) <= 1000000) || scopes.some((p, i) => i > 0 && !(scopes[i - 1] < p)))
|
|
13
|
+
return "profiles must list 1 to 64 sorted distinct exact profile/version references";
|
|
14
|
+
}
|
|
15
|
+
if (!isDid(r.issuer))
|
|
16
|
+
return "issuer must be a did:key";
|
|
17
|
+
if (!isInt(r.seq, 0, 1_000_000_000))
|
|
18
|
+
return "seq must be an integer from 0";
|
|
19
|
+
if (r.seq === 0 ? r.prev !== null : !isSha(r.prev))
|
|
20
|
+
return "prev must be null at seq 0 and the previous record's hash after";
|
|
21
|
+
if (!isIso(r.active_from) || !isIso(r.issued_at))
|
|
22
|
+
return "active_from and issued_at must be ISO UTC timestamps";
|
|
23
|
+
if (ms(r.active_from) < ms(r.issued_at))
|
|
24
|
+
return "active_from must not be before issued_at";
|
|
25
|
+
const s = r.suites;
|
|
26
|
+
if (!Array.isArray(s) || s.length < 1 || s.length > 200)
|
|
27
|
+
return "suites must list 1 to 200 suites";
|
|
28
|
+
for (const x of s)
|
|
29
|
+
if (!isObj(x) || Object.keys(x).length !== 2 || typeof x.id !== "string" || !SLUG.test(x.id) || !isInt(x.version, 1, 1_000_000))
|
|
30
|
+
return "each suite is {id, version}";
|
|
31
|
+
const ids = s.map((x) => x.id);
|
|
32
|
+
for (let i = 1; i < ids.length; i++)
|
|
33
|
+
if (!(ids[i - 1] < ids[i]))
|
|
34
|
+
return "suites must be sorted by id and distinct";
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/** Sign a rotation record as the lab issuer. `suites` is sorted here. */
|
|
38
|
+
export async function buildRotation(fields, signer) {
|
|
39
|
+
const unsigned = {
|
|
40
|
+
v: fields.profiles === undefined ? 1 : 2,
|
|
41
|
+
type: "certify.suite-rotation",
|
|
42
|
+
issuer: signerDid(signer),
|
|
43
|
+
seq: fields.seq,
|
|
44
|
+
prev: fields.prev,
|
|
45
|
+
active_from: fields.activeFrom,
|
|
46
|
+
suites: Array.isArray(fields.suites)
|
|
47
|
+
? [...fields.suites].map((x) => ({ id: x.id, version: x.version })).sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
48
|
+
: fields.suites,
|
|
49
|
+
issued_at: fields.issuedAt,
|
|
50
|
+
...(fields.profiles === undefined ? {} : { profiles: [...fields.profiles].sort() }),
|
|
51
|
+
};
|
|
52
|
+
const err = fieldError(unsigned);
|
|
53
|
+
if (err)
|
|
54
|
+
throw new Error(err);
|
|
55
|
+
return signWith(unsigned, signer);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Section 6.2: a rotation log is valid when every record is well formed and signed by `issuer`,
|
|
59
|
+
* `seq` counts from 0 with no gap, each `prev` is the hash of the record before, and neither
|
|
60
|
+
* `issued_at` nor per-lane `active_from` goes backwards.
|
|
61
|
+
*/
|
|
62
|
+
export function verifyRotationLog(log, opts) {
|
|
63
|
+
if (!Array.isArray(log) || log.length > 10000 || !withinVerificationBudget(log, { maxBytes: 64 * 1024 * 1024, maxDepth: 8, maxNodes: 1000000 }))
|
|
64
|
+
return { ok: false, reasons: ["rotation history exceeds its verification budget"] };
|
|
65
|
+
const reasons = [];
|
|
66
|
+
let prev = null;
|
|
67
|
+
const lanes = new Map();
|
|
68
|
+
log.forEach((x, i) => {
|
|
69
|
+
const r = isObj(x) ? x : {};
|
|
70
|
+
const at = `record ${i}`;
|
|
71
|
+
if ((r.v !== 1 && r.v !== 2) || r.type !== "certify.suite-rotation" || Object.keys(r).length !== (r.v === 2 ? 11 : 10) || (r.v === 1 && "profiles" in r)) {
|
|
72
|
+
reasons.push(`${at}: not a suite rotation (v1)`);
|
|
73
|
+
prev = r;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!verifySignedObject(r) || r.signer !== r.issuer)
|
|
77
|
+
reasons.push(`${at}: bad signature`);
|
|
78
|
+
if (r.issuer !== opts.issuer)
|
|
79
|
+
reasons.push(`${at}: not signed by the lab issuer`);
|
|
80
|
+
const err = fieldError(r);
|
|
81
|
+
if (err)
|
|
82
|
+
reasons.push(`${at}: ${err}`);
|
|
83
|
+
if (r.seq !== i)
|
|
84
|
+
reasons.push(`${at}: seq is not ${i}`);
|
|
85
|
+
if (prev) {
|
|
86
|
+
if (r.prev !== jcsHash(prev))
|
|
87
|
+
reasons.push(`${at}: prev is not the hash of the record before`);
|
|
88
|
+
if (ms(r.issued_at) < ms(prev.issued_at))
|
|
89
|
+
reasons.push(`${at}: issued_at or active_from goes backwards`);
|
|
90
|
+
}
|
|
91
|
+
const scopes = r.v === 2 && Array.isArray(r.profiles) ? r.profiles : ["*"];
|
|
92
|
+
if (scopes.some(scope => lanes.has(String(scope)) && ms(r.active_from) < lanes.get(String(scope))))
|
|
93
|
+
reasons.push(`${at}: issued_at or active_from goes backwards`);
|
|
94
|
+
for (const scope of scopes)
|
|
95
|
+
lanes.set(String(scope), ms(r.active_from));
|
|
96
|
+
prev = r;
|
|
97
|
+
});
|
|
98
|
+
return { ok: reasons.length === 0, reasons };
|
|
99
|
+
}
|
|
100
|
+
/** Exact-profile override if effective, else global fallback; no profile selects global only. */
|
|
101
|
+
export function activeRotation(log, at, profile) {
|
|
102
|
+
let best = null, scoped = null;
|
|
103
|
+
for (const r of log)
|
|
104
|
+
if (ms(r.active_from) <= ms(at)) {
|
|
105
|
+
if (r.v === 2) {
|
|
106
|
+
if (profile && r.profiles?.includes(profile) && (!scoped || r.seq > scoped.seq))
|
|
107
|
+
scoped = r;
|
|
108
|
+
}
|
|
109
|
+
else if (!best || r.seq > best.seq)
|
|
110
|
+
best = r;
|
|
111
|
+
}
|
|
112
|
+
return scoped ?? best;
|
|
113
|
+
}
|
|
114
|
+
/** Standalone integrity validation; callers must separately authenticate the issuer policy. */
|
|
115
|
+
export function verifyRotationRecord(value) {
|
|
116
|
+
if (!withinVerificationBudget(value, { maxBytes: 65536, maxDepth: 6, maxNodes: 2000 }))
|
|
117
|
+
return { ok: false, reasons: ["rotation record exceeds its verification budget"] };
|
|
118
|
+
if (!isObj(value) || (value.v !== 1 && value.v !== 2) || value.type !== "certify.suite-rotation" || Object.keys(value).length !== (value.v === 2 ? 11 : 10) || (value.v === 1 && "profiles" in value))
|
|
119
|
+
return { ok: false, reasons: ["invalid suite rotation format"] };
|
|
120
|
+
const err = fieldError(value), reasons = err ? [err] : [];
|
|
121
|
+
if (value.signer !== value.issuer || !verifySignedObject(value))
|
|
122
|
+
reasons.push("bad rotation signature");
|
|
123
|
+
return { ok: reasons.length === 0, reasons };
|
|
124
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type SignedTreeHead } from "@zanii/core";
|
|
2
|
+
import { type Credential } from "@zanii/credentials";
|
|
3
|
+
import { type LicenceOwnerHistoryPolicy } from "./licence-owner.ts";
|
|
4
|
+
export interface RoyaltyFeedOptions {
|
|
5
|
+
owner: string;
|
|
6
|
+
licence: unknown;
|
|
7
|
+
pages: readonly unknown[];
|
|
8
|
+
issuers: readonly string[];
|
|
9
|
+
checkpoint: SignedTreeHead;
|
|
10
|
+
certificates: readonly Credential[];
|
|
11
|
+
now: string;
|
|
12
|
+
maxCheckpointAgeSeconds: number;
|
|
13
|
+
/** Proposal accounting only; does not imply owner acceptance. Defaults false. */
|
|
14
|
+
allowUncountersigned?: boolean;
|
|
15
|
+
ownerHistory?: LicenceOwnerHistoryPolicy;
|
|
16
|
+
}
|
|
17
|
+
export interface RoyaltyFeedCheck {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
reasons: string[];
|
|
20
|
+
expected: string[];
|
|
21
|
+
missingEvidence: string[];
|
|
22
|
+
}
|
|
23
|
+
/** Reconcile only authenticated, complete supplied history; status `since` never substitutes for signed issuance time. */
|
|
24
|
+
export declare function reconcileRoyaltyStatementFeed(statement: unknown, options: RoyaltyFeedOptions): RoyaltyFeedCheck;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jcsHash, publicKeyFromDid } from "@zanii/core";
|
|
2
|
+
import { credentialHash } from "@zanii/credentials";
|
|
3
|
+
import { claimReasons } from "./certificate.js";
|
|
4
|
+
import { verifyFeed } from "./federation.js";
|
|
5
|
+
import { verifyLicence, verifyRoyaltyStatement } from "./licence.js";
|
|
6
|
+
import { verifyLicenceWithOwnerHistory } from "./licence-owner.js";
|
|
7
|
+
import { isInt, isIso, isObj, ms } from "./shared.js";
|
|
8
|
+
import { signatureOk } from "./signer.js";
|
|
9
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
10
|
+
/** Reconcile only authenticated, complete supplied history; status `since` never substitutes for signed issuance time. */
|
|
11
|
+
export function reconcileRoyaltyStatementFeed(statement, options) {
|
|
12
|
+
const denied = (reasons) => ({ ok: false, reasons, expected: [], missingEvidence: [] });
|
|
13
|
+
if (!withinVerificationBudget({ statement, options }))
|
|
14
|
+
return denied(["royalty reconciliation evidence exceeds the JSON budget"]);
|
|
15
|
+
if (!isObj(options) || ![8, 9, 10].includes(Object.keys(options).length) || ["owner", "licence", "pages", "issuers", "checkpoint", "certificates", "now", "maxCheckpointAgeSeconds"].some(k => !Object.hasOwn(options, k))
|
|
16
|
+
|| Object.keys(options).some(k => !["owner", "licence", "pages", "issuers", "checkpoint", "certificates", "now", "maxCheckpointAgeSeconds", "allowUncountersigned", "ownerHistory"].includes(k))
|
|
17
|
+
|| (options.ownerHistory !== undefined && !isObj(options.ownerHistory))
|
|
18
|
+
|| (Object.hasOwn(options, "allowUncountersigned") && typeof options.allowUncountersigned !== "boolean") || !Array.isArray(options.pages) || options.pages.length < 1 || options.pages.length > 1000
|
|
19
|
+
|| !Array.isArray(options.certificates) || options.certificates.length > 50_000 || !Array.isArray(options.issuers) || options.issuers.length < 1 || options.issuers.length > 64
|
|
20
|
+
|| !options.issuers.every((d) => typeof d === "string" && publicKeyFromDid(d)) || !isIso(options.now) || !isInt(options.maxCheckpointAgeSeconds, 1, 31_536_000))
|
|
21
|
+
return denied(["invalid royalty reconciliation policy or evidence bounds"]);
|
|
22
|
+
let count = 0;
|
|
23
|
+
for (const page of options.pages) {
|
|
24
|
+
if (!isObj(page) || !Array.isArray(page.entries) || (count += page.entries.length) > 50_000)
|
|
25
|
+
return denied(["royalty reconciliation feed exceeds 50000 entries"]);
|
|
26
|
+
}
|
|
27
|
+
const ownerPolicy = options.ownerHistory !== undefined ? { ownerHistory: options.ownerHistory } : {};
|
|
28
|
+
const checkedLicence = options.ownerHistory !== undefined ? verifyLicenceWithOwnerHistory(options.licence, { root: options.owner, ...options.ownerHistory }) : verifyLicence(options.licence, { owner: options.owner });
|
|
29
|
+
const checkedStatement = verifyRoyaltyStatement(statement, { owner: options.owner, licence: options.licence, ...ownerPolicy });
|
|
30
|
+
const statementReasons = (reasons) => options.allowUncountersigned === true ? reasons.filter(r => r !== "royalty statement is not countersigned by the scheme owner") : reasons;
|
|
31
|
+
const feed = verifyFeed(options.pages, { issuers: options.issuers, ...(isObj(options.checkpoint) ? { checkpoint: options.checkpoint } : {}) });
|
|
32
|
+
const reasons = [...checkedLicence.reasons, ...statementReasons(checkedStatement.reasons), ...feed.reasons];
|
|
33
|
+
for (const page of options.pages)
|
|
34
|
+
if (isObj(page) && Array.isArray(page.entries) && page.entries.some((e) => isObj(e) && ms(e.since) > ms(page.issued_at)))
|
|
35
|
+
reasons.push("royalty feed status time is after its page");
|
|
36
|
+
if (!isObj(options.licence) || !options.issuers.includes(String(options.licence.licensee)))
|
|
37
|
+
reasons.push("licence licensee is not trusted for this feed");
|
|
38
|
+
const cp = options.checkpoint, s = isObj(statement) ? statement : {}, period = isObj(s.period) ? s.period : {};
|
|
39
|
+
if (!isObj(cp) || !options.issuers.includes(String(cp.log_id)))
|
|
40
|
+
reasons.push("royalty feed checkpoint is not signed by a trusted instance issuer");
|
|
41
|
+
const last = options.pages.at(-1);
|
|
42
|
+
if (!isObj(cp) || !isIso(cp.ts) || ms(cp.ts) > ms(options.now) || ms(options.now) - ms(cp.ts) >= options.maxCheckpointAgeSeconds * 1000)
|
|
43
|
+
reasons.push("royalty feed checkpoint is missing, stale or in the future");
|
|
44
|
+
if (!isIso(period.until) || !isObj(last) || !isIso(last.issued_at) || ms(last.issued_at) < ms(period.until) || ms(cp?.ts) < ms(last.issued_at) || ms(last.issued_at) > ms(options.now))
|
|
45
|
+
reasons.push("royalty feed does not cover the complete reporting period");
|
|
46
|
+
if (reasons.length)
|
|
47
|
+
return denied([...new Set(reasons)]);
|
|
48
|
+
const licenceHash = jcsHash(options.licence), docs = new Map(), missingEvidence = [], expected = [];
|
|
49
|
+
for (const cert of options.certificates) {
|
|
50
|
+
if (!isObj(cert) || cert.v !== 1 || cert.type !== "credential" || typeof cert.issuer !== "string" || !options.issuers.includes(cert.issuer) || !isIso(cert.issued_at) || !isObj(cert.claim)
|
|
51
|
+
|| !isIso(cert.exp) || ms(cert.issued_at) > ms(options.now) || claimReasons(cert).length)
|
|
52
|
+
return denied(["invalid signed original certificate evidence"]);
|
|
53
|
+
const { sig, ...unsigned } = cert;
|
|
54
|
+
if (!signatureOk(unsigned, sig, cert.issuer))
|
|
55
|
+
return denied(["invalid signed original certificate evidence"]);
|
|
56
|
+
const original = cert;
|
|
57
|
+
const hash = credentialHash(original);
|
|
58
|
+
if (docs.has(hash))
|
|
59
|
+
return denied(["duplicate original certificate evidence"]);
|
|
60
|
+
docs.set(hash, original);
|
|
61
|
+
}
|
|
62
|
+
for (const entry of feed.entries) {
|
|
63
|
+
if (entry.licence !== licenceHash)
|
|
64
|
+
continue;
|
|
65
|
+
const cert = docs.get(entry.certificate);
|
|
66
|
+
if (!cert) {
|
|
67
|
+
missingEvidence.push(entry.certificate);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const claim = cert.claim;
|
|
71
|
+
if (claim.licence !== licenceHash || claim.profile !== entry.profile || claim.level !== entry.level)
|
|
72
|
+
reasons.push(`original certificate does not match feed entry ${entry.certificate}`);
|
|
73
|
+
if (ms(cert.issued_at) > ms(entry.since) || ms(cert.issued_at) > ms(last.issued_at))
|
|
74
|
+
reasons.push(`original certificate issuance is after its feed state ${entry.certificate}`);
|
|
75
|
+
if (ms(cert.issued_at) >= ms(period.from) && ms(cert.issued_at) < ms(period.until))
|
|
76
|
+
expected.push(entry.certificate);
|
|
77
|
+
}
|
|
78
|
+
const feedHashes = new Set(feed.entries.map((e) => e.certificate));
|
|
79
|
+
for (const [hash, cert] of docs)
|
|
80
|
+
if (isObj(cert.claim) && cert.claim.licence === licenceHash && !feedHashes.has(hash))
|
|
81
|
+
reasons.push(`original certificate is missing from the feed ${hash}`);
|
|
82
|
+
if (missingEvidence.length)
|
|
83
|
+
reasons.push("signed original certificates are missing for the licence feed");
|
|
84
|
+
expected.sort();
|
|
85
|
+
missingEvidence.sort();
|
|
86
|
+
reasons.push(...statementReasons(verifyRoyaltyStatement(statement, { owner: options.owner, licence: options.licence, certificates: expected, ...ownerPolicy }).reasons));
|
|
87
|
+
return { ok: reasons.length === 0, reasons: [...new Set(reasons)], expected, missingEvidence };
|
|
88
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type RuntimeNitroBinding } from "./runtime-nitro.ts";
|
|
2
|
+
import { type RuntimeApproval, type RuntimePolicy } from "./runtime.ts";
|
|
3
|
+
import { type Signer } from "./signer.ts";
|
|
4
|
+
export interface RuntimeAttestationBinding extends RuntimeNitroBinding {
|
|
5
|
+
/** The Nitro nonce is the authority's challenge, not the challenge request's replay nonce. */
|
|
6
|
+
nonce: string;
|
|
7
|
+
/** Exact Ed25519 SubjectPublicKeyInfo DER (44 bytes). */
|
|
8
|
+
publicKey: Uint8Array;
|
|
9
|
+
/** UTF-8 bytes of runtimeNitroBindingHash, including its sha256: prefix. */
|
|
10
|
+
userData: Uint8Array;
|
|
11
|
+
}
|
|
12
|
+
export interface RuntimeReporterOptions {
|
|
13
|
+
authority: string;
|
|
14
|
+
issuers: readonly string[];
|
|
15
|
+
approval: RuntimeApproval;
|
|
16
|
+
policy: RuntimePolicy;
|
|
17
|
+
deployment: string;
|
|
18
|
+
signer: Signer;
|
|
19
|
+
attest: (binding: RuntimeAttestationBinding) => Promise<Uint8Array>;
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
/** Optional agent-bound admission credential, separate from workload proof. */
|
|
22
|
+
transportKey?: string;
|
|
23
|
+
clock?: () => number;
|
|
24
|
+
intervalMs?: number;
|
|
25
|
+
onResult?: (result: RuntimeReportResult | {
|
|
26
|
+
accepted: false;
|
|
27
|
+
error: Error;
|
|
28
|
+
}) => void | Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface RuntimeReportResult {
|
|
31
|
+
accepted: true;
|
|
32
|
+
deployment: string;
|
|
33
|
+
evidence_until: string;
|
|
34
|
+
}
|
|
35
|
+
/** One reporter has at most one underlying operation, including an uncancellable attester after timeout. */
|
|
36
|
+
export declare function createRuntimeReporter(options: RuntimeReporterOptions): {
|
|
37
|
+
send(): Promise<RuntimeReportResult>;
|
|
38
|
+
start(): void;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
};
|