@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,46 @@
|
|
|
1
|
+
import { jcsHash } from "@zanii/core";
|
|
2
|
+
import { isObj, isSha } from "./shared.js";
|
|
3
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
4
|
+
export const SCHEME_TEXT_MAX_BYTES = 4_194_304;
|
|
5
|
+
const scheme = (value) => typeof value === "string" && /^zanii-certify\/(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$/.exec(value)?.[0] === value;
|
|
6
|
+
const path = (value) => typeof value === "string" && /^[a-z0-9][a-z0-9._/-]{0,127}$/.exec(value)?.[0] === value && !value.endsWith("/") && value.split("/").every(part => part !== "" && part !== "." && part !== "..");
|
|
7
|
+
const text = (value) => typeof value === "string" && value.length <= 524_288 && value.isWellFormed() && Buffer.byteLength(value, "utf8") <= 524_288 && !value.startsWith("\uFEFF") && !/[\x00-\x08\x0b-\x1f\x7f]/.test(value);
|
|
8
|
+
function reasons(doc) {
|
|
9
|
+
if (!withinVerificationBudget(doc, { maxBytes: SCHEME_TEXT_MAX_BYTES, maxNodes: 1000, maxDepth: 4 }))
|
|
10
|
+
return ["scheme text exceeds its 4 MiB JSON budget"];
|
|
11
|
+
if (!isObj(doc) || Object.keys(doc).length !== 4 || doc.v !== 1 || doc.type !== "certify.scheme-text" || !scheme(doc.scheme) || !Array.isArray(doc.files) || doc.files.length < 1 || doc.files.length > 64)
|
|
12
|
+
return ["invalid scheme text bundle or file count"];
|
|
13
|
+
let previous = "";
|
|
14
|
+
for (const file of doc.files) {
|
|
15
|
+
if (!isObj(file) || Object.keys(file).length !== 2 || !path(file.path) || file.path <= previous || !text(file.text) || file.text.includes("\r"))
|
|
16
|
+
return ["scheme files must be sorted distinct relative paths and bounded UTF-8 text with LF endings"];
|
|
17
|
+
previous = file.path;
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
/** LF normalization is explicit; Unicode content is retained byte for byte otherwise. */
|
|
22
|
+
export function buildSchemeTextBundle(input) {
|
|
23
|
+
if (!withinVerificationBudget(input, { maxBytes: SCHEME_TEXT_MAX_BYTES, maxNodes: 1000, maxDepth: 4 }) || !isObj(input) || Object.keys(input).length !== 2 || !scheme(input.scheme) || !Array.isArray(input.files) || input.files.length < 1 || input.files.length > 64)
|
|
24
|
+
throw new Error("invalid bounded scheme text input");
|
|
25
|
+
const files = input.files.map(file => { if (!isObj(file) || Object.keys(file).length !== 2 || !path(file.path) || typeof file.text !== "string")
|
|
26
|
+
throw new Error("invalid scheme file"); return { path: file.path, text: file.text.replace(/\r\n?/g, "\n") }; }).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
27
|
+
const bundle = { v: 1, type: "certify.scheme-text", scheme: input.scheme, files }, bad = reasons(bundle);
|
|
28
|
+
if (bad.length)
|
|
29
|
+
throw new Error(bad[0]);
|
|
30
|
+
return bundle;
|
|
31
|
+
}
|
|
32
|
+
/** The released file is these canonical JSON bytes; a digest alone is not board approval. */
|
|
33
|
+
export function schemeTextBundleHash(doc) { const bad = reasons(doc); if (bad.length)
|
|
34
|
+
throw new Error(bad[0]); return jcsHash(doc); }
|
|
35
|
+
export function verifySchemeTextBundle(doc, expected) {
|
|
36
|
+
if (!isObj(expected) || Object.keys(expected).length !== 2 || !scheme(expected.scheme) || typeof expected.sha256 !== "string" || expected.sha256.length !== 71 || !isSha(expected.sha256))
|
|
37
|
+
return { ok: false, reasons: ["independently selected scheme and released digest are required"] };
|
|
38
|
+
const bad = reasons(doc);
|
|
39
|
+
if (bad.length)
|
|
40
|
+
return { ok: false, reasons: bad };
|
|
41
|
+
if (doc.scheme !== expected.scheme)
|
|
42
|
+
bad.push("scheme text names another scheme version");
|
|
43
|
+
if (jcsHash(doc) !== expected.sha256)
|
|
44
|
+
bad.push("scheme text does not match the released content digest");
|
|
45
|
+
return { ok: bad.length === 0, reasons: bad };
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SchemeTimingPolicy {
|
|
2
|
+
v: 1;
|
|
3
|
+
type: "certify.scheme-timing-policy";
|
|
4
|
+
max_future_s: number;
|
|
5
|
+
}
|
|
6
|
+
export type SchemeTimingCode = "SCHEME_TIMING_POLICY" | "SCHEME_EFFECTIVE_INVALID" | "SCHEME_EFFECTIVE_PAST" | "SCHEME_EFFECTIVE_TOO_FAR";
|
|
7
|
+
export interface SchemeTimingCheck {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
codes: SchemeTimingCode[];
|
|
10
|
+
reasons: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Check again under the append lock using the trusted commit clock. No clock skew allowance. */
|
|
13
|
+
export declare function verifySchemeEffectiveTime(doc: unknown, opts: {
|
|
14
|
+
policy: unknown;
|
|
15
|
+
now: string;
|
|
16
|
+
}): SchemeTimingCheck;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Admission policy for NEW owner submissions, never a reinterpretation of signed history.
|
|
2
|
+
import { isInt, isIso, isObj, ms } from "./shared.js";
|
|
3
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
4
|
+
const TYPES = ["certify.board", "certify.release", "certify.release-withdrawal", "certify.body-register"];
|
|
5
|
+
/** Check again under the append lock using the trusted commit clock. No clock skew allowance. */
|
|
6
|
+
export function verifySchemeEffectiveTime(doc, opts) {
|
|
7
|
+
const codes = [], reasons = [];
|
|
8
|
+
const fail = (code, reason) => { codes.push(code); reasons.push(reason); };
|
|
9
|
+
const p = opts?.policy;
|
|
10
|
+
if (!isObj(p) || Object.keys(p).length !== 3 || p.v !== 1 || p.type !== "certify.scheme-timing-policy" || !isInt(p.max_future_s, 1, 2_592_000))
|
|
11
|
+
fail("SCHEME_TIMING_POLICY", "a closed scheme timing policy with max_future_s from 1 to 2592000 is required");
|
|
12
|
+
if (!isIso(opts?.now) || !withinVerificationBudget(doc, { maxBytes: 1_048_576, maxNodes: 100_000 }) || !isObj(doc) || !TYPES.includes(doc.type) || !isIso(doc.effective_from))
|
|
13
|
+
fail("SCHEME_EFFECTIVE_INVALID", "a supported scheme entry, effective_from and trusted reference time are required");
|
|
14
|
+
if (!codes.length && isObj(doc) && isObj(p)) {
|
|
15
|
+
const delta = ms(doc.effective_from) - ms(opts.now);
|
|
16
|
+
if (delta < 0)
|
|
17
|
+
fail("SCHEME_EFFECTIVE_PAST", "effective_from must not precede the protected append time");
|
|
18
|
+
else if (delta > Number(p.max_future_s) * 1000)
|
|
19
|
+
fail("SCHEME_EFFECTIVE_TOO_FAR", "effective_from exceeds the scheme timing policy's future limit");
|
|
20
|
+
}
|
|
21
|
+
return { ok: codes.length === 0, codes, reasons };
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type BodySignature } from "./board.ts";
|
|
2
|
+
import { type SchemeAppendStatement } from "./scheme-log.ts";
|
|
3
|
+
import { type Signer } from "./signer.ts";
|
|
4
|
+
export interface SchemeTransitionBody {
|
|
5
|
+
v: 1;
|
|
6
|
+
type: "certify.board-transition";
|
|
7
|
+
owner: string;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
epoch: number;
|
|
11
|
+
at: string;
|
|
12
|
+
cancel: string[];
|
|
13
|
+
}
|
|
14
|
+
export type SchemeTransition = SchemeTransitionBody & {
|
|
15
|
+
signatures: BodySignature[];
|
|
16
|
+
};
|
|
17
|
+
export interface SchemeTransitionAnchor {
|
|
18
|
+
statement: SchemeAppendStatement;
|
|
19
|
+
append: unknown;
|
|
20
|
+
receipt: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface SchemeTransitionContext {
|
|
23
|
+
owner: string;
|
|
24
|
+
/** Authenticated newest board, exact proposed next board and next scheme-log position. */
|
|
25
|
+
board: unknown;
|
|
26
|
+
nextBoard: unknown;
|
|
27
|
+
index: number;
|
|
28
|
+
previousAt: string;
|
|
29
|
+
/** ONLY authenticated prefix entries. Canceled records remain in this history. */
|
|
30
|
+
entry: (hash: string) => unknown;
|
|
31
|
+
anchor: SchemeTransitionAnchor;
|
|
32
|
+
nextAnchor: SchemeTransitionAnchor;
|
|
33
|
+
}
|
|
34
|
+
export declare function buildSchemeTransition(fields: Omit<SchemeTransitionBody, "v" | "type">, signers: readonly Signer[]): Promise<SchemeTransition>;
|
|
35
|
+
/** Strict effect verification: both authenticated append receipts are mandatory. */
|
|
36
|
+
export declare function verifySchemeTransition(doc: unknown, c: SchemeTransitionContext): {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
reasons: string[];
|
|
39
|
+
canceled: string[];
|
|
40
|
+
};
|
|
41
|
+
/** Admission only. This does NOT authorize cancellation or a new board before anchoring. */
|
|
42
|
+
export declare function verifySchemeTransitionProposal(doc: unknown, c: Omit<SchemeTransitionContext, "anchor" | "nextAnchor"> & {
|
|
43
|
+
now: string;
|
|
44
|
+
}): {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
reasons: string[];
|
|
47
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// An explicit anchored cancellation of future entries, followed immediately by one exact board.
|
|
2
|
+
import { jcsHash } from "@zanii/core";
|
|
3
|
+
import { boardFieldError, bodySignature, isBoard, isRelease, isWithdrawal, releaseFieldError, signersOf, withSignatures, withdrawalFieldError } from "./board.js";
|
|
4
|
+
import { isBodyRegister, registerFieldError } from "./register.js";
|
|
5
|
+
import { verifySchemeAppend } from "./scheme-log.js";
|
|
6
|
+
import { isDid, isInt, isIso, isObj, isSha, ms } from "./shared.js";
|
|
7
|
+
import {} from "./signer.js";
|
|
8
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
9
|
+
function fieldError(d) {
|
|
10
|
+
if (!isObj(d) || Object.keys(d).length !== 9 || d.v !== 1 || d.type !== "certify.board-transition")
|
|
11
|
+
return "not a closed board-transition (v1)";
|
|
12
|
+
if (!isDid(d.owner) || !isSha(d.from) || !isSha(d.to) || !isInt(d.epoch, 1, Number.MAX_SAFE_INTEGER) || !isIso(d.at))
|
|
13
|
+
return "invalid transition authority, board binding, epoch or time";
|
|
14
|
+
if (!Array.isArray(d.cancel) || d.cancel.length < 1 || d.cancel.length > 100 || !d.cancel.every((h, i, xs) => isSha(h) && (i === 0 || xs[i - 1] < h)))
|
|
15
|
+
return "cancel must contain 1 to 100 sorted distinct entry hashes";
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export async function buildSchemeTransition(fields, signers) {
|
|
19
|
+
if (!withinVerificationBudget(fields, { maxBytes: 32_768, maxNodes: 1_000 }) || !Array.isArray(signers) || signers.length < 1 || signers.length > 65)
|
|
20
|
+
throw new Error("transition exceeds its signing budget");
|
|
21
|
+
const body = { v: 1, type: "certify.board-transition", owner: fields.owner, from: fields.from, to: fields.to, epoch: fields.epoch, at: fields.at, cancel: Array.isArray(fields.cancel) ? [...fields.cancel] : fields.cancel };
|
|
22
|
+
const err = fieldError({ ...body, signatures: [] });
|
|
23
|
+
if (err)
|
|
24
|
+
throw new Error(err);
|
|
25
|
+
return withSignatures(body, await Promise.all(signers.map((s) => bodySignature(body, s))));
|
|
26
|
+
}
|
|
27
|
+
/** The caller must atomically accept this entry and its immediately following exact next board. */
|
|
28
|
+
function transitionCheck(doc, c, proposalNow) {
|
|
29
|
+
const reasons = [];
|
|
30
|
+
const out = () => ({ ok: reasons.length === 0, reasons, canceled: reasons.length || !isObj(doc) ? [] : [...doc.cancel] });
|
|
31
|
+
if (!withinVerificationBudget(doc, { maxBytes: 65_536, maxNodes: 3_000 }) || !withinVerificationBudget(c?.board, { maxBytes: 65_536, maxNodes: 3_000 }) || !withinVerificationBudget(c?.nextBoard, { maxBytes: 65_536, maxNodes: 3_000 }) || proposalNow === undefined && (!withinVerificationBudget(c?.anchor, { maxBytes: 655_360, maxNodes: 60_000 }) || !withinVerificationBudget(c?.nextAnchor, { maxBytes: 655_360, maxNodes: 60_000 })))
|
|
32
|
+
return { ok: false, reasons: ["board transition exceeds the verification budget"], canceled: [] };
|
|
33
|
+
const err = fieldError(doc);
|
|
34
|
+
if (err || !isObj(doc))
|
|
35
|
+
return { ok: false, reasons: [err], canceled: [] };
|
|
36
|
+
const b = c?.board, next = c?.nextBoard;
|
|
37
|
+
if (!isDid(c?.owner) || doc.owner !== c.owner || !isBoard(b) || boardFieldError(b) || b.owner !== c.owner || !isBoard(next) || boardFieldError(next) || next.owner !== c.owner)
|
|
38
|
+
return { ok: false, reasons: ["trusted outgoing and next boards are required"], canceled: [] };
|
|
39
|
+
if (doc.from !== jcsHash(b) || doc.to !== jcsHash(next) || doc.epoch !== next.seq || next.seq !== Number(b.seq) + 1 || next.prev !== doc.from)
|
|
40
|
+
reasons.push("transition is not bound to the exact outgoing and next board epoch");
|
|
41
|
+
if (!isDid(b.ledger) || next.ledger !== b.ledger)
|
|
42
|
+
reasons.push("transition requires the same explicit ledger identity on both boards");
|
|
43
|
+
if (!(ms(next.effective_from) > ms(b.effective_from)))
|
|
44
|
+
reasons.push("next board does not take effect after the outgoing board");
|
|
45
|
+
if (!isIso(c.previousAt))
|
|
46
|
+
reasons.push("the authenticated previous append time is required");
|
|
47
|
+
const allowed = [c.owner, ...b.members];
|
|
48
|
+
const signed = signersOf(doc, allowed), members = signed.signers.filter((s) => b.members.includes(s));
|
|
49
|
+
if (!signed.wellFormed || !signed.signers.includes(c.owner) || members.length < Number(b.threshold))
|
|
50
|
+
reasons.push("transition requires the owner and outgoing board threshold signatures");
|
|
51
|
+
const nextSigned = signersOf(next, b.members);
|
|
52
|
+
if (!nextSigned.wellFormed || nextSigned.signers.length < Number(b.threshold))
|
|
53
|
+
reasons.push("next board is not signed by the outgoing threshold");
|
|
54
|
+
let anchoredAt = proposalNow ?? "";
|
|
55
|
+
if (proposalNow !== undefined && (!isIso(proposalNow) || ms(proposalNow) < ms(doc.at) || ms(proposalNow) - ms(doc.at) > 60_000))
|
|
56
|
+
reasons.push("transition proposal must be within 60 seconds after its authorization time");
|
|
57
|
+
if (proposalNow === undefined) {
|
|
58
|
+
for (const [offset, target, a] of [[0, doc, c.anchor], [1, next, c.nextAnchor]]) {
|
|
59
|
+
if (!a || !isObj(a.statement) || Object.keys(a.statement).length !== 7 || a.statement.v !== 1 || a.statement.type !== "certify.scheme-append" || !isInt(c.index, 1, Number.MAX_SAFE_INTEGER - 2) || a.statement?.index !== c.index + offset || a.statement?.size !== c.index + offset + 1 || a.statement?.log !== c.owner || a.statement?.entry !== jcsHash(target) || !isSha(a.statement?.root))
|
|
60
|
+
reasons.push("authenticated transition and next-board appends at the exact prefix positions are required");
|
|
61
|
+
else
|
|
62
|
+
reasons.push(...verifySchemeAppend(a.statement, target, a.append, a.receipt, { ledger: b.ledger }).reasons);
|
|
63
|
+
if (!isObj(a?.receipt) || a.receipt.v !== 1 || a.receipt.action !== "certify.scheme-append" || a.receipt.target !== `certify.scheme/${c.index + offset}`)
|
|
64
|
+
reasons.push("transition anchor receipt has another purpose or target");
|
|
65
|
+
const time = isObj(a?.append) ? a.append.anchored_at : undefined;
|
|
66
|
+
if (!isIso(time) || ms(time) < ms(doc.at) || ms(time) - ms(doc.at) > 60_000 || ms(time) < ms(c.previousAt) || anchoredAt && ms(time) < ms(anchoredAt))
|
|
67
|
+
reasons.push("transition anchors must be ordered within 60 seconds after their authorization time");
|
|
68
|
+
if (isIso(time))
|
|
69
|
+
anchoredAt = time;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!isIso(anchoredAt) || ms(next.effective_from) < ms(anchoredAt))
|
|
73
|
+
reasons.push("next board takes effect before the authenticated transition anchors");
|
|
74
|
+
for (const hash of doc.cancel) {
|
|
75
|
+
let entry;
|
|
76
|
+
try {
|
|
77
|
+
entry = c.entry(hash);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
reasons.push("transition prefix lookup failed");
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (!withinVerificationBudget(entry, { maxBytes: 1_048_576, maxNodes: 100_000 }) || !isObj(entry)) {
|
|
84
|
+
reasons.push("cancellation target is absent from the authenticated prefix");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const invalid = isRelease(entry) ? releaseFieldError(entry) : isWithdrawal(entry) ? withdrawalFieldError(entry) : isBodyRegister(entry) ? registerFieldError(entry) : "not cancellable";
|
|
88
|
+
if (invalid || jcsHash(entry) !== hash || entry.board !== doc.from)
|
|
89
|
+
reasons.push("cancellation target is not an outgoing-board release, withdrawal or register");
|
|
90
|
+
if (!isIso(anchoredAt) || !(ms(entry.effective_from) > ms(anchoredAt)))
|
|
91
|
+
reasons.push("cancellation target is already effective at the authenticated transition anchor");
|
|
92
|
+
}
|
|
93
|
+
return out();
|
|
94
|
+
}
|
|
95
|
+
/** Strict effect verification: both authenticated append receipts are mandatory. */
|
|
96
|
+
export function verifySchemeTransition(doc, c) { return transitionCheck(doc, c); }
|
|
97
|
+
/** Admission only. This does NOT authorize cancellation or a new board before anchoring. */
|
|
98
|
+
export function verifySchemeTransitionProposal(doc, c) {
|
|
99
|
+
const { ok, reasons } = transitionCheck(doc, c, c?.now ?? "");
|
|
100
|
+
return { ok, reasons };
|
|
101
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type Verdict = "pass" | "fail" | "inconclusive";
|
|
2
|
+
export interface CategoryScore {
|
|
3
|
+
rate_ppm: number;
|
|
4
|
+
low_ppm: number;
|
|
5
|
+
high_ppm: number;
|
|
6
|
+
verdict: Verdict;
|
|
7
|
+
}
|
|
8
|
+
/** Section 1: the 95% Wilson range of `passes` in `trials`, judged against `thresholdPpm`. */
|
|
9
|
+
export declare function scoreCategory(passes: number, trials: number, thresholdPpm: number): CategoryScore;
|
|
10
|
+
export interface ScoredResult {
|
|
11
|
+
category: string;
|
|
12
|
+
trials: number;
|
|
13
|
+
threshold_ppm: number;
|
|
14
|
+
verdict: Verdict;
|
|
15
|
+
}
|
|
16
|
+
export interface ProfileCategoryRule {
|
|
17
|
+
id: string;
|
|
18
|
+
required_levels: number[];
|
|
19
|
+
min_trials: number;
|
|
20
|
+
threshold_ppm: number;
|
|
21
|
+
}
|
|
22
|
+
export interface Overall {
|
|
23
|
+
pass: boolean;
|
|
24
|
+
/** Codes `<RULE>:<category>` (or `LEVEL_NOT_IN_PROFILE`), sorted. Empty on a pass. */
|
|
25
|
+
reasons: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Section 2: pass only when the level is in the profile and every category the profile requires
|
|
29
|
+
* at that level is present, passes, ran at least min_trials and was judged at a threshold no
|
|
30
|
+
* lower than the profile's.
|
|
31
|
+
*/
|
|
32
|
+
export declare function overallVerdict(results: readonly ScoredResult[], profile: {
|
|
33
|
+
levels: number[];
|
|
34
|
+
categories: readonly ProfileCategoryRule[];
|
|
35
|
+
}, level: number): Overall;
|
package/dist/scoring.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// spec/scoring.md: per-category verdicts from repeated trials, and the overall pass rule.
|
|
2
|
+
import { successRange } from "@zanii/blackbox";
|
|
3
|
+
import { isInt } from "./shared.js";
|
|
4
|
+
/** Section 1: the 95% Wilson range of `passes` in `trials`, judged against `thresholdPpm`. */
|
|
5
|
+
export function scoreCategory(passes, trials, thresholdPpm) {
|
|
6
|
+
if (!isInt(trials, 1, 1_000_000))
|
|
7
|
+
throw new Error("trials must be an integer 1-1000000");
|
|
8
|
+
if (!isInt(passes, 0, trials))
|
|
9
|
+
throw new Error("passes must be an integer 0-trials");
|
|
10
|
+
if (!isInt(thresholdPpm, 0, 1_000_000))
|
|
11
|
+
throw new Error("threshold_ppm must be 0-1000000");
|
|
12
|
+
const [low, high] = successRange(passes, trials);
|
|
13
|
+
const verdict = low >= thresholdPpm ? "pass" : high < thresholdPpm ? "fail" : "inconclusive";
|
|
14
|
+
return {
|
|
15
|
+
rate_ppm: Math.floor((passes * 1_000_000) / trials),
|
|
16
|
+
low_ppm: low,
|
|
17
|
+
high_ppm: high,
|
|
18
|
+
verdict,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Section 2: pass only when the level is in the profile and every category the profile requires
|
|
23
|
+
* at that level is present, passes, ran at least min_trials and was judged at a threshold no
|
|
24
|
+
* lower than the profile's.
|
|
25
|
+
*/
|
|
26
|
+
export function overallVerdict(results, profile, level) {
|
|
27
|
+
const reasons = [];
|
|
28
|
+
if (!profile.levels.includes(level))
|
|
29
|
+
reasons.push("LEVEL_NOT_IN_PROFILE");
|
|
30
|
+
const by = new Map(results.map((r) => [r.category, r]));
|
|
31
|
+
for (const c of profile.categories) {
|
|
32
|
+
if (!c.required_levels.includes(level))
|
|
33
|
+
continue;
|
|
34
|
+
const r = by.get(c.id);
|
|
35
|
+
if (!r) {
|
|
36
|
+
reasons.push(`CATEGORY_MISSING:${c.id}`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (r.trials < c.min_trials)
|
|
40
|
+
reasons.push(`TOO_FEW_TRIALS:${c.id}`);
|
|
41
|
+
if (r.threshold_ppm < c.threshold_ppm)
|
|
42
|
+
reasons.push(`THRESHOLD_BELOW_PROFILE:${c.id}`);
|
|
43
|
+
if (r.verdict === "fail")
|
|
44
|
+
reasons.push(`CATEGORY_FAILED:${c.id}`);
|
|
45
|
+
if (r.verdict === "inconclusive")
|
|
46
|
+
reasons.push(`CATEGORY_INCONCLUSIVE:${c.id}`);
|
|
47
|
+
}
|
|
48
|
+
reasons.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
49
|
+
return { pass: reasons.length === 0, reasons };
|
|
50
|
+
}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const SHA256: RegExp;
|
|
2
|
+
export declare const ISO_UTC: RegExp;
|
|
3
|
+
export declare const DID: RegExp;
|
|
4
|
+
export declare const SLUG: RegExp;
|
|
5
|
+
/** A real UTC instant in the section 2 form (Date.parse alone accepts 2026-02-30). */
|
|
6
|
+
export declare const isIso: (v: unknown) => v is string;
|
|
7
|
+
export declare const isSha: (v: unknown) => v is string;
|
|
8
|
+
export declare const isDid: (v: unknown) => v is string;
|
|
9
|
+
export declare const isObj: (v: unknown) => v is Record<string, unknown>;
|
|
10
|
+
export declare const isInt: (v: unknown, min: number, max: number) => v is number;
|
|
11
|
+
/** Milliseconds since the epoch; NaN for anything that is not an ISO timestamp. */
|
|
12
|
+
export declare const ms: (v: unknown) => number;
|
|
13
|
+
/** `t` plus `days` whole days, as `toISOString()` writes it. */
|
|
14
|
+
export declare const addDays: (t: string, days: number) => string;
|
|
15
|
+
/** Sorted (UTF-16 code unit order, as JCS sorts keys), duplicates removed. */
|
|
16
|
+
export declare const sortedUnique: (xs: readonly string[]) => string[];
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Field rules shared by every Certify format (spec/README.md §2).
|
|
2
|
+
export const SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
3
|
+
export const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
4
|
+
export const DID = /^did:key:z[1-9A-HJ-NP-Za-km-z]{40,60}$/;
|
|
5
|
+
export const SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6
|
+
/** A real UTC instant in the section 2 form (Date.parse alone accepts 2026-02-30). */
|
|
7
|
+
export const isIso = (v) => typeof v === "string" &&
|
|
8
|
+
ISO_UTC.test(v) &&
|
|
9
|
+
!Number.isNaN(Date.parse(v)) &&
|
|
10
|
+
new Date(v).toISOString().slice(0, 19) === v.slice(0, 19);
|
|
11
|
+
export const isSha = (v) => typeof v === "string" && SHA256.test(v);
|
|
12
|
+
export const isDid = (v) => typeof v === "string" && DID.test(v);
|
|
13
|
+
export const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
14
|
+
export const isInt = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
|
|
15
|
+
/** Milliseconds since the epoch; NaN for anything that is not an ISO timestamp. */
|
|
16
|
+
export const ms = (v) => (isIso(v) ? Date.parse(v) : Number.NaN);
|
|
17
|
+
/** `t` plus `days` whole days, as `toISOString()` writes it. */
|
|
18
|
+
export const addDays = (t, days) => new Date(Date.parse(t) + days * 86_400_000).toISOString();
|
|
19
|
+
/** Sorted (UTF-16 code unit order, as JCS sorts keys), duplicates removed. */
|
|
20
|
+
export const sortedUnique = (xs) => [...new Set(xs)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
package/dist/signer.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Signer {
|
|
2
|
+
/** The raw 32-byte Ed25519 public key. */
|
|
3
|
+
publicKey: Uint8Array;
|
|
4
|
+
/** Returns the raw 64-byte Ed25519 signature over `message`. */
|
|
5
|
+
sign: (message: Uint8Array) => Promise<Uint8Array>;
|
|
6
|
+
}
|
|
7
|
+
/** A Signer over a raw 32-byte Ed25519 seed held in this process (tests, development). */
|
|
8
|
+
export declare function keySigner(privateKey: Uint8Array): Signer;
|
|
9
|
+
export declare const signerDid: (signer: Signer) => string;
|
|
10
|
+
/** `ed25519:<hex>` over the RFC 8785 bytes of `unsigned`. */
|
|
11
|
+
export declare function signatureOver(unsigned: object, signer: Signer): Promise<string>;
|
|
12
|
+
/** Does `sig` (`ed25519:<hex>`) by `did` verify over the RFC 8785 bytes of `unsigned`? Never throws. */
|
|
13
|
+
export declare function signatureOk(unsigned: object, sig: unknown, did: string): boolean;
|
|
14
|
+
/** @zanii/core signObject with a Signer: the same bytes, verified by verifySignedObject. */
|
|
15
|
+
export declare function signWith<T extends object>(obj: T, signer: Signer): Promise<T & {
|
|
16
|
+
signer: string;
|
|
17
|
+
sig: string;
|
|
18
|
+
}>;
|
package/dist/signer.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// spec/certificate.md §4: everything Certify signs goes through a Signer, the same shape as
|
|
2
|
+
// @zanii/sdk AgentSigner and @zanii/kms RemoteSigner, so the issuer key can live in a KMS or HSM.
|
|
3
|
+
import { createPrivateKey, createPublicKey, sign as nodeSign, verify as nodeVerify } from "node:crypto";
|
|
4
|
+
import { canonicalBytes, didFromPublicKey, publicKeyFromDid } from "@zanii/core";
|
|
5
|
+
const PKCS8_ED25519 = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
6
|
+
const SPKI_ED25519 = Buffer.from("302a300506032b6570032100", "hex");
|
|
7
|
+
/** A Signer over a raw 32-byte Ed25519 seed held in this process (tests, development). */
|
|
8
|
+
export function keySigner(privateKey) {
|
|
9
|
+
if (privateKey.length !== 32)
|
|
10
|
+
throw new Error("ed25519 private key must be 32 bytes");
|
|
11
|
+
const key = createPrivateKey({
|
|
12
|
+
key: Buffer.concat([PKCS8_ED25519, privateKey]),
|
|
13
|
+
format: "der",
|
|
14
|
+
type: "pkcs8",
|
|
15
|
+
});
|
|
16
|
+
const spki = createPublicKey(key).export({ format: "der", type: "spki" });
|
|
17
|
+
return {
|
|
18
|
+
publicKey: new Uint8Array(spki.subarray(spki.length - 32)),
|
|
19
|
+
sign: async (message) => new Uint8Array(nodeSign(null, message, key)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export const signerDid = (signer) => didFromPublicKey(signer.publicKey);
|
|
23
|
+
/** `ed25519:<hex>` over the RFC 8785 bytes of `unsigned`. */
|
|
24
|
+
export async function signatureOver(unsigned, signer) {
|
|
25
|
+
const sig = await signer.sign(canonicalBytes(unsigned));
|
|
26
|
+
if (sig.length !== 64)
|
|
27
|
+
throw new Error("signer returned a signature that is not 64 bytes");
|
|
28
|
+
return `ed25519:${Buffer.from(sig).toString("hex")}`;
|
|
29
|
+
}
|
|
30
|
+
/** Does `sig` (`ed25519:<hex>`) by `did` verify over the RFC 8785 bytes of `unsigned`? Never throws. */
|
|
31
|
+
export function signatureOk(unsigned, sig, did) {
|
|
32
|
+
const pub = publicKeyFromDid(did);
|
|
33
|
+
if (!pub || typeof sig !== "string" || !/^ed25519:[0-9a-f]{128}$/.test(sig))
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
const key = createPublicKey({ key: Buffer.concat([SPKI_ED25519, pub]), format: "der", type: "spki" });
|
|
37
|
+
return nodeVerify(null, canonicalBytes(unsigned), key, Buffer.from(sig.slice(8), "hex"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** @zanii/core signObject with a Signer: the same bytes, verified by verifySignedObject. */
|
|
44
|
+
export async function signWith(obj, signer) {
|
|
45
|
+
if ("sig" in obj)
|
|
46
|
+
throw new Error("signWith: the object already carries a sig");
|
|
47
|
+
const unsigned = { ...obj, signer: signerDid(signer) };
|
|
48
|
+
return { ...unsigned, sig: await signatureOver(unsigned, signer) };
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function signingIdentityReasons(owner: unknown, issuers: unknown, statusIssuers?: unknown): string[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// A witness log id is its signing DID: an owner and a status issuer must never share that key.
|
|
2
|
+
import { publicKeyFromDid } from "@zanii/core";
|
|
3
|
+
const did = (v) => typeof v === "string" && v.length <= 69 && publicKeyFromDid(v) !== null;
|
|
4
|
+
export function signingIdentityReasons(owner, issuers, statusIssuers = []) {
|
|
5
|
+
if (!did(owner))
|
|
6
|
+
return ["signing identity: an actual Ed25519 owner DID is required"];
|
|
7
|
+
const valid = (list) => Array.isArray(list) && list.length <= 256 && Array.from(list).every(did) && new Set(list).size === list.length;
|
|
8
|
+
if (!valid(issuers) || !valid(statusIssuers))
|
|
9
|
+
return ["signing identity: issuer and status identities must be distinct bounded Ed25519 DID lists"];
|
|
10
|
+
return issuers.includes(owner) || statusIssuers.includes(owner) ? ["signing identity: the owner key must not also be an issuer or status signing key"] : [];
|
|
11
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type SignedTreeHead } from "@zanii/core";
|
|
2
|
+
import type { RevocationList } from "@zanii/credentials";
|
|
3
|
+
import { type PqBinding } from "@zanii/pq";
|
|
4
|
+
import type { WitnessCosignature } from "@zanii/witness";
|
|
5
|
+
import { type PqCosignature } from "./pq.ts";
|
|
6
|
+
import { type Signer } from "./signer.ts";
|
|
7
|
+
import STATUS_V2_SCHEMA from "./formats/status-v2.schema.json";
|
|
8
|
+
export { STATUS_V2_SCHEMA };
|
|
9
|
+
export declare const REASONS: readonly ["FINGERPRINT_MISMATCH", "RETEST_OVERDUE", "RETEST_FAILED", "INCIDENT", "APPLICANT_REQUEST", "ISSUER_ERROR",
|
|
10
|
+
/** licensing.md section 2a: the issuing licensee's licence was withdrawn or suspended for cause. */
|
|
11
|
+
"LICENCE_WITHDRAWN",
|
|
12
|
+
/** audit.md section 3: a Level 3 certificate's audit attestation lapsed. */
|
|
13
|
+
"AUDIT_EXPIRED"];
|
|
14
|
+
export declare const REASONS_V2: readonly ["FINGERPRINT_MISMATCH", "RETEST_OVERDUE", "RETEST_FAILED", "INCIDENT", "APPLICANT_REQUEST", "ISSUER_ERROR", "LICENCE_WITHDRAWN", "AUDIT_EXPIRED", "MARK_MISUSE"];
|
|
15
|
+
export declare const STATUS_V2_MAX_ENTRIES = 100000;
|
|
16
|
+
export type Reason = (typeof REASONS_V2)[number];
|
|
17
|
+
export type StatusVersion = 1 | 2;
|
|
18
|
+
export type CertificateStatus = "valid" | "suspended" | "revoked";
|
|
19
|
+
export type StatusAction = "grant" | "suspend" | "revoke" | "reinstate";
|
|
20
|
+
export interface StatusEntry {
|
|
21
|
+
status: CertificateStatus;
|
|
22
|
+
/** Null when valid; the reason code when suspended or revoked. */
|
|
23
|
+
reason: Reason | null;
|
|
24
|
+
since: string;
|
|
25
|
+
}
|
|
26
|
+
/** Keyed by the certificate's credential hash. */
|
|
27
|
+
export type StatusEntries = Record<string, StatusEntry>;
|
|
28
|
+
export interface StatusDocument {
|
|
29
|
+
v: StatusVersion;
|
|
30
|
+
type: "certify.status";
|
|
31
|
+
issuer: string;
|
|
32
|
+
entries: StatusEntries;
|
|
33
|
+
issued_at: string;
|
|
34
|
+
valid_until: string;
|
|
35
|
+
/** spec/witness.md: the document's position in the status log, from 0. */
|
|
36
|
+
seq?: number;
|
|
37
|
+
/** spec/witness.md: jcsHash of the previous document in the log; null at seq 0. */
|
|
38
|
+
prev?: string | null;
|
|
39
|
+
/** Section 5: bindingHash of the issuer's @zanii/pq binding, when co-signed. */
|
|
40
|
+
pq_binding?: string;
|
|
41
|
+
signer: string;
|
|
42
|
+
sig: string;
|
|
43
|
+
}
|
|
44
|
+
/** spec/witness.md section 3: what a verifier needs to require a witnessed checkpoint. */
|
|
45
|
+
export interface StatusWitness {
|
|
46
|
+
/** The issuer's signed tree head over the status log (`statusCheckpoint`). */
|
|
47
|
+
checkpoint: SignedTreeHead;
|
|
48
|
+
/** The inclusion proof of the document at index `seq` (`statusInclusionProof`), hex. */
|
|
49
|
+
proof: string[];
|
|
50
|
+
cosignatures: WitnessCosignature[];
|
|
51
|
+
/** The witness DIDs this verifier trusts. */
|
|
52
|
+
trusted: string[];
|
|
53
|
+
/** How many of them must have co-signed the checkpoint. */
|
|
54
|
+
k: number;
|
|
55
|
+
}
|
|
56
|
+
/** A log position is `seq` and `prev` together: seq 0 has prev null, later ones a hash. */
|
|
57
|
+
export declare const logPositionOk: (seq: unknown, prev: unknown) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* §2: apply one status change and return the new entries (the input is not changed). Throws on a
|
|
60
|
+
* change the rules forbid: revoked is final, only a valid certificate is suspended, only a
|
|
61
|
+
* suspended one is reinstated, and time never goes backwards for one certificate.
|
|
62
|
+
*/
|
|
63
|
+
export declare function applyStatus(entries: StatusEntries, certificate: string, action: StatusAction, reason: Reason | null, at: string, opts?: {
|
|
64
|
+
version?: StatusVersion;
|
|
65
|
+
}): StatusEntries;
|
|
66
|
+
/** §1: sign a status document. `valid_until` is the freshness bound verifiers enforce. */
|
|
67
|
+
export declare function buildStatus(fields: {
|
|
68
|
+
version?: StatusVersion;
|
|
69
|
+
entries: StatusEntries;
|
|
70
|
+
issuedAt: string;
|
|
71
|
+
validUntil: string;
|
|
72
|
+
/** spec/witness.md: place the document in the status log (both or neither). */
|
|
73
|
+
seq?: number;
|
|
74
|
+
prev?: string | null;
|
|
75
|
+
/** Section 5: commit to the issuer's @zanii/pq binding so the document can be co-signed. */
|
|
76
|
+
pqBinding?: PqBinding;
|
|
77
|
+
}, signer: Signer): Promise<StatusDocument>;
|
|
78
|
+
export interface StatusCheck {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
reasons: string[];
|
|
81
|
+
freshness: "fresh" | "stale" | null;
|
|
82
|
+
/** Only with a `witness` policy: the trusted witnesses that co-signed the checkpoint. */
|
|
83
|
+
witnesses?: string[];
|
|
84
|
+
/** Only with `issuerDoc` (section 5a): `required`, `opted_out` or `not_required`. */
|
|
85
|
+
pqPolicy?: "required" | "opted_out" | "not_required";
|
|
86
|
+
}
|
|
87
|
+
export interface StatusVerifyOptions {
|
|
88
|
+
/** Independently known scheme log owner, which cannot share this status signing identity. */
|
|
89
|
+
schemeOwner?: string;
|
|
90
|
+
issuer?: string;
|
|
91
|
+
now?: string;
|
|
92
|
+
/** Section 5: the document's post-quantum co-signature. */
|
|
93
|
+
pq?: PqCosignature;
|
|
94
|
+
/** Section 5: refuse a document without a valid post-quantum co-signature. */
|
|
95
|
+
requirePq?: boolean;
|
|
96
|
+
/** spec/witness.md: require the document in a checkpoint co-signed by k trusted witnesses. */
|
|
97
|
+
witness?: StatusWitness;
|
|
98
|
+
/** Section 5a: the issuer document; `require_pq: true` in it makes `requirePq` the default. */
|
|
99
|
+
issuerDoc?: unknown;
|
|
100
|
+
/** Section 5a: explicitly verify without the co-signature the issuer document requires. */
|
|
101
|
+
allowNoPq?: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Is this an intact status document from `issuer` (when given), still fresh at `now`? With `pq`
|
|
105
|
+
* or `requirePq` also its post-quantum co-signature, with `witness` its witnessed checkpoint.
|
|
106
|
+
*/
|
|
107
|
+
export declare function verifyStatus(doc: unknown, opts?: StatusVerifyOptions): StatusCheck;
|
|
108
|
+
/**
|
|
109
|
+
* §3: the @zanii/credentials revocation list for a status document: every certificate whose status
|
|
110
|
+
* is not valid, with the same issued_at and valid_until. Byte-identical to `buildRevocationList`
|
|
111
|
+
* with the issuer's key, so plain credential verifiers refuse suspended and revoked certificates.
|
|
112
|
+
*/
|
|
113
|
+
export declare function revocationListFromStatus(doc: StatusDocument, signer: Signer): Promise<RevocationList>;
|