@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/cli.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// spec/cli.md: the `certify` command. Its output is byte-identical with the Python `certify`.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { canonicalBytes } from "@zanii/core";
|
|
5
|
+
import { credentialHash, resolveIssuer, } from "@zanii/credentials";
|
|
6
|
+
import { claimReasons, verifyCertificate } from "./certificate.js";
|
|
7
|
+
import { verifyAuditAttestation } from "./audit.js";
|
|
8
|
+
import messages from "./cli-messages.json" with { type: "json" };
|
|
9
|
+
import { buildFingerprint, promptSha256, sha256Bytes, toolsSha256 } from "./fingerprint.js";
|
|
10
|
+
import { pqPolicyOf } from "./pq.js";
|
|
11
|
+
import { fetchJsonBounded, resolveIssuerDetailed } from "./issuer-resolution.js";
|
|
12
|
+
import { registerTrust } from "./register.js";
|
|
13
|
+
import { isDid, isInt, isIso, isObj } from "./shared.js";
|
|
14
|
+
import { verifyStatus } from "./status.js";
|
|
15
|
+
import { isIssuerPolicy } from "./verification-policy.js";
|
|
16
|
+
import { witnessReasons } from "./witness.js";
|
|
17
|
+
import { verifyCertificateVc } from "./vc.js";
|
|
18
|
+
/** spec/cli.md section 4. */
|
|
19
|
+
export const EXIT = { valid: 0, invalid: 1, partial: 2, usage: 3, online: 4 };
|
|
20
|
+
const OPTIONS = {
|
|
21
|
+
verify: ["--status", "--issuer-doc", "--fingerprint", "--online", "--now", "--lang", "--pq", "--audit", "--trusted-bodies", "--body-register", "--status-witness", "--release-policy", "--strict-policy"],
|
|
22
|
+
fingerprint: ["--model", "--prompt-file", "--tools-file", "--policy-file", "--knowledge-file", "--lang"],
|
|
23
|
+
};
|
|
24
|
+
// Control and bidirectional formatting characters from input files are not written to the terminal.
|
|
25
|
+
const UNSAFE = /[\u0000-\u001f\u007f-\u009f--]/g;
|
|
26
|
+
/** A value from an input file as printed: strings and integers as they are, nothing for a missing value, `?` otherwise. */
|
|
27
|
+
const show = (v) => typeof v === "string" ? v.replace(UNSAFE, "?") : Number.isInteger(v) ? String(v) : v == null ? "" : "?";
|
|
28
|
+
class Stop extends Error {
|
|
29
|
+
exit;
|
|
30
|
+
constructor(text, exit) {
|
|
31
|
+
super(text);
|
|
32
|
+
this.exit = exit;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Run `certify` with `argv` (without the program name); returns the exit code. */
|
|
36
|
+
export async function main(argv, io = {}) {
|
|
37
|
+
const out = io.out ?? ((s) => process.stdout.write(s));
|
|
38
|
+
const err = io.err ?? ((s) => process.stderr.write(s));
|
|
39
|
+
const li = argv.indexOf("--lang");
|
|
40
|
+
const lang = argv[li + 1] === "ar" && li >= 0 ? "ar" : "en";
|
|
41
|
+
const t = (key, ...args) => messages[lang][key].replace(/\{(\d)\}/g, (_, i) => show(args[Number(i)]));
|
|
42
|
+
const fail = (key, ...args) => new Stop(t("error", t(key, ...args)), EXIT.usage);
|
|
43
|
+
try {
|
|
44
|
+
const [cmd, ...rest] = argv;
|
|
45
|
+
if (cmd === "--help-v2") {
|
|
46
|
+
out(`${t("usage_v2")}\n`);
|
|
47
|
+
return EXIT.valid;
|
|
48
|
+
}
|
|
49
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
50
|
+
out(`${t("usage")}\n`);
|
|
51
|
+
return EXIT.valid;
|
|
52
|
+
}
|
|
53
|
+
if (!cmd || !OPTIONS[cmd])
|
|
54
|
+
throw new Stop(t("usage"), EXIT.usage);
|
|
55
|
+
const pos = [];
|
|
56
|
+
const opts = {};
|
|
57
|
+
for (let i = 0; i < rest.length; i++) {
|
|
58
|
+
const a = rest[i];
|
|
59
|
+
if (!a.startsWith("--"))
|
|
60
|
+
pos.push(a);
|
|
61
|
+
else if (!OPTIONS[cmd].includes(a))
|
|
62
|
+
throw fail("unknown_option", a);
|
|
63
|
+
else if (i + 1 >= rest.length)
|
|
64
|
+
throw fail("missing_value", a);
|
|
65
|
+
else
|
|
66
|
+
opts[a] = rest[++i];
|
|
67
|
+
}
|
|
68
|
+
if (opts["--lang"] !== undefined && opts["--lang"] !== "en" && opts["--lang"] !== "ar")
|
|
69
|
+
throw fail("bad_lang");
|
|
70
|
+
const read = (path) => {
|
|
71
|
+
try {
|
|
72
|
+
return readFileSync(path);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw fail("cannot_read", path);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const text = (path) => {
|
|
79
|
+
try {
|
|
80
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(read(path));
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
throw e instanceof Stop ? e : fail("not_text", path);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const json = (path) => {
|
|
87
|
+
const s = text(path);
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(s);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw fail("not_json", path);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
if (cmd === "fingerprint") {
|
|
96
|
+
if (pos.length || opts["--model"] === undefined)
|
|
97
|
+
throw new Stop(t("usage"), EXIT.usage);
|
|
98
|
+
const p = opts["--prompt-file"];
|
|
99
|
+
const tf = opts["--tools-file"];
|
|
100
|
+
let tools = tf === undefined ? null : json(tf);
|
|
101
|
+
if (isObj(tools) && Array.isArray(tools.tools))
|
|
102
|
+
tools = tools.tools;
|
|
103
|
+
if (tf !== undefined && !Array.isArray(tools))
|
|
104
|
+
throw fail("not_tools", tf);
|
|
105
|
+
const pack = (k) => (opts[k] === undefined ? null : sha256Bytes(read(opts[k])));
|
|
106
|
+
let fp;
|
|
107
|
+
try {
|
|
108
|
+
fp = buildFingerprint({
|
|
109
|
+
model: opts["--model"],
|
|
110
|
+
prompt: p === undefined ? null : promptSha256(text(p)),
|
|
111
|
+
tools: Array.isArray(tools) ? toolsSha256(tools) : null,
|
|
112
|
+
policy: pack("--policy-file"),
|
|
113
|
+
knowledge: pack("--knowledge-file"),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
throw e instanceof Stop ? e : new Stop(t("error", e.message), EXIT.usage);
|
|
118
|
+
}
|
|
119
|
+
out(`${new TextDecoder().decode(canonicalBytes(fp))}\n`);
|
|
120
|
+
return EXIT.valid;
|
|
121
|
+
}
|
|
122
|
+
// verify
|
|
123
|
+
if (pos.length !== 1)
|
|
124
|
+
throw new Stop(t("usage"), EXIT.usage);
|
|
125
|
+
const now = opts["--now"];
|
|
126
|
+
if (now !== undefined && !isIso(now))
|
|
127
|
+
throw fail("bad_now");
|
|
128
|
+
const input = json(pos[0]);
|
|
129
|
+
// A W3C VC export (spec/vc.md) is verified, then its certificate is checked as any other.
|
|
130
|
+
const vc = isObj(input) && Array.isArray(input.type) && input.type.includes("VerifiableCredential") ? verifyCertificateVc(input, now ? { at: now } : {}) : null;
|
|
131
|
+
if (vc && !vc.certificate) {
|
|
132
|
+
out(`${t("vc_bad")}\n${t("result_invalid")}\n`);
|
|
133
|
+
return EXIT.invalid;
|
|
134
|
+
}
|
|
135
|
+
const cert = (vc?.certificate ?? input);
|
|
136
|
+
let status;
|
|
137
|
+
let list;
|
|
138
|
+
let onlineStatusPq;
|
|
139
|
+
const takeStatus = (v, where) => {
|
|
140
|
+
if (isObj(v) && v.type === "certify.status")
|
|
141
|
+
status = v;
|
|
142
|
+
else if (isObj(v) && isObj(v.status) && v.status.type === "certify.status" && isObj(v.revocation_list) && v.revocation_list.type === "revocation_list") {
|
|
143
|
+
status = v.status;
|
|
144
|
+
list = v.revocation_list;
|
|
145
|
+
if (isObj(v.pq))
|
|
146
|
+
onlineStatusPq = v.pq;
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
throw fail("not_status", where);
|
|
150
|
+
};
|
|
151
|
+
if (opts["--status"] !== undefined)
|
|
152
|
+
takeStatus(json(opts["--status"]), opts["--status"]);
|
|
153
|
+
const base = opts["--online"];
|
|
154
|
+
const doFetch = io.fetch ?? fetch;
|
|
155
|
+
if (base !== undefined && !status) {
|
|
156
|
+
let url = base;
|
|
157
|
+
try {
|
|
158
|
+
const u = new URL("/v1/status", base);
|
|
159
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
160
|
+
throw new Error("not http(s)");
|
|
161
|
+
url = u.href;
|
|
162
|
+
const res = await fetchJsonBounded(url, { fetch: doFetch, maxBytes: 8_388_608 });
|
|
163
|
+
if (!res.ok)
|
|
164
|
+
throw new Error(res.code);
|
|
165
|
+
takeStatus(res.document, url);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
throw e instanceof Stop ? e : new Stop(t("error", t("online_failed", url)), EXIT.online);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
let resolver;
|
|
172
|
+
// certificate.md section 5: the issuer document's status_dids, when it lists them
|
|
173
|
+
let issuerDoc;
|
|
174
|
+
let resolutionFailure;
|
|
175
|
+
if (opts["--issuer-doc"] !== undefined) {
|
|
176
|
+
const doc = json(opts["--issuer-doc"]);
|
|
177
|
+
issuerDoc = doc;
|
|
178
|
+
resolver = (d, domain) => resolveIssuer(d, domain, { fetchImpl: async () => Response.json(doc) });
|
|
179
|
+
}
|
|
180
|
+
else if (base !== undefined) {
|
|
181
|
+
const resolved = await resolveIssuerDetailed(String(cert?.issuer), String(cert?.issuer_domain), { fetch: doFetch });
|
|
182
|
+
if (resolved.ok)
|
|
183
|
+
issuerDoc = resolved.document;
|
|
184
|
+
else
|
|
185
|
+
resolutionFailure = resolved;
|
|
186
|
+
resolver = (d, domain) => issuerDoc === undefined ? null : resolveIssuer(d, domain, { fetchImpl: async () => Response.json(issuerDoc) });
|
|
187
|
+
}
|
|
188
|
+
const sd = isObj(issuerDoc) ? issuerDoc.status_dids : undefined;
|
|
189
|
+
const statusDids = Array.isArray(sd) ? sd.filter((x) => typeof x === "string") : undefined;
|
|
190
|
+
const fingerprint = opts["--fingerprint"] === undefined ? undefined : json(opts["--fingerprint"]);
|
|
191
|
+
const policyFile = opts["--strict-policy"] === undefined ? undefined : json(opts["--strict-policy"]);
|
|
192
|
+
if (policyFile !== undefined && !isIssuerPolicy(policyFile))
|
|
193
|
+
throw fail("bad_policy", "--strict-policy");
|
|
194
|
+
const issuerPolicy = isIssuerPolicy(policyFile) ? policyFile : undefined;
|
|
195
|
+
const audit = opts["--audit"] === undefined ? undefined : json(opts["--audit"]);
|
|
196
|
+
if (audit !== undefined && (!isObj(audit) || audit.type !== "certify.audit-attestation"))
|
|
197
|
+
throw fail("bad_policy", "--audit");
|
|
198
|
+
const trustedBodies = opts["--trusted-bodies"] === undefined ? undefined : json(opts["--trusted-bodies"]);
|
|
199
|
+
if (trustedBodies !== undefined && (!Array.isArray(trustedBodies) || trustedBodies.length > 64 || !trustedBodies.every(isDid)))
|
|
200
|
+
throw fail("bad_policy", "--trusted-bodies");
|
|
201
|
+
const bodyRegister = opts["--body-register"] === undefined ? undefined : json(opts["--body-register"]);
|
|
202
|
+
if (bodyRegister !== undefined && (!isObj(bodyRegister) || !isDid(bodyRegister.owner) || !Array.isArray(bodyRegister.boards) || bodyRegister.boards.length > 10_000 || !Array.isArray(bodyRegister.registers) || bodyRegister.registers.length > 10_000))
|
|
203
|
+
throw fail("bad_policy", "--body-register");
|
|
204
|
+
const statusWitness = opts["--status-witness"] === undefined ? undefined : json(opts["--status-witness"]);
|
|
205
|
+
if (statusWitness !== undefined && (!isObj(statusWitness) || !isObj(statusWitness.checkpoint) || !Array.isArray(statusWitness.proof) || statusWitness.proof.length > 32
|
|
206
|
+
|| !Array.isArray(statusWitness.cosignatures) || statusWitness.cosignatures.length > 64 || !Array.isArray(statusWitness.trusted) || statusWitness.trusted.length > 64
|
|
207
|
+
|| !statusWitness.trusted.every(isDid) || !isInt(statusWitness.k, 1, 64)))
|
|
208
|
+
throw fail("bad_policy", "--status-witness");
|
|
209
|
+
const releases = opts["--release-policy"] === undefined ? undefined : json(opts["--release-policy"]);
|
|
210
|
+
if (releases !== undefined && !isObj(releases))
|
|
211
|
+
throw fail("bad_policy", "--release-policy");
|
|
212
|
+
// spec/cli.md section 2: `--pq <file>` holds the co-signatures ({certificate, status} or one
|
|
213
|
+
// for the certificate); `--pq off` opts out of the issuer document's require_pq, and is printed.
|
|
214
|
+
const pqArg = opts["--pq"];
|
|
215
|
+
const pqFile = pqArg === undefined || pqArg === "off" ? undefined : json(pqArg);
|
|
216
|
+
const pqPair = isObj(pqFile) && pqFile.type !== "certify.pq-signature" ? pqFile : { certificate: pqFile };
|
|
217
|
+
let certPq = isObj(pqPair.certificate) ? pqPair.certificate : undefined;
|
|
218
|
+
let statusPq = isObj(pqPair.status) ? pqPair.status : undefined;
|
|
219
|
+
if (pqFile !== undefined && !certPq && !statusPq)
|
|
220
|
+
throw fail("not_pq", pqArg);
|
|
221
|
+
const c = (isObj(cert) ? cert : {});
|
|
222
|
+
if (c.v !== 1 || c.type !== "credential" || !c.issuer || !c.holder || !c.issuer_domain) {
|
|
223
|
+
out(`${t("not_certificate")}\n${t("result_invalid")}\n`);
|
|
224
|
+
return EXIT.invalid;
|
|
225
|
+
}
|
|
226
|
+
const onlinePq = base !== undefined && pqArg !== "off" && (pqPolicyOf(issuerPolicy ?? issuerDoc, false, String(c.issued_at)) === "required");
|
|
227
|
+
if (onlinePq) {
|
|
228
|
+
statusPq ??= onlineStatusPq;
|
|
229
|
+
if (status && !statusPq) {
|
|
230
|
+
const url = new URL("/v1/status", base).href;
|
|
231
|
+
const response = await fetchJsonBounded(url, { fetch: doFetch, maxBytes: 8_388_608 });
|
|
232
|
+
if (!response.ok)
|
|
233
|
+
throw new Stop(t("error", t("online_failed", url)), EXIT.online);
|
|
234
|
+
if (isObj(response.document) && isObj(response.document.pq))
|
|
235
|
+
statusPq = response.document.pq;
|
|
236
|
+
}
|
|
237
|
+
if (!certPq) {
|
|
238
|
+
let url;
|
|
239
|
+
try {
|
|
240
|
+
url = new URL(`/v1/certificates/${encodeURIComponent(credentialHash(cert))}`, base).href;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
throw new Stop(t("error", t("online_failed", base)), EXIT.online);
|
|
244
|
+
}
|
|
245
|
+
const response = await fetchJsonBounded(url, { fetch: doFetch, maxBytes: 1_048_576 });
|
|
246
|
+
if (!response.ok)
|
|
247
|
+
throw new Stop(t("error", t("online_failed", url)), EXIT.online);
|
|
248
|
+
if (isObj(response.document) && isObj(response.document.pq))
|
|
249
|
+
certPq = response.document.pq;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const r = await verifyCertificate(cert, {
|
|
253
|
+
...(status ? { status } : {}),
|
|
254
|
+
...(list ? { revocationList: list } : {}),
|
|
255
|
+
...(resolver ? { resolver } : {}),
|
|
256
|
+
...(statusDids ? { statusDids } : {}),
|
|
257
|
+
...(fingerprint ? { fingerprint } : {}),
|
|
258
|
+
...(now ? { now } : {}),
|
|
259
|
+
...(issuerDoc !== undefined ? { issuerDoc } : {}),
|
|
260
|
+
...(pqArg === "off" ? { allowNoPq: true } : {}),
|
|
261
|
+
...(certPq ? { pq: certPq } : {}),
|
|
262
|
+
...(statusPq ? { statusPq } : {}),
|
|
263
|
+
...(issuerPolicy ? { issuerPolicy } : {}), ...(audit !== undefined ? { audit } : {}), ...(trustedBodies ? { trustedBodies } : {}),
|
|
264
|
+
...(bodyRegister ? { bodyRegister } : {}), ...(statusWitness ? { statusWitness } : {}), ...(releases !== undefined ? { releases } : {}),
|
|
265
|
+
});
|
|
266
|
+
const cr = r.credential.reasons;
|
|
267
|
+
const claim = isObj(c.claim) ? c.claim : {};
|
|
268
|
+
const lines = [
|
|
269
|
+
t("certificate", credentialHash(cert)),
|
|
270
|
+
t("holder", c.holder),
|
|
271
|
+
t("profile", claim.profile, claim.level),
|
|
272
|
+
t("issuer", c.issuer, c.issuer_domain),
|
|
273
|
+
t("expires", c.exp),
|
|
274
|
+
];
|
|
275
|
+
const marks = [];
|
|
276
|
+
const check = (name, mark, detail) => {
|
|
277
|
+
marks.push(mark);
|
|
278
|
+
lines.push(`[${mark}] ${t(name)}: ${detail}`);
|
|
279
|
+
};
|
|
280
|
+
if (vc)
|
|
281
|
+
check("vc", "ok", t("vc_ok"));
|
|
282
|
+
const sigBad = cr.includes("bad issuer signature");
|
|
283
|
+
check("signature", sigBad ? "fail" : "ok", t(sigBad ? "sig_bad" : "sig_ok"));
|
|
284
|
+
const expBad = cr.some((x) => x === "invalid exp" || x.startsWith("credential expired on"));
|
|
285
|
+
check("expiry", expBad ? "fail" : "ok", t(expBad ? "exp_bad" : "exp_ok", c.exp));
|
|
286
|
+
const claimBad = claimReasons(cert).length > 0;
|
|
287
|
+
check("claim", claimBad ? "fail" : "ok", t(claimBad ? "claim_bad" : "claim_ok"));
|
|
288
|
+
if (!status)
|
|
289
|
+
check("status", "skip", t("not_supplied"));
|
|
290
|
+
else {
|
|
291
|
+
// after a key rotation the document is signed by another DID the issuer domain publishes
|
|
292
|
+
const rotated = status.issuer !== c.issuer && !!statusDids?.includes(status.issuer) && !!resolver && !!(await resolver(status.issuer, String(c.issuer_domain)));
|
|
293
|
+
const s = verifyStatus(status, { issuer: rotated ? status.issuer : String(c.issuer), ...(now ? { now } : {}) });
|
|
294
|
+
const e = r.status;
|
|
295
|
+
if (s.reasons.some((x) => !x.startsWith("status document expired on")) || r.reasons.includes("status document is not signed by a current status key of the issuer"))
|
|
296
|
+
check("status", "fail", t("status_invalid"));
|
|
297
|
+
else if (s.freshness === "stale")
|
|
298
|
+
check("status", "fail", t("status_stale", status.valid_until));
|
|
299
|
+
else if (!e)
|
|
300
|
+
check("status", "fail", t("status_unlisted"));
|
|
301
|
+
else if (e.status === "valid")
|
|
302
|
+
check("status", "ok", t("status_valid", e.since));
|
|
303
|
+
else
|
|
304
|
+
check("status", "fail", t(e.status === "suspended" ? "status_suspended" : "status_revoked", e.reason, e.since));
|
|
305
|
+
}
|
|
306
|
+
if (!list)
|
|
307
|
+
check("revocation", "skip", t("not_supplied"));
|
|
308
|
+
else if (["revocation list is from a different issuer", "revocation list signature is invalid", "revocation list is not a revocation list (v1)", "revocation list is not signed by a current status key of the issuer"].some((x) => cr.includes(x)))
|
|
309
|
+
check("revocation", "fail", t("rev_invalid"));
|
|
310
|
+
else if (cr.includes("credential has been REVOKED by its issuer"))
|
|
311
|
+
check("revocation", "fail", t("rev_revoked"));
|
|
312
|
+
else if (cr.some((x) => x.startsWith("revocation list expired on")))
|
|
313
|
+
check("revocation", "fail", t("rev_stale", list.valid_until));
|
|
314
|
+
else
|
|
315
|
+
check("revocation", "ok", t("rev_ok"));
|
|
316
|
+
if (!resolver)
|
|
317
|
+
check("issuer_domain", "skip", t("issuer_skip"));
|
|
318
|
+
else {
|
|
319
|
+
const bad = cr.some((x) => x.includes(" is not published by "));
|
|
320
|
+
check("issuer_domain", bad ? "fail" : "ok", resolutionFailure && resolutionFailure.code !== "key_missing" ? t("issuer_resolution", resolutionFailure.code) : t(bad ? "issuer_bad" : "issuer_ok", c.issuer_domain));
|
|
321
|
+
}
|
|
322
|
+
if (r.fingerprint === null)
|
|
323
|
+
check("fingerprint", "skip", t("not_supplied"));
|
|
324
|
+
else
|
|
325
|
+
check("fingerprint", r.fingerprint === "match" ? "ok" : "fail", t(r.fingerprint === "match" ? "fp_ok" : "fp_bad"));
|
|
326
|
+
if (claim.level === 3) {
|
|
327
|
+
if (audit === undefined)
|
|
328
|
+
check("audit", "skip", t("audit_skip"));
|
|
329
|
+
else {
|
|
330
|
+
const reg = bodyRegister ? registerTrust(cert, audit?.body, bodyRegister) : null;
|
|
331
|
+
const checked = verifyAuditAttestation(audit, { certificate: cert, trustedBodies: reg?.trusted ?? trustedBodies, ...(now ? { now } : {}) });
|
|
332
|
+
const bad = !!reg?.reasons.length || !checked.ok;
|
|
333
|
+
check("audit", bad ? "fail" : "ok", t(bad ? "audit_bad" : "audit_ok"));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (statusWitness) {
|
|
337
|
+
const checked = status ? witnessReasons(status, statusWitness) : null;
|
|
338
|
+
check("witness", checked && !checked.reasons.length ? "ok" : "fail", t(checked && !checked.reasons.length ? "policy_ok" : "policy_bad"));
|
|
339
|
+
}
|
|
340
|
+
for (const [policy, key, prefix] of [[issuerPolicy, "issuer_policy", "issuer policy:"], [releases, "release_policy", "release policy:"]])
|
|
341
|
+
if (policy !== undefined) {
|
|
342
|
+
const bad = r.reasons.filter((reason) => reason.startsWith(prefix));
|
|
343
|
+
check(key, bad.length ? "fail" : "ok", bad.length ? bad.join("; ") : t("policy_ok"));
|
|
344
|
+
}
|
|
345
|
+
if (r.pqPolicy === "opted_out")
|
|
346
|
+
check("pq", "skip", t("pq_optout"));
|
|
347
|
+
else if (r.pqPolicy === "required" || pqFile !== undefined || onlinePq || issuerPolicy?.require_pq) {
|
|
348
|
+
const bad = r.reasons.filter((x) => x.includes("post-quantum"));
|
|
349
|
+
if (!bad.length)
|
|
350
|
+
check("pq", "ok", t("pq_ok"));
|
|
351
|
+
else
|
|
352
|
+
check("pq", "fail", t(bad.every((x) => x.endsWith("a post-quantum signature is required")) ? "pq_missing" : "pq_bad"));
|
|
353
|
+
}
|
|
354
|
+
// A reason no line explains (none is expected) still makes the certificate invalid.
|
|
355
|
+
const failed = marks.includes("fail") || (!marks.includes("skip") && !r.ok);
|
|
356
|
+
const code = failed ? EXIT.invalid : marks.includes("skip") ? EXIT.partial : EXIT.valid;
|
|
357
|
+
lines.push(t(code === EXIT.valid ? "result_valid" : code === EXIT.invalid ? "result_invalid" : "result_partial"));
|
|
358
|
+
out(`${lines.join("\n")}\n`);
|
|
359
|
+
return resolutionFailure && ["dns", "tls", "timeout", "network", "busy", "http", "too_large"].includes(resolutionFailure.code) ? EXIT.online : code;
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
if (!(e instanceof Stop))
|
|
363
|
+
throw e;
|
|
364
|
+
err(`${e.message}\n`);
|
|
365
|
+
return e.exit;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (import.meta.main)
|
|
369
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Credential } from "@zanii/credentials";
|
|
2
|
+
import { type VerifiableCredential } from "@zanii/vc";
|
|
3
|
+
import { type Signer } from "./signer.ts";
|
|
4
|
+
import { type CertificateVcCheck } from "./vc.ts";
|
|
5
|
+
export interface CurrentIssuerVcOptions {
|
|
6
|
+
created: string;
|
|
7
|
+
index: number;
|
|
8
|
+
revocationList: string;
|
|
9
|
+
suspensionList: string;
|
|
10
|
+
owner: string;
|
|
11
|
+
issuerDoc: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface CurrentIssuerVcTrust {
|
|
14
|
+
owner: string;
|
|
15
|
+
issuers: readonly string[];
|
|
16
|
+
domain: string;
|
|
17
|
+
at: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function certificateToCurrentIssuerVc(cert: Credential, signer: Signer, opts: CurrentIssuerVcOptions): Promise<VerifiableCredential>;
|
|
20
|
+
export declare function verifyCurrentIssuerCertificateVc(vc: unknown, opts: CurrentIssuerVcTrust): CertificateVcCheck;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// A current-authority wrapper preserves the original signature; independent roots still establish trust.
|
|
2
|
+
import { publicKeyFromDid } from "@zanii/core";
|
|
3
|
+
import { credentialHash } from "@zanii/credentials";
|
|
4
|
+
import { VC_CONTEXT, verifyVC } from "@zanii/vc";
|
|
5
|
+
import { bitstringStatusEntry, isBitstringStatusEntry } from "./bitstring-status.js";
|
|
6
|
+
import { claimReasons } from "./certificate.js";
|
|
7
|
+
import { issuerArtifactAuthorization } from "./issuer-authorization.js";
|
|
8
|
+
import { isIso, isObj, ms } from "./shared.js";
|
|
9
|
+
import { signatureOk, signerDid } from "./signer.js";
|
|
10
|
+
import { signVcWith } from "./vc.js";
|
|
11
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
12
|
+
const did = (v) => typeof v === "string" && v.length <= 69 && publicKeyFromDid(v) !== null;
|
|
13
|
+
function authorization(cert, current, document, owner, domain, at) {
|
|
14
|
+
if (!withinVerificationBudget(document, { maxBytes: 1_048_576 }))
|
|
15
|
+
return ["issuer authorization exceeds its JSON budget"];
|
|
16
|
+
if (cert?.issuer_domain !== domain)
|
|
17
|
+
return ["current issuer wrapper requires independently pinned owner, domain and current key"];
|
|
18
|
+
if (!isObj(cert) || Object.keys(cert).length !== 10 || cert.v !== 1 || cert.type !== "credential" || !did(cert.issuer) || !did(cert.holder) || !isIso(cert.issued_at) || !isIso(cert.exp) || ms(cert.exp) <= ms(cert.issued_at)
|
|
19
|
+
|| ms(cert.issued_at) > ms(at) || claimReasons(cert).length)
|
|
20
|
+
return ["invalid original certificate in current issuer wrapper"];
|
|
21
|
+
const { sig, ...unsigned } = cert;
|
|
22
|
+
if (typeof sig !== "string" || sig.length !== 136 || !signatureOk(unsigned, sig, cert.issuer))
|
|
23
|
+
return ["bad original certificate signature"];
|
|
24
|
+
return issuerArtifactAuthorization({ signer: cert.issuer, issued_at: cert.issued_at }, current, document, owner, domain, at).map((r) => r.replaceAll("artifact", "original certificate"));
|
|
25
|
+
}
|
|
26
|
+
export async function certificateToCurrentIssuerVc(cert, signer, opts) {
|
|
27
|
+
if (!withinVerificationBudget({ cert, opts }, { maxBytes: 4_194_304 }) || !isIso(opts.created) || opts.revocationList === opts.suspensionList)
|
|
28
|
+
throw new Error("invalid or oversized current issuer wrapper input");
|
|
29
|
+
const current = signerDid(signer), reasons = authorization(cert, current, opts.issuerDoc, opts.owner, cert?.issuer_domain, opts.created);
|
|
30
|
+
if (reasons.length)
|
|
31
|
+
throw new Error(reasons[0]);
|
|
32
|
+
return signVcWith({ "@context": [VC_CONTEXT], type: ["VerifiableCredential", "ZaniiCertifyCertificateV3"], issuer: current,
|
|
33
|
+
validFrom: cert.issued_at, validUntil: cert.exp, credentialSubject: { id: cert.holder, certificate: credentialHash(cert), credential: cert },
|
|
34
|
+
credentialStatus: [bitstringStatusEntry({ index: opts.index, list: opts.revocationList, purpose: "revocation" }), bitstringStatusEntry({ index: opts.index, list: opts.suspensionList, purpose: "suspension" })], issuerAuthorization: opts.issuerDoc }, signer, opts.created);
|
|
35
|
+
}
|
|
36
|
+
export function verifyCurrentIssuerCertificateVc(vc, opts) {
|
|
37
|
+
const denied = (reasons) => ({ ok: false, reasons, certificate: null });
|
|
38
|
+
if (!withinVerificationBudget({ vc, opts }, { maxBytes: 4_194_304 }) || !isObj(opts) || !did(opts.owner) || !Array.isArray(opts.issuers) || opts.issuers.length < 1 || opts.issuers.length > 64 || !Array.from(opts.issuers).every(did) || !isIso(opts.at))
|
|
39
|
+
return denied(["independent current issuer wrapper trust inputs are required"]);
|
|
40
|
+
if (!isObj(vc) || Object.keys(vc).length !== 9 || !Array.isArray(vc.type) || vc.type.length !== 2 || vc.type[0] !== "VerifiableCredential" || vc.type[1] !== "ZaniiCertifyCertificateV3" || !opts.issuers.includes(String(vc.issuer))
|
|
41
|
+
|| !Array.isArray(vc["@context"]) || vc["@context"].length !== 1 || vc["@context"][0] !== VC_CONTEXT || !isObj(vc.proof) || Object.keys(vc.proof).length !== 6
|
|
42
|
+
|| !isIso(vc.proof.created) || ms(vc.proof.created) > ms(opts.at) || typeof vc.proof.proofValue !== "string" || vc.proof.proofValue.length > 100)
|
|
43
|
+
return denied(["invalid current issuer wrapper v3 proof or pinned issuer"]);
|
|
44
|
+
if (!withinVerificationBudget(vc.issuerAuthorization, { maxBytes: 1_048_576 }))
|
|
45
|
+
return denied(["issuer authorization exceeds its JSON budget"]);
|
|
46
|
+
const checked = verifyVC(vc, { at: opts.at });
|
|
47
|
+
if (!checked.ok)
|
|
48
|
+
return denied(checked.reasons);
|
|
49
|
+
const subject = vc.credentialSubject;
|
|
50
|
+
if (!isObj(subject) || Object.keys(subject).length !== 3 || !isObj(subject.credential))
|
|
51
|
+
return denied(["current issuer wrapper does not carry the original certificate"]);
|
|
52
|
+
const cert = subject.credential, reasons = authorization(cert, String(vc.issuer), vc.issuerAuthorization, opts.owner, opts.domain, opts.at);
|
|
53
|
+
if (subject.id !== cert.holder || subject.certificate !== credentialHash(cert) || vc.validFrom !== cert.issued_at || vc.validUntil !== cert.exp)
|
|
54
|
+
reasons.push("current issuer wrapper changed the original certificate binding");
|
|
55
|
+
const entries = vc.credentialStatus;
|
|
56
|
+
if (!Array.isArray(entries) || entries.length !== 2 || !Array.from(entries).every(isBitstringStatusEntry) || entries[0].statusPurpose !== "revocation" || entries[1].statusPurpose !== "suspension"
|
|
57
|
+
|| entries[0].statusListIndex !== entries[1].statusListIndex || entries[0].statusListCredential === entries[1].statusListCredential)
|
|
58
|
+
reasons.push("current issuer wrapper has invalid bitstring status bindings");
|
|
59
|
+
return { ok: reasons.length === 0, reasons, certificate: reasons.length ? null : cert };
|
|
60
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type Identifier } from "@zanii/blackbox";
|
|
2
|
+
import { type Disclosure, type RedactEnvelope } from "@zanii/redact";
|
|
3
|
+
import type { EvidenceHead } from "./appliance.ts";
|
|
4
|
+
import { type Signer } from "./signer.ts";
|
|
5
|
+
/** Section 1: a Blackbox identifier, optionally redacting only its first capture group. */
|
|
6
|
+
export type RedactionKind = Identifier & {
|
|
7
|
+
group?: 1;
|
|
8
|
+
};
|
|
9
|
+
/** Section 1: the GCC formats Blackbox's core identifiers do not cover. */
|
|
10
|
+
export declare const GCC_IDENTIFIERS: readonly RedactionKind[];
|
|
11
|
+
/** Section 1: every identifier kind `redactEvidence` knows, sorted. */
|
|
12
|
+
export declare const REDACTION_KINDS: readonly string[];
|
|
13
|
+
export interface RedactOptions {
|
|
14
|
+
/** The lab's redaction key (at least 16 bytes): the token and salt keys come from it. */
|
|
15
|
+
key: Uint8Array | string;
|
|
16
|
+
/** The identifier kinds to replace; default every kind in REDACTION_KINDS. */
|
|
17
|
+
kinds?: readonly string[];
|
|
18
|
+
/** Configured secret values (endpoint credentials, canary values), replaced as kind `secret`. */
|
|
19
|
+
secrets?: readonly string[];
|
|
20
|
+
/** Section 1: the run's 32 random bytes: tokens and salts are then keyed by HKDF of the key under it (version 2). */
|
|
21
|
+
saltSeed?: Uint8Array;
|
|
22
|
+
/** Section 1: RFC 6901 pointers of entry fields whose every leaf is replaced, as kind `withheld`. */
|
|
23
|
+
withhold?: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Section 1: the entries with every identifier of the chosen kinds and every configured secret
|
|
27
|
+
* replaced by a keyed token, in every string value (the top-level `seq`, `prev` and `hash` are
|
|
28
|
+
* kept), every withheld field replaced whole, and how many of each kind were replaced.
|
|
29
|
+
*/
|
|
30
|
+
export declare function redactEvidence(entries: readonly Record<string, unknown>[], opts: RedactOptions): {
|
|
31
|
+
entries: Record<string, unknown>[];
|
|
32
|
+
redactions: Record<string, number>;
|
|
33
|
+
};
|
|
34
|
+
export declare const AUDIENCES: readonly ["reviewer", "regulator", "applicant"];
|
|
35
|
+
export type Audience = (typeof AUDIENCES)[number];
|
|
36
|
+
export interface SharedEntry {
|
|
37
|
+
chain: string;
|
|
38
|
+
/** The redacted entry. */
|
|
39
|
+
entry: Record<string, unknown>;
|
|
40
|
+
/** @zanii/redact envelope over the original entry's fields (RFC 6901 pointers). */
|
|
41
|
+
envelope: RedactEnvelope;
|
|
42
|
+
/** One @zanii/redact disclosure per field the redaction left unchanged. */
|
|
43
|
+
disclosures: Disclosure[];
|
|
44
|
+
}
|
|
45
|
+
export interface EvidenceShare {
|
|
46
|
+
v: 1;
|
|
47
|
+
type: "certify.evidence-share";
|
|
48
|
+
audience: Audience;
|
|
49
|
+
heads: EvidenceHead[];
|
|
50
|
+
kinds: string[];
|
|
51
|
+
redactions: Record<string, number>;
|
|
52
|
+
items: SharedEntry[];
|
|
53
|
+
shared_at: string;
|
|
54
|
+
signer: string;
|
|
55
|
+
sig: string;
|
|
56
|
+
}
|
|
57
|
+
export interface EvidenceShareInput extends RedactOptions {
|
|
58
|
+
/** Whole evidence chains, entries from seq 0 (`{seq, prev, hash, ...}`, hash the jcsHash of the entry without it). */
|
|
59
|
+
chains: readonly {
|
|
60
|
+
chain: string;
|
|
61
|
+
entries: readonly Record<string, unknown>[];
|
|
62
|
+
}[];
|
|
63
|
+
audience: Audience;
|
|
64
|
+
sharedAt: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Section 4: the @zanii/redact disclosure of one field (`pointer`) of an original chain entry, as a
|
|
68
|
+
* share built with the same key and seed committed it: a field the share hid, opened for one
|
|
69
|
+
* receiver. Throws when the entry has no such field.
|
|
70
|
+
*/
|
|
71
|
+
export declare function discloseEvidenceField(entry: Record<string, unknown>, chain: string, pointer: string, opts: {
|
|
72
|
+
key: Uint8Array | string;
|
|
73
|
+
saltSeed?: Uint8Array;
|
|
74
|
+
}): Disclosure;
|
|
75
|
+
/** Section 2: redact whole evidence chains and sign them, with their heads and commitments, for one audience. */
|
|
76
|
+
export declare function buildEvidenceShare(f: EvidenceShareInput, signer: Signer): Promise<EvidenceShare>;
|
|
77
|
+
/** Internal detached-signing stage used by the server's bounded private broker worker. */
|
|
78
|
+
export declare function buildEvidenceShareDraft(f: EvidenceShareInput): Omit<EvidenceShare, "signer" | "sig">;
|
|
79
|
+
/**
|
|
80
|
+
* Section 3: `signers` are the lab DIDs this receiver trusts; `heads` the chain heads it holds from
|
|
81
|
+
* elsewhere (the result bundle's), each of which the share must carry unchanged.
|
|
82
|
+
*/
|
|
83
|
+
export declare function verifyEvidenceShare(doc: unknown, opts?: {
|
|
84
|
+
signers?: readonly string[];
|
|
85
|
+
heads?: readonly EvidenceHead[];
|
|
86
|
+
}): {
|
|
87
|
+
ok: boolean;
|
|
88
|
+
reasons: string[];
|
|
89
|
+
};
|