@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.
- package/LICENSE +159 -0
- package/README.md +112 -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 +38 -0
- package/dist/beacon-client.js +52 -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 +92 -0
- package/dist/bundle.js +109 -0
- package/dist/certificate.d.ts +161 -0
- package/dist/certificate.js +345 -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 +242 -0
- package/dist/governance.js +411 -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 +58 -0
- package/dist/index.js +59 -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 +20 -0
- package/dist/profile.js +161 -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 +38 -0
- package/dist/runtime-client.js +185 -0
- package/dist/runtime-gateway.d.ts +24 -0
- package/dist/runtime-gateway.js +167 -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 +80 -0
- package/dist/schemas/scenarios.schema.json +573 -0
- package/dist/schemas/suite.schema.json +138 -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 +257 -0
- package/dist/suite.js +441 -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 +76 -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 +57 -3
package/dist/board.d.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { type Signer } from "./signer.ts";
|
|
2
|
+
import type { SchemeLogsInput } from "./scheme-log.ts";
|
|
3
|
+
/** Section 1: one signature over the canonical bytes of a document without `signatures`. */
|
|
4
|
+
export interface BodySignature {
|
|
5
|
+
signer: string;
|
|
6
|
+
sig: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function bodySignature(body: object, signer: Signer): Promise<BodySignature>;
|
|
9
|
+
/** The document with its signatures sorted by signer; one signer signs once. */
|
|
10
|
+
export declare function withSignatures<T extends object>(body: T, sigs: readonly BodySignature[]): T & {
|
|
11
|
+
signatures: BodySignature[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Section 1: the signers of `doc` whose signature verifies, and whether `signatures` is well formed
|
|
15
|
+
* (1 to 64 `{signer, sig}`, strictly sorted by signer, each verifying, each in `allowed` when given).
|
|
16
|
+
*/
|
|
17
|
+
export declare function signersOf(doc: Record<string, unknown>, allowed?: readonly string[]): {
|
|
18
|
+
signers: string[];
|
|
19
|
+
wellFormed: boolean;
|
|
20
|
+
};
|
|
21
|
+
export interface BoardInput {
|
|
22
|
+
owner: string;
|
|
23
|
+
seq: number;
|
|
24
|
+
prev: string | null;
|
|
25
|
+
members: readonly string[];
|
|
26
|
+
threshold: number;
|
|
27
|
+
effectiveFrom: string;
|
|
28
|
+
/** scheme-log.md section 3: the scheme owner's ledger identity, the root of the delegation of its append receipts. */
|
|
29
|
+
ledger?: string;
|
|
30
|
+
}
|
|
31
|
+
export type BoardBody = {
|
|
32
|
+
v: 1;
|
|
33
|
+
type: "certify.board";
|
|
34
|
+
owner: string;
|
|
35
|
+
seq: number;
|
|
36
|
+
prev: string | null;
|
|
37
|
+
members: string[];
|
|
38
|
+
threshold: number;
|
|
39
|
+
effective_from: string;
|
|
40
|
+
ledger?: string;
|
|
41
|
+
};
|
|
42
|
+
export type Board = BoardBody & {
|
|
43
|
+
signatures: BodySignature[];
|
|
44
|
+
};
|
|
45
|
+
export interface BoardCheck {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
reasons: string[];
|
|
48
|
+
}
|
|
49
|
+
declare function boardError(b: Record<string, unknown>): string | null;
|
|
50
|
+
/** The unsigned board (each signer signs it with `bodySignature`). */
|
|
51
|
+
export declare function boardBody(f: BoardInput): BoardBody;
|
|
52
|
+
/** A board signed by `signers`: the owner for the first board, k of the previous board after. */
|
|
53
|
+
export declare function buildBoard(f: BoardInput, signers: readonly Signer[]): Promise<Board>;
|
|
54
|
+
export declare const isBoard: (b: unknown) => b is Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* Section 2: the boards form one log: board i at seq i naming board i - 1, one owner, each taking
|
|
57
|
+
* effect after the previous one; board 0 signed by the owner, every later board by at least the
|
|
58
|
+
* previous board's threshold of its members, and no other signature.
|
|
59
|
+
*/
|
|
60
|
+
export declare function verifyBoardLog(boards: readonly unknown[], opts?: {
|
|
61
|
+
owner?: string;
|
|
62
|
+
}): BoardCheck;
|
|
63
|
+
export declare const RELEASE_KINDS: readonly ["scheme", "profile"];
|
|
64
|
+
export type ReleaseKind = (typeof RELEASE_KINDS)[number];
|
|
65
|
+
export interface ReleaseSubject {
|
|
66
|
+
kind: ReleaseKind;
|
|
67
|
+
/** `zanii-certify/<major>.<minor>` for a scheme, `<slug>/<version>` for a profile. */
|
|
68
|
+
ref: string;
|
|
69
|
+
}
|
|
70
|
+
export interface ReleaseInput {
|
|
71
|
+
seq: number;
|
|
72
|
+
prev: string | null;
|
|
73
|
+
subject: ReleaseSubject;
|
|
74
|
+
/** The released content: a scheme's published text, file hash; a profile, jcsHash of the profile. */
|
|
75
|
+
sha256: string;
|
|
76
|
+
effectiveFrom: string;
|
|
77
|
+
notes: {
|
|
78
|
+
en: string;
|
|
79
|
+
ar: string;
|
|
80
|
+
};
|
|
81
|
+
/** jcsHash of the board that signs the release (the board in force at effectiveFrom). */
|
|
82
|
+
board: string;
|
|
83
|
+
}
|
|
84
|
+
export type ReleaseBody = {
|
|
85
|
+
v: 1;
|
|
86
|
+
type: "certify.release";
|
|
87
|
+
seq: number;
|
|
88
|
+
prev: string | null;
|
|
89
|
+
subject: ReleaseSubject;
|
|
90
|
+
sha256: string;
|
|
91
|
+
effective_from: string;
|
|
92
|
+
notes: {
|
|
93
|
+
en: string;
|
|
94
|
+
ar: string;
|
|
95
|
+
};
|
|
96
|
+
board: string;
|
|
97
|
+
};
|
|
98
|
+
export type Release = ReleaseBody & {
|
|
99
|
+
signatures: BodySignature[];
|
|
100
|
+
};
|
|
101
|
+
export declare const isText: (t: unknown) => boolean;
|
|
102
|
+
export declare function subjectOk(s: unknown): s is ReleaseSubject;
|
|
103
|
+
declare function releaseError(r: Record<string, unknown>): string | null;
|
|
104
|
+
export declare function releaseBody(f: ReleaseInput): ReleaseBody;
|
|
105
|
+
/** A release signed by `signers` (members of the board it names). */
|
|
106
|
+
export declare function buildRelease(f: ReleaseInput, signers: readonly Signer[]): Promise<Release>;
|
|
107
|
+
export declare const isRelease: (r: unknown) => r is Record<string, unknown>;
|
|
108
|
+
/**
|
|
109
|
+
* A board-signed document (release, withdrawal, body register) against a board log already checked:
|
|
110
|
+
* the board it names is in the log and in force at its effective_from, and k of its members signed it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function boardSignedReasons(r: Record<string, unknown>, boards: readonly unknown[], noun: string): string[];
|
|
113
|
+
/** Section 3: one release, with the board log it names (checked with `verifyBoardLog`). */
|
|
114
|
+
export declare function verifyRelease(release: unknown, boards: readonly unknown[], opts?: {
|
|
115
|
+
owner?: string;
|
|
116
|
+
}): BoardCheck;
|
|
117
|
+
export declare const subjectKey: (s: unknown) => string;
|
|
118
|
+
/** Section 4: the release log: every release valid, at seq i naming release i - 1, effective_from never going back, each subject released once. */
|
|
119
|
+
export declare function verifyReleaseLog(releases: readonly unknown[], boards: readonly unknown[], opts?: {
|
|
120
|
+
owner?: string;
|
|
121
|
+
schemeLog?: SchemeLogsInput;
|
|
122
|
+
}): BoardCheck;
|
|
123
|
+
/**
|
|
124
|
+
* Section 5: may a lab use `subject` at `at`? The release log must verify, release the subject, take
|
|
125
|
+
* effect no later than `at` and (with `sha256`) for exactly that content. With `withdrawals`
|
|
126
|
+
* (section 6), a release withdrawn at or before `at` may not be used.
|
|
127
|
+
*/
|
|
128
|
+
export declare function requireRelease(releases: readonly unknown[], boards: readonly unknown[], subject: ReleaseSubject, at: string, opts?: {
|
|
129
|
+
owner?: string;
|
|
130
|
+
sha256?: string;
|
|
131
|
+
withdrawals?: readonly unknown[];
|
|
132
|
+
schemeLog?: SchemeLogsInput;
|
|
133
|
+
}): BoardCheck & {
|
|
134
|
+
release: Release | null;
|
|
135
|
+
withdrawal?: ReleaseWithdrawal | null;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Section 4a: may `board` be appended to the board log now? The board log with it verifies, it takes
|
|
139
|
+
* effect after the effective_from of every release already in the release log, and the release log
|
|
140
|
+
* still verifies against the longer board log (a board change never breaks a release).
|
|
141
|
+
*/
|
|
142
|
+
export declare function verifyBoardAppend(boards: readonly unknown[], releases: readonly unknown[], board: unknown, opts?: {
|
|
143
|
+
owner?: string;
|
|
144
|
+
}): BoardCheck;
|
|
145
|
+
/**
|
|
146
|
+
* Section 4a: may `release` be appended to the release log now? The release log with it verifies,
|
|
147
|
+
* and it is signed by the newest board of the board log (a board that was replaced signs nothing more).
|
|
148
|
+
*/
|
|
149
|
+
export declare function verifyReleaseAppend(releases: readonly unknown[], boards: readonly unknown[], release: unknown, opts?: {
|
|
150
|
+
owner?: string;
|
|
151
|
+
}): BoardCheck;
|
|
152
|
+
export declare const WITHDRAWAL_REASONS: readonly ["DEFECT", "SECURITY", "LEGAL", "OTHER"];
|
|
153
|
+
export type WithdrawalReason = (typeof WITHDRAWAL_REASONS)[number];
|
|
154
|
+
export interface ReleaseWithdrawalInput {
|
|
155
|
+
seq: number;
|
|
156
|
+
prev: string | null;
|
|
157
|
+
subject: ReleaseSubject;
|
|
158
|
+
/** jcsHash of the release withdrawn. */
|
|
159
|
+
release: string;
|
|
160
|
+
reason: WithdrawalReason;
|
|
161
|
+
notes: {
|
|
162
|
+
en: string;
|
|
163
|
+
ar: string;
|
|
164
|
+
};
|
|
165
|
+
effectiveFrom: string;
|
|
166
|
+
/** jcsHash of the board that signs (the newest board when it is appended). */
|
|
167
|
+
board: string;
|
|
168
|
+
}
|
|
169
|
+
export type ReleaseWithdrawalBody = {
|
|
170
|
+
v: 1;
|
|
171
|
+
type: "certify.release-withdrawal";
|
|
172
|
+
seq: number;
|
|
173
|
+
prev: string | null;
|
|
174
|
+
subject: ReleaseSubject;
|
|
175
|
+
release: string;
|
|
176
|
+
reason: WithdrawalReason;
|
|
177
|
+
notes: {
|
|
178
|
+
en: string;
|
|
179
|
+
ar: string;
|
|
180
|
+
};
|
|
181
|
+
effective_from: string;
|
|
182
|
+
board: string;
|
|
183
|
+
};
|
|
184
|
+
export type ReleaseWithdrawal = ReleaseWithdrawalBody & {
|
|
185
|
+
signatures: BodySignature[];
|
|
186
|
+
};
|
|
187
|
+
declare function withdrawalError(w: Record<string, unknown>): string | null;
|
|
188
|
+
export { boardError as boardFieldError, releaseError as releaseFieldError, withdrawalError as withdrawalFieldError };
|
|
189
|
+
export declare function releaseWithdrawalBody(f: ReleaseWithdrawalInput): ReleaseWithdrawalBody;
|
|
190
|
+
/** A release withdrawal signed by `signers` (k members of the board it names). */
|
|
191
|
+
export declare function buildReleaseWithdrawal(f: ReleaseWithdrawalInput, signers: readonly Signer[]): Promise<ReleaseWithdrawal>;
|
|
192
|
+
export declare const isWithdrawal: (w: unknown) => w is Record<string, unknown>;
|
|
193
|
+
/**
|
|
194
|
+
* Section 6: the withdrawal log: the release log verifies, every withdrawal at seq i naming
|
|
195
|
+
* withdrawal i - 1, effective_from never going back, naming a release of the log with its subject,
|
|
196
|
+
* taking effect no earlier than it, each release withdrawn once, signed by k of the board in force.
|
|
197
|
+
*/
|
|
198
|
+
export declare function verifyWithdrawalLog(withdrawals: readonly unknown[], releases: readonly unknown[], boards: readonly unknown[], opts?: {
|
|
199
|
+
owner?: string;
|
|
200
|
+
schemeLog?: SchemeLogsInput;
|
|
201
|
+
}): BoardCheck;
|
|
202
|
+
/**
|
|
203
|
+
* Section 6: a certificate issued under a withdrawn scheme or profile version is flagged, never
|
|
204
|
+
* failed. `ok` is whether the withdrawal log verified; `flags` names each withdrawal that touches
|
|
205
|
+
* the certificate's scheme or profile.
|
|
206
|
+
*/
|
|
207
|
+
export declare function withdrawalFlags(cert: {
|
|
208
|
+
issued_at?: unknown;
|
|
209
|
+
claim?: unknown;
|
|
210
|
+
}, logs: {
|
|
211
|
+
withdrawals: readonly unknown[];
|
|
212
|
+
releases: readonly unknown[];
|
|
213
|
+
boards: readonly unknown[];
|
|
214
|
+
owner?: string;
|
|
215
|
+
schemeLog?: SchemeLogsInput;
|
|
216
|
+
}): BoardCheck & {
|
|
217
|
+
flags: string[];
|
|
218
|
+
};
|
package/dist/board.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// spec/governance-board.md: the scheme governance board (k of n; each membership change signed by
|
|
2
|
+
// k of the outgoing board, the first by the scheme owner) and the append-only log of scheme and
|
|
3
|
+
// profile releases the board signs. A lab may only use a scheme or profile version with a release.
|
|
4
|
+
import { jcsHash } from "@zanii/core";
|
|
5
|
+
import { isDid, isInt, isIso, isObj, isSha, ms, SLUG, sortedUnique } from "./shared.js";
|
|
6
|
+
import { signatureOk, signatureOver, signerDid } from "./signer.js";
|
|
7
|
+
import { logPositionOk } from "./status.js";
|
|
8
|
+
import { schemeAuthority } from "./scheme-authority.js";
|
|
9
|
+
export async function bodySignature(body, signer) {
|
|
10
|
+
if ("signatures" in body)
|
|
11
|
+
throw new Error("sign the body, without signatures");
|
|
12
|
+
return { signer: signerDid(signer), sig: await signatureOver(body, signer) };
|
|
13
|
+
}
|
|
14
|
+
const bySigner = (a, b) => (a.signer < b.signer ? -1 : a.signer > b.signer ? 1 : 0);
|
|
15
|
+
/** The document with its signatures sorted by signer; one signer signs once. */
|
|
16
|
+
export function withSignatures(body, sigs) {
|
|
17
|
+
if ("signatures" in body)
|
|
18
|
+
throw new Error("the body already carries signatures");
|
|
19
|
+
const sorted = sigs.map((s) => ({ signer: s.signer, sig: s.sig })).sort(bySigner);
|
|
20
|
+
if (new Set(sorted.map((s) => s.signer)).size !== sorted.length)
|
|
21
|
+
throw new Error("a signer signs once");
|
|
22
|
+
return { ...body, signatures: sorted };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Section 1: the signers of `doc` whose signature verifies, and whether `signatures` is well formed
|
|
26
|
+
* (1 to 64 `{signer, sig}`, strictly sorted by signer, each verifying, each in `allowed` when given).
|
|
27
|
+
*/
|
|
28
|
+
export function signersOf(doc, allowed) {
|
|
29
|
+
const { signatures, ...body } = doc;
|
|
30
|
+
if (!Array.isArray(signatures) || signatures.length < 1 || signatures.length > (doc.type === "certify.board-transition" ? 65 : 64))
|
|
31
|
+
return { signers: [], wellFormed: false };
|
|
32
|
+
const signers = [];
|
|
33
|
+
let wellFormed = true;
|
|
34
|
+
let last = "";
|
|
35
|
+
for (const s of signatures) {
|
|
36
|
+
if (!isObj(s) || Object.keys(s).length !== 2 || !isDid(s.signer) || s.signer <= last) {
|
|
37
|
+
wellFormed = false;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
last = s.signer;
|
|
41
|
+
if (!signatureOk(body, s.sig, s.signer) || (allowed && !allowed.includes(s.signer)))
|
|
42
|
+
wellFormed = false;
|
|
43
|
+
else
|
|
44
|
+
signers.push(s.signer);
|
|
45
|
+
}
|
|
46
|
+
return { signers, wellFormed };
|
|
47
|
+
}
|
|
48
|
+
const sortedDistinct = (xs) => Array.isArray(xs) && xs.every((x) => typeof x === "string") && sortedUnique(xs).join("\n") === xs.join("\n");
|
|
49
|
+
function boardError(b) {
|
|
50
|
+
if (!isDid(b.owner))
|
|
51
|
+
return "owner must be a did:key";
|
|
52
|
+
if (!logPositionOk(b.seq, b.prev))
|
|
53
|
+
return "seq and prev are not a log position";
|
|
54
|
+
const m = b.members;
|
|
55
|
+
if (!Array.isArray(m) || m.length < 1 || m.length > 64 || !m.every(isDid) || !sortedDistinct(m))
|
|
56
|
+
return "members must be 1 to 64 sorted, distinct did:keys";
|
|
57
|
+
if (!isInt(b.threshold, 1, m.length))
|
|
58
|
+
return "threshold must be an integer from 1 to the number of members";
|
|
59
|
+
if (!isIso(b.effective_from))
|
|
60
|
+
return "effective_from must be an ISO timestamp";
|
|
61
|
+
if ("ledger" in b && !isDid(b.ledger))
|
|
62
|
+
return "ledger must be a did:key";
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** The unsigned board (each signer signs it with `bodySignature`). */
|
|
66
|
+
export function boardBody(f) {
|
|
67
|
+
const body = {
|
|
68
|
+
v: 1,
|
|
69
|
+
type: "certify.board",
|
|
70
|
+
owner: f.owner,
|
|
71
|
+
seq: f.seq,
|
|
72
|
+
prev: f.prev,
|
|
73
|
+
members: Array.isArray(f.members) ? sortedUnique(f.members) : f.members,
|
|
74
|
+
threshold: f.threshold,
|
|
75
|
+
effective_from: f.effectiveFrom,
|
|
76
|
+
...(f.ledger !== undefined ? { ledger: f.ledger } : {}),
|
|
77
|
+
};
|
|
78
|
+
const err = boardError(body);
|
|
79
|
+
if (err)
|
|
80
|
+
throw new Error(err);
|
|
81
|
+
return body;
|
|
82
|
+
}
|
|
83
|
+
/** A board signed by `signers`: the owner for the first board, k of the previous board after. */
|
|
84
|
+
export async function buildBoard(f, signers) {
|
|
85
|
+
const body = boardBody(f);
|
|
86
|
+
return withSignatures(body, await Promise.all(signers.map((s) => bodySignature(body, s))));
|
|
87
|
+
}
|
|
88
|
+
export const isBoard = (b) => isObj(b) && b.v === 1 && b.type === "certify.board" && Object.keys(b).length === ("ledger" in b ? 10 : 9);
|
|
89
|
+
const ownerReason = (owner, opts) => opts.owner === undefined
|
|
90
|
+
? ["no trusted scheme owner supplied: the owner was NOT checked"]
|
|
91
|
+
: owner !== opts.owner
|
|
92
|
+
? ["the board log is not the trusted scheme owner's"]
|
|
93
|
+
: [];
|
|
94
|
+
/**
|
|
95
|
+
* Section 2: the boards form one log: board i at seq i naming board i - 1, one owner, each taking
|
|
96
|
+
* effect after the previous one; board 0 signed by the owner, every later board by at least the
|
|
97
|
+
* previous board's threshold of its members, and no other signature.
|
|
98
|
+
*/
|
|
99
|
+
export function verifyBoardLog(boards, opts = {}) {
|
|
100
|
+
if (!Array.isArray(boards) || boards.length === 0)
|
|
101
|
+
return { ok: false, reasons: ["the board log is empty"] };
|
|
102
|
+
const reasons = [];
|
|
103
|
+
boards.forEach((b, i) => {
|
|
104
|
+
const prev = i > 0 ? boards[i - 1] : null;
|
|
105
|
+
if (!isBoard(b)) {
|
|
106
|
+
reasons.push(`board ${i} is not a board (v1)`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const err = boardError(b);
|
|
110
|
+
if (err) {
|
|
111
|
+
reasons.push(`board ${i} is malformed: ${err}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (b.seq !== i)
|
|
115
|
+
reasons.push(`board ${i} is not at position ${i}`);
|
|
116
|
+
if (b.prev !== (i > 0 ? jcsHash(prev) : null))
|
|
117
|
+
reasons.push(`board ${i} does not name the previous board`);
|
|
118
|
+
if (i > 0 && isObj(boards[0]) && b.owner !== boards[0].owner)
|
|
119
|
+
reasons.push(`board ${i} names another scheme owner`);
|
|
120
|
+
if (i > 0 && isBoard(prev) && !(ms(b.effective_from) > ms(prev.effective_from)))
|
|
121
|
+
reasons.push(`board ${i} does not take effect after the previous board`);
|
|
122
|
+
const allowed = i === 0 ? [b.owner] : isBoard(prev) && Array.isArray(prev.members) ? prev.members : [];
|
|
123
|
+
const need = i === 0 ? 1 : isBoard(prev) && Number.isInteger(prev.threshold) ? prev.threshold : 1;
|
|
124
|
+
const s = signersOf(b, allowed);
|
|
125
|
+
if (!s.wellFormed)
|
|
126
|
+
reasons.push(`board ${i} carries a signature that is malformed, invalid or not by an authorised signer`);
|
|
127
|
+
if (i === 0 && s.signers.length < 1)
|
|
128
|
+
reasons.push("board 0 is not signed by the scheme owner");
|
|
129
|
+
if (i > 0 && s.signers.length < need)
|
|
130
|
+
reasons.push(`board ${i} is signed by ${s.signers.length} of the ${need} previous board members required`);
|
|
131
|
+
});
|
|
132
|
+
reasons.push(...ownerReason(isObj(boards[0]) ? boards[0].owner : undefined, opts));
|
|
133
|
+
return { ok: reasons.length === 0, reasons };
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------- 3. releases
|
|
136
|
+
export const RELEASE_KINDS = ["scheme", "profile"];
|
|
137
|
+
const SCHEME_REF = /^zanii-certify\/(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$/;
|
|
138
|
+
const PROFILE_REF = new RegExp(`^${SLUG.source.slice(1, -1)}/[1-9][0-9]{0,5}$`);
|
|
139
|
+
const text = (v) => typeof v === "string" && v.length >= 1 && v.length <= 4000;
|
|
140
|
+
export const isText = (t) => isObj(t) && Object.keys(t).length === 2 && text(t.en) && text(t.ar);
|
|
141
|
+
export function subjectOk(s) {
|
|
142
|
+
if (!isObj(s) || Object.keys(s).length !== 2 || typeof s.ref !== "string")
|
|
143
|
+
return false;
|
|
144
|
+
return s.kind === "scheme" ? SCHEME_REF.test(s.ref) : s.kind === "profile" && PROFILE_REF.test(s.ref);
|
|
145
|
+
}
|
|
146
|
+
function releaseError(r) {
|
|
147
|
+
if (!logPositionOk(r.seq, r.prev))
|
|
148
|
+
return "seq and prev are not a log position";
|
|
149
|
+
if (!subjectOk(r.subject))
|
|
150
|
+
return "subject must be {kind: scheme, ref: zanii-certify/<m>.<n>} or {kind: profile, ref: <slug>/<version>}";
|
|
151
|
+
if (!isSha(r.sha256))
|
|
152
|
+
return "sha256 must be a sha256 hash";
|
|
153
|
+
if (!isIso(r.effective_from))
|
|
154
|
+
return "effective_from must be an ISO timestamp";
|
|
155
|
+
if (!isText(r.notes))
|
|
156
|
+
return "notes need en and ar text (1-4000)";
|
|
157
|
+
if (!isSha(r.board))
|
|
158
|
+
return "board must be a board hash";
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
export function releaseBody(f) {
|
|
162
|
+
const body = {
|
|
163
|
+
v: 1,
|
|
164
|
+
type: "certify.release",
|
|
165
|
+
seq: f.seq,
|
|
166
|
+
prev: f.prev,
|
|
167
|
+
subject: { kind: f.subject?.kind, ref: f.subject?.ref },
|
|
168
|
+
sha256: f.sha256,
|
|
169
|
+
effective_from: f.effectiveFrom,
|
|
170
|
+
notes: { en: f.notes?.en, ar: f.notes?.ar },
|
|
171
|
+
board: f.board,
|
|
172
|
+
};
|
|
173
|
+
const err = releaseError(body);
|
|
174
|
+
if (err)
|
|
175
|
+
throw new Error(err);
|
|
176
|
+
return body;
|
|
177
|
+
}
|
|
178
|
+
/** A release signed by `signers` (members of the board it names). */
|
|
179
|
+
export async function buildRelease(f, signers) {
|
|
180
|
+
const body = releaseBody(f);
|
|
181
|
+
return withSignatures(body, await Promise.all(signers.map((s) => bodySignature(body, s))));
|
|
182
|
+
}
|
|
183
|
+
export const isRelease = (r) => isObj(r) && r.v === 1 && r.type === "certify.release" && Object.keys(r).length === 10;
|
|
184
|
+
/**
|
|
185
|
+
* A board-signed document (release, withdrawal, body register) against a board log already checked:
|
|
186
|
+
* the board it names is in the log and in force at its effective_from, and k of its members signed it.
|
|
187
|
+
*/
|
|
188
|
+
export function boardSignedReasons(r, boards, noun) {
|
|
189
|
+
const reasons = [];
|
|
190
|
+
const i = boards.findIndex((b) => isBoard(b) && jcsHash(b) === r.board);
|
|
191
|
+
const board = boards[i];
|
|
192
|
+
if (!isBoard(board))
|
|
193
|
+
return [`${noun} names a board that is not in the board log`];
|
|
194
|
+
const next = boards[i + 1];
|
|
195
|
+
const at = ms(r.effective_from);
|
|
196
|
+
if (!(ms(board.effective_from) <= at) || (isBoard(next) && !(ms(next.effective_from) > at)))
|
|
197
|
+
reasons.push(`the named board is not the board in force at the ${noun}'s effective_from`);
|
|
198
|
+
const need = Number.isInteger(board.threshold) ? board.threshold : 1;
|
|
199
|
+
const s = signersOf(r, Array.isArray(board.members) ? board.members : []);
|
|
200
|
+
if (!s.wellFormed)
|
|
201
|
+
reasons.push(`${noun} carries a signature that is malformed, invalid or not by a board member`);
|
|
202
|
+
if (s.signers.length < need)
|
|
203
|
+
reasons.push(`${noun} is signed by ${s.signers.length} of the ${need} board members required`);
|
|
204
|
+
return reasons;
|
|
205
|
+
}
|
|
206
|
+
/** One release against a board log already checked: fields, its board in force, k board signatures. */
|
|
207
|
+
function releaseReasons(r, boards) {
|
|
208
|
+
if (!isRelease(r))
|
|
209
|
+
return ["not a scheme release (v1)"];
|
|
210
|
+
const err = releaseError(r);
|
|
211
|
+
if (err)
|
|
212
|
+
return [`release is malformed: ${err}`];
|
|
213
|
+
return boardSignedReasons(r, boards, "release");
|
|
214
|
+
}
|
|
215
|
+
/** Section 3: one release, with the board log it names (checked with `verifyBoardLog`). */
|
|
216
|
+
export function verifyRelease(release, boards, opts = {}) {
|
|
217
|
+
const reasons = [...verifyBoardLog(boards, opts).reasons, ...releaseReasons(release, Array.isArray(boards) ? boards : [])];
|
|
218
|
+
return { ok: reasons.length === 0, reasons };
|
|
219
|
+
}
|
|
220
|
+
export const subjectKey = (s) => (isObj(s) ? `${String(s.kind)} ${String(s.ref)}` : "?");
|
|
221
|
+
/** Section 4: the release log: every release valid, at seq i naming release i - 1, effective_from never going back, each subject released once. */
|
|
222
|
+
export function verifyReleaseLog(releases, boards, opts = {}) {
|
|
223
|
+
if (opts.schemeLog !== undefined) {
|
|
224
|
+
const { reasons } = schemeAuthority(opts.schemeLog, opts.owner, { "certify.board": boards, "certify.release": releases });
|
|
225
|
+
return { ok: reasons.length === 0, reasons };
|
|
226
|
+
}
|
|
227
|
+
const reasons = [...verifyBoardLog(boards, opts).reasons];
|
|
228
|
+
if (!Array.isArray(releases) || releases.length === 0)
|
|
229
|
+
return { ok: false, reasons: [...reasons, "the release log is empty"] };
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
releases.forEach((r, i) => {
|
|
232
|
+
reasons.push(...releaseReasons(r, Array.isArray(boards) ? boards : []).map((x) => `release ${i}: ${x}`));
|
|
233
|
+
if (!isRelease(r))
|
|
234
|
+
return;
|
|
235
|
+
const prev = i > 0 ? releases[i - 1] : null;
|
|
236
|
+
if (r.seq !== i)
|
|
237
|
+
reasons.push(`release ${i} is not at position ${i}`);
|
|
238
|
+
if (r.prev !== (i > 0 ? jcsHash(prev) : null))
|
|
239
|
+
reasons.push(`release ${i} does not name the previous release`);
|
|
240
|
+
if (i > 0 && isRelease(prev) && !(ms(r.effective_from) >= ms(prev.effective_from)))
|
|
241
|
+
reasons.push(`release ${i} takes effect before the previous release`);
|
|
242
|
+
const key = subjectKey(r.subject);
|
|
243
|
+
if (seen.has(key))
|
|
244
|
+
reasons.push(`release ${i} releases ${key} again`);
|
|
245
|
+
seen.add(key);
|
|
246
|
+
});
|
|
247
|
+
return { ok: reasons.length === 0, reasons };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Section 5: may a lab use `subject` at `at`? The release log must verify, release the subject, take
|
|
251
|
+
* effect no later than `at` and (with `sha256`) for exactly that content. With `withdrawals`
|
|
252
|
+
* (section 6), a release withdrawn at or before `at` may not be used.
|
|
253
|
+
*/
|
|
254
|
+
export function requireRelease(releases, boards, subject, at, opts = {}) {
|
|
255
|
+
if (!isIso(at))
|
|
256
|
+
throw new Error("at must be an ISO timestamp");
|
|
257
|
+
const authority = opts.schemeLog !== undefined ? schemeAuthority(opts.schemeLog, opts.owner, { "certify.board": boards, "certify.release": releases, ...(opts.withdrawals ? { "certify.release-withdrawal": opts.withdrawals } : {}) }) : null;
|
|
258
|
+
const reasons = [...(authority ? authority.reasons : verifyReleaseLog(releases, boards, opts).reasons)];
|
|
259
|
+
const key = subjectKey(subject);
|
|
260
|
+
const r = (Array.isArray(releases) ? releases : []).find((x) => isRelease(x) && !authority?.canceled.has(jcsHash(x)) && subjectKey(x.subject) === key);
|
|
261
|
+
if (!r)
|
|
262
|
+
reasons.push(`${key} has no release`);
|
|
263
|
+
else {
|
|
264
|
+
if (!(ms(r.effective_from) <= ms(at)))
|
|
265
|
+
reasons.push(`${key} is not released until ${r.effective_from}`);
|
|
266
|
+
if (opts.sha256 !== undefined && r.sha256 !== opts.sha256)
|
|
267
|
+
reasons.push(`${key} content is not the released content`);
|
|
268
|
+
}
|
|
269
|
+
if (opts.withdrawals === undefined)
|
|
270
|
+
return { ok: reasons.length === 0, reasons, release: r ?? null };
|
|
271
|
+
if (!authority)
|
|
272
|
+
reasons.push(...withdrawalReasons(opts.withdrawals, releases, boards));
|
|
273
|
+
const w = r ? withdrawalOf((Array.isArray(opts.withdrawals) ? opts.withdrawals : []).filter(x => !authority?.canceled.size || !authority.canceled.has(jcsHash(x))), r) : null;
|
|
274
|
+
if (w && ms(w.effective_from) <= ms(at))
|
|
275
|
+
reasons.push(`${key} is withdrawn from ${w.effective_from} (${w.reason})`);
|
|
276
|
+
return { ok: reasons.length === 0, reasons, release: r ?? null, withdrawal: w };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Section 4a: may `board` be appended to the board log now? The board log with it verifies, it takes
|
|
280
|
+
* effect after the effective_from of every release already in the release log, and the release log
|
|
281
|
+
* still verifies against the longer board log (a board change never breaks a release).
|
|
282
|
+
*/
|
|
283
|
+
export function verifyBoardAppend(boards, releases, board, opts = {}) {
|
|
284
|
+
const next = [...(Array.isArray(boards) ? boards : []), board];
|
|
285
|
+
const rel = Array.isArray(releases) ? releases : [];
|
|
286
|
+
const reasons = [...verifyBoardLog(next, opts).reasons];
|
|
287
|
+
if (isBoard(board) && isIso(board.effective_from) && rel.some((r) => isRelease(r) && isIso(r.effective_from) && !(ms(board.effective_from) > ms(r.effective_from))))
|
|
288
|
+
reasons.push("the board does not take effect after every release's effective_from");
|
|
289
|
+
if (rel.length)
|
|
290
|
+
reasons.push(...verifyReleaseLog(rel, next, opts).reasons);
|
|
291
|
+
return { ok: reasons.length === 0, reasons: [...new Set(reasons)] };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Section 4a: may `release` be appended to the release log now? The release log with it verifies,
|
|
295
|
+
* and it is signed by the newest board of the board log (a board that was replaced signs nothing more).
|
|
296
|
+
*/
|
|
297
|
+
export function verifyReleaseAppend(releases, boards, release, opts = {}) {
|
|
298
|
+
const reasons = [...verifyReleaseLog([...(Array.isArray(releases) ? releases : []), release], boards, opts).reasons];
|
|
299
|
+
const last = Array.isArray(boards) ? boards.at(-1) : undefined;
|
|
300
|
+
if (isRelease(release) && !(isBoard(last) && release.board === jcsHash(last)))
|
|
301
|
+
reasons.push("the release is not signed by the newest board of the board log");
|
|
302
|
+
return { ok: reasons.length === 0, reasons: [...new Set(reasons)] };
|
|
303
|
+
}
|
|
304
|
+
// ---------------------------------------------------------------- 6. release withdrawals
|
|
305
|
+
export const WITHDRAWAL_REASONS = ["DEFECT", "SECURITY", "LEGAL", "OTHER"];
|
|
306
|
+
function withdrawalError(w) {
|
|
307
|
+
if (!logPositionOk(w.seq, w.prev))
|
|
308
|
+
return "seq and prev are not a log position";
|
|
309
|
+
if (!subjectOk(w.subject))
|
|
310
|
+
return "subject must be {kind: scheme, ref: zanii-certify/<m>.<n>} or {kind: profile, ref: <slug>/<version>}";
|
|
311
|
+
if (!isSha(w.release))
|
|
312
|
+
return "release must be a release hash";
|
|
313
|
+
if (!WITHDRAWAL_REASONS.includes(w.reason))
|
|
314
|
+
return `reason must be one of ${WITHDRAWAL_REASONS.join(", ")}`;
|
|
315
|
+
if (!isText(w.notes))
|
|
316
|
+
return "notes need en and ar text (1-4000)";
|
|
317
|
+
if (!isIso(w.effective_from))
|
|
318
|
+
return "effective_from must be an ISO timestamp";
|
|
319
|
+
if (!isSha(w.board))
|
|
320
|
+
return "board must be a board hash";
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
// Shared by the incremental verifier; field rules have one implementation.
|
|
324
|
+
export { boardError as boardFieldError, releaseError as releaseFieldError, withdrawalError as withdrawalFieldError };
|
|
325
|
+
export function releaseWithdrawalBody(f) {
|
|
326
|
+
const body = {
|
|
327
|
+
v: 1,
|
|
328
|
+
type: "certify.release-withdrawal",
|
|
329
|
+
seq: f.seq,
|
|
330
|
+
prev: f.prev,
|
|
331
|
+
subject: { kind: f.subject?.kind, ref: f.subject?.ref },
|
|
332
|
+
release: f.release,
|
|
333
|
+
reason: f.reason,
|
|
334
|
+
notes: { en: f.notes?.en, ar: f.notes?.ar },
|
|
335
|
+
effective_from: f.effectiveFrom,
|
|
336
|
+
board: f.board,
|
|
337
|
+
};
|
|
338
|
+
const err = withdrawalError(body);
|
|
339
|
+
if (err)
|
|
340
|
+
throw new Error(err);
|
|
341
|
+
return body;
|
|
342
|
+
}
|
|
343
|
+
/** A release withdrawal signed by `signers` (k members of the board it names). */
|
|
344
|
+
export async function buildReleaseWithdrawal(f, signers) {
|
|
345
|
+
const body = releaseWithdrawalBody(f);
|
|
346
|
+
return withSignatures(body, await Promise.all(signers.map((s) => bodySignature(body, s))));
|
|
347
|
+
}
|
|
348
|
+
export const isWithdrawal = (w) => isObj(w) && w.v === 1 && w.type === "certify.release-withdrawal" && Object.keys(w).length === 11;
|
|
349
|
+
/** The withdrawal of `release` in a list (a release is withdrawn at most once), or null. */
|
|
350
|
+
function withdrawalOf(withdrawals, release) {
|
|
351
|
+
const h = jcsHash(release);
|
|
352
|
+
return (Array.isArray(withdrawals) ? withdrawals : []).find((w) => isWithdrawal(w) && w.release === h) ?? null;
|
|
353
|
+
}
|
|
354
|
+
/** Section 6 without the release log's own reasons. */
|
|
355
|
+
function withdrawalReasons(withdrawals, releases, boards) {
|
|
356
|
+
if (!Array.isArray(withdrawals))
|
|
357
|
+
return ["the withdrawal log is not a list"];
|
|
358
|
+
const rel = Array.isArray(releases) ? releases : [];
|
|
359
|
+
const bl = Array.isArray(boards) ? boards : [];
|
|
360
|
+
const reasons = [];
|
|
361
|
+
const seen = new Set();
|
|
362
|
+
withdrawals.forEach((w, i) => {
|
|
363
|
+
if (!isWithdrawal(w)) {
|
|
364
|
+
reasons.push(`withdrawal ${i} is not a release withdrawal (v1)`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const err = withdrawalError(w);
|
|
368
|
+
if (err) {
|
|
369
|
+
reasons.push(`withdrawal ${i} is malformed: ${err}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const prev = i > 0 ? withdrawals[i - 1] : null;
|
|
373
|
+
if (w.seq !== i)
|
|
374
|
+
reasons.push(`withdrawal ${i} is not at position ${i}`);
|
|
375
|
+
if (w.prev !== (i > 0 ? jcsHash(prev) : null))
|
|
376
|
+
reasons.push(`withdrawal ${i} does not name the previous withdrawal`);
|
|
377
|
+
if (i > 0 && isWithdrawal(prev) && !(ms(w.effective_from) >= ms(prev.effective_from)))
|
|
378
|
+
reasons.push(`withdrawal ${i} takes effect before the previous withdrawal`);
|
|
379
|
+
const r = rel.find((x) => isRelease(x) && jcsHash(x) === w.release);
|
|
380
|
+
if (!isRelease(r))
|
|
381
|
+
reasons.push(`withdrawal ${i} names a release that is not in the release log`);
|
|
382
|
+
else {
|
|
383
|
+
if (subjectKey(r.subject) !== subjectKey(w.subject))
|
|
384
|
+
reasons.push(`withdrawal ${i} subject is not the release's subject`);
|
|
385
|
+
if (!(ms(w.effective_from) >= ms(r.effective_from)))
|
|
386
|
+
reasons.push(`withdrawal ${i} takes effect before the release`);
|
|
387
|
+
}
|
|
388
|
+
if (seen.has(w.release))
|
|
389
|
+
reasons.push(`withdrawal ${i} withdraws a release again`);
|
|
390
|
+
seen.add(w.release);
|
|
391
|
+
reasons.push(...boardSignedReasons(w, bl, "withdrawal").map((x) => `withdrawal ${i}: ${x}`));
|
|
392
|
+
});
|
|
393
|
+
return reasons;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Section 6: the withdrawal log: the release log verifies, every withdrawal at seq i naming
|
|
397
|
+
* withdrawal i - 1, effective_from never going back, naming a release of the log with its subject,
|
|
398
|
+
* taking effect no earlier than it, each release withdrawn once, signed by k of the board in force.
|
|
399
|
+
*/
|
|
400
|
+
export function verifyWithdrawalLog(withdrawals, releases, boards, opts = {}) {
|
|
401
|
+
if (opts.schemeLog !== undefined) {
|
|
402
|
+
const { reasons } = schemeAuthority(opts.schemeLog, opts.owner, { "certify.board": boards, "certify.release": releases, "certify.release-withdrawal": withdrawals });
|
|
403
|
+
return { ok: reasons.length === 0, reasons };
|
|
404
|
+
}
|
|
405
|
+
const reasons = [...verifyReleaseLog(releases, boards, opts).reasons, ...withdrawalReasons(withdrawals, releases, boards)];
|
|
406
|
+
return { ok: reasons.length === 0, reasons };
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Section 6: a certificate issued under a withdrawn scheme or profile version is flagged, never
|
|
410
|
+
* failed. `ok` is whether the withdrawal log verified; `flags` names each withdrawal that touches
|
|
411
|
+
* the certificate's scheme or profile.
|
|
412
|
+
*/
|
|
413
|
+
export function withdrawalFlags(cert, logs) {
|
|
414
|
+
const authority = logs.schemeLog !== undefined ? schemeAuthority(logs.schemeLog, logs.owner, { "certify.board": logs.boards, "certify.release": logs.releases, "certify.release-withdrawal": logs.withdrawals }) : null;
|
|
415
|
+
const reasons = authority ? authority.reasons : verifyWithdrawalLog(logs.withdrawals, logs.releases, logs.boards, logs.owner === undefined ? {} : { owner: logs.owner }).reasons;
|
|
416
|
+
const claim = isObj(cert?.claim) ? cert.claim : {};
|
|
417
|
+
const flags = [];
|
|
418
|
+
for (const subject of [{ kind: "scheme", ref: claim.scheme }, { kind: "profile", ref: claim.profile }]) {
|
|
419
|
+
const key = subjectKey(subject);
|
|
420
|
+
const r = (Array.isArray(logs.releases) ? logs.releases : []).find((x) => isRelease(x) && !authority?.canceled.has(jcsHash(x)) && subjectKey(x.subject) === key);
|
|
421
|
+
const w = r ? withdrawalOf((Array.isArray(logs.withdrawals) ? logs.withdrawals : []).filter(x => !authority?.canceled.size || !authority.canceled.has(jcsHash(x))), r) : null;
|
|
422
|
+
if (!w)
|
|
423
|
+
continue;
|
|
424
|
+
const after = ms(cert?.issued_at) >= ms(w.effective_from) ? "; the certificate was issued after it" : "";
|
|
425
|
+
flags.push(`${key} was withdrawn from ${w.effective_from} (${w.reason})${after}`);
|
|
426
|
+
}
|
|
427
|
+
return { ok: reasons.length === 0, reasons, flags };
|
|
428
|
+
}
|