@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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// spec/certificate.md: a Certify certificate is a @zanii/credentials Credential with a certify claim.
|
|
2
|
+
import { credentialHash, verifyCredential, } from "@zanii/credentials";
|
|
3
|
+
import { jcsHash } from "@zanii/core";
|
|
4
|
+
import { bindingHash } from "@zanii/pq";
|
|
5
|
+
import { verifyAuditAttestation } from "./audit.js";
|
|
6
|
+
import { checkFingerprint } from "./fingerprint.js";
|
|
7
|
+
import { licenceCoverage, licenceReasons, verifyLicence } from "./licence.js";
|
|
8
|
+
import { checkPqBinding, pqCommitment, pqPolicyOf, verifyPqCosignature } from "./pq.js";
|
|
9
|
+
import { registerTrust } from "./register.js";
|
|
10
|
+
import { addDays, isDid, isIso, isObj, isSha, ms, SLUG, sortedUnique } from "./shared.js";
|
|
11
|
+
import { signatureOk, signatureOver, signerDid } from "./signer.js";
|
|
12
|
+
import { verifyStatus } from "./status.js";
|
|
13
|
+
import { afterCompromise, compromisedKeyReasons, issuerLineage, succeeds } from "./succession.js";
|
|
14
|
+
import { issuerPolicyCheck, releasePolicyReasons } from "./verification-policy.js";
|
|
15
|
+
import { verifyAuditRenewal } from "./audit-renewal.js";
|
|
16
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
17
|
+
import { signingIdentityReasons } from "./signing-identity.js";
|
|
18
|
+
/** scheme.md section 8: every scheme version a certificate or bundle may name. */
|
|
19
|
+
export const SCHEMES = ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3"];
|
|
20
|
+
/** The current scheme. */
|
|
21
|
+
export const SCHEME = "zanii-certify/0.3";
|
|
22
|
+
export const LEVELS = [1, 2, 3];
|
|
23
|
+
/** scheme.md section 2: the levels each scheme version issues. */
|
|
24
|
+
export const SCHEME_LEVELS = {
|
|
25
|
+
"zanii-certify/0.1": [1, 2],
|
|
26
|
+
"zanii-certify/0.2": [1, 2, 3],
|
|
27
|
+
"zanii-certify/0.3": [1, 2, 3],
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* scheme.md section 8: the scheme a certificate or bundle is issued under. Given, it must issue
|
|
31
|
+
* `level` (and allow 0.2 features when `needs02`, 0.3 features when `needs03`); absent, 0.3 with a
|
|
32
|
+
* 0.3 feature (a licence), else 0.1 for Levels 1 and 2 without 0.2 features, else 0.2.
|
|
33
|
+
*/
|
|
34
|
+
export function pickScheme(scheme, level, needs02 = false, needs03 = false) {
|
|
35
|
+
const s = scheme ?? (needs03 ? "zanii-certify/0.3" : level === 3 || needs02 ? "zanii-certify/0.2" : "zanii-certify/0.1");
|
|
36
|
+
if (!SCHEMES.includes(s))
|
|
37
|
+
throw new Error(`scheme must be one of ${SCHEMES.join(", ")}`);
|
|
38
|
+
if (!SCHEME_LEVELS[s].includes(level))
|
|
39
|
+
throw new Error(`level ${level} is not issued under ${s}`);
|
|
40
|
+
if (needs02 && s === "zanii-certify/0.1")
|
|
41
|
+
throw new Error("a post-quantum binding needs scheme zanii-certify/0.2");
|
|
42
|
+
if (needs03 && s !== "zanii-certify/0.3")
|
|
43
|
+
throw new Error("a licence needs scheme zanii-certify/0.3");
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
export const CONFLICTS = [
|
|
47
|
+
"blackbox_customer",
|
|
48
|
+
"ledger_customer",
|
|
49
|
+
"investor",
|
|
50
|
+
"staff_link",
|
|
51
|
+
"other",
|
|
52
|
+
];
|
|
53
|
+
/** scheme.md section 5: the default re-test interval per level. */
|
|
54
|
+
export const SURVEILLANCE = { 1: "P30D", 2: "P90D", 3: "P90D" };
|
|
55
|
+
/** scheme.md section 4: validity is 365 days from issue. */
|
|
56
|
+
export const VALIDITY_DAYS = 365;
|
|
57
|
+
export const DOMAIN = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
58
|
+
const COUNTRY = /^[A-Z]{2}$/;
|
|
59
|
+
const DURATION = /^P[1-9][0-9]{0,3}D$/;
|
|
60
|
+
/** `certify.<profile slug>.l<level>` */
|
|
61
|
+
export const certificateScope = (slug, level) => `certify.${slug}.l${level}`;
|
|
62
|
+
/** Issue a certificate, signed by `signer` (the lab issuer key, in a KMS or HSM). */
|
|
63
|
+
export async function issueCertificate(input, signer) {
|
|
64
|
+
const { profile, level, fingerprint, applicant } = input;
|
|
65
|
+
const domain = String(input.issuerDomain ?? "").toLowerCase();
|
|
66
|
+
if (!DOMAIN.test(domain))
|
|
67
|
+
throw new Error("issuerDomain must be a DNS name");
|
|
68
|
+
if (!isDid(input.holder))
|
|
69
|
+
throw new Error("holder must be a did:key");
|
|
70
|
+
if (!SLUG.test(profile?.slug ?? "") || !Number.isInteger(profile.version) || profile.version < 1)
|
|
71
|
+
throw new Error("profile needs a slug and a version >= 1");
|
|
72
|
+
if (!LEVELS.includes(level))
|
|
73
|
+
throw new Error("level must be 1, 2 or 3");
|
|
74
|
+
const scheme = pickScheme(input.scheme, level, input.pqBinding !== undefined, input.licence !== undefined);
|
|
75
|
+
if (!checkFingerprint(fingerprint))
|
|
76
|
+
throw new Error("fingerprint is not a valid fingerprint");
|
|
77
|
+
if (!isSha(input.bundle))
|
|
78
|
+
throw new Error("bundle must be a result bundle hash");
|
|
79
|
+
if (!input.conflicts.every((c) => CONFLICTS.includes(c)))
|
|
80
|
+
throw new Error("unknown conflict");
|
|
81
|
+
const name = typeof applicant?.name === "string" ? applicant.name : "";
|
|
82
|
+
if (!name || name.length > 200 || !COUNTRY.test(applicant.country ?? ""))
|
|
83
|
+
throw new Error("applicant needs a name (1-200) and an ISO 3166-1 alpha-2 country");
|
|
84
|
+
if (!isIso(input.issuedAt))
|
|
85
|
+
throw new Error("issuedAt must be an ISO UTC timestamp");
|
|
86
|
+
const exp = input.exp ?? addDays(input.issuedAt, VALIDITY_DAYS);
|
|
87
|
+
if (!isIso(exp) || ms(exp) <= ms(input.issuedAt))
|
|
88
|
+
throw new Error("exp must be after issuedAt");
|
|
89
|
+
const surveillance = input.surveillance ?? SURVEILLANCE[level];
|
|
90
|
+
if (!DURATION.test(surveillance))
|
|
91
|
+
throw new Error("surveillance must be PnD");
|
|
92
|
+
const issuer = signerDid(signer);
|
|
93
|
+
if (input.pqBinding !== undefined)
|
|
94
|
+
checkPqBinding(input.pqBinding, issuer);
|
|
95
|
+
if ((level === 3) !== (input.audit !== undefined))
|
|
96
|
+
throw new Error("an audit attestation is required at Level 3 and only there");
|
|
97
|
+
if (input.audit) {
|
|
98
|
+
const a = input.audit;
|
|
99
|
+
// Whether the body is trusted is the issuer's decision before issuing; here: signature,
|
|
100
|
+
// fields, validity at issue, and that it is for this agent and applicant.
|
|
101
|
+
const check = verifyAuditAttestation(a, { trustedBodies: [a.body], now: input.issuedAt });
|
|
102
|
+
if (!check.ok)
|
|
103
|
+
throw new Error(check.reasons[0]);
|
|
104
|
+
if (a.holder !== input.holder || a.applicant.name !== name || a.applicant.country !== applicant.country)
|
|
105
|
+
throw new Error("audit attestation is for a different agent or applicant");
|
|
106
|
+
if (ms(a.audited_at) > ms(input.issuedAt))
|
|
107
|
+
throw new Error("audit is dated after issuedAt");
|
|
108
|
+
}
|
|
109
|
+
if (input.licence !== undefined) {
|
|
110
|
+
// Signature and fields (whether the owner is trusted is the verifier's), and that it covers this certificate.
|
|
111
|
+
const l = input.licence;
|
|
112
|
+
const check = verifyLicence(l, { owner: String(l?.signer) });
|
|
113
|
+
if (!check.ok)
|
|
114
|
+
throw new Error(check.reasons[0]);
|
|
115
|
+
const covered = licenceCoverage(l, {
|
|
116
|
+
issuer,
|
|
117
|
+
issued_at: input.issuedAt,
|
|
118
|
+
claim: { profile: `${profile.slug}/${profile.version}`, level, applicant: { name, country: applicant.country } },
|
|
119
|
+
}, input.lineage ?? []);
|
|
120
|
+
if (covered.length)
|
|
121
|
+
throw new Error(covered[0]);
|
|
122
|
+
}
|
|
123
|
+
const claim = {
|
|
124
|
+
scheme,
|
|
125
|
+
profile: `${profile.slug}/${profile.version}`,
|
|
126
|
+
level,
|
|
127
|
+
fingerprint: fingerprint.id,
|
|
128
|
+
configuration: fingerprint.configuration,
|
|
129
|
+
bundle: input.bundle,
|
|
130
|
+
surveillance,
|
|
131
|
+
conflicts: sortedUnique(input.conflicts),
|
|
132
|
+
applicant: { name, country: applicant.country },
|
|
133
|
+
...(input.audit ? { audit: jcsHash(input.audit) } : {}),
|
|
134
|
+
...(input.pqBinding ? { pq_binding: bindingHash(input.pqBinding) } : {}),
|
|
135
|
+
...(input.licence ? { licence: jcsHash(input.licence) } : {}),
|
|
136
|
+
};
|
|
137
|
+
// The exact unsigned shape @zanii/credentials buildCredential signs.
|
|
138
|
+
const unsigned = {
|
|
139
|
+
v: 1,
|
|
140
|
+
type: "credential",
|
|
141
|
+
issuer,
|
|
142
|
+
issuer_domain: domain,
|
|
143
|
+
holder: input.holder,
|
|
144
|
+
scope: certificateScope(profile.slug, level),
|
|
145
|
+
claim: claim,
|
|
146
|
+
issued_at: input.issuedAt,
|
|
147
|
+
exp,
|
|
148
|
+
};
|
|
149
|
+
return { ...unsigned, sig: await signatureOver(unsigned, signer) };
|
|
150
|
+
}
|
|
151
|
+
/** A @zanii/credentials revocation list in shape: v 1, type revocation_list, revoked a list of strings. */
|
|
152
|
+
const isRevocationList = (l) => isObj(l) && l.v === 1 && l.type === "revocation_list" && Array.isArray(l.revoked) && l.revoked.every((x) => typeof x === "string");
|
|
153
|
+
/** The certify claim's own rules (certificate.md section 2); empty when the claim is well formed. */
|
|
154
|
+
export function claimReasons(cert) {
|
|
155
|
+
const c = isObj(cert?.claim) ? cert.claim : {};
|
|
156
|
+
const out = [];
|
|
157
|
+
const scheme = SCHEMES.includes(c.scheme) ? c.scheme : null;
|
|
158
|
+
if (!scheme)
|
|
159
|
+
out.push(`claim scheme is not one of ${SCHEMES.join(", ")}`);
|
|
160
|
+
const [slug, version] = typeof c.profile === "string" ? c.profile.split("/") : [];
|
|
161
|
+
if (!slug || !SLUG.test(slug) || !/^[1-9][0-9]*$/.test(version ?? ""))
|
|
162
|
+
out.push("claim profile is not <slug>/<version>");
|
|
163
|
+
if (!LEVELS.includes(c.level) || (scheme && !SCHEME_LEVELS[scheme].includes(c.level)))
|
|
164
|
+
out.push("claim level is not a level of the claim scheme");
|
|
165
|
+
else if (slug && cert.scope !== certificateScope(slug, c.level))
|
|
166
|
+
out.push("scope does not match the claim profile and level");
|
|
167
|
+
if (!isSha(c.fingerprint) || !isSha(c.bundle))
|
|
168
|
+
out.push("claim fingerprint or bundle is not a hash");
|
|
169
|
+
if (c.level === 3 ? !isSha(c.audit) : "audit" in c)
|
|
170
|
+
out.push("claim audit is an audit attestation hash, required at level 3 and only there");
|
|
171
|
+
if ("pq_binding" in c && ((scheme !== "zanii-certify/0.2" && scheme !== "zanii-certify/0.3") || !isSha(c.pq_binding)))
|
|
172
|
+
out.push("claim pq_binding needs scheme zanii-certify/0.2 and a binding hash");
|
|
173
|
+
if ("licence" in c && (scheme !== "zanii-certify/0.3" || !isSha(c.licence)))
|
|
174
|
+
out.push("claim licence needs scheme zanii-certify/0.3 and a licence hash");
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
/** `verifyCredential`'s revocation check, for a list signed by another DID of the issuer document. */
|
|
178
|
+
function rotatedListCheck(credential, cert, list, now) {
|
|
179
|
+
const reasons = credential.reasons.filter((r) => !r.startsWith("no revocation list supplied"));
|
|
180
|
+
const { sig, ...unsigned } = list;
|
|
181
|
+
let revocationFreshness = null;
|
|
182
|
+
if (!signatureOk(unsigned, sig, list.issuer))
|
|
183
|
+
reasons.push("revocation list signature is invalid");
|
|
184
|
+
else {
|
|
185
|
+
revocationFreshness = Date.parse(list.valid_until) >= (now ? Date.parse(now) : Date.now()) ? "fresh" : "stale";
|
|
186
|
+
if (list.revoked.includes(credentialHash(cert)))
|
|
187
|
+
reasons.push("credential has been REVOKED by its issuer");
|
|
188
|
+
// the @zanii/credentials wording, its dash written as an escape
|
|
189
|
+
if (revocationFreshness === "stale")
|
|
190
|
+
reasons.push(`revocation list expired on ${list.valid_until} \u2014 revocation status is a stale snapshot`);
|
|
191
|
+
}
|
|
192
|
+
return { ...credential, ok: reasons.length === 0, reasons, revocationFreshness };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Verify a certificate: the four @zanii/credentials checks, the certify claim, its live status in
|
|
196
|
+
* the signed status document, and (when supplied) that the agent's current fingerprint matches.
|
|
197
|
+
* Anything not checked is reported, never assumed.
|
|
198
|
+
*/
|
|
199
|
+
export async function verifyCertificate(cert, opts = {}) {
|
|
200
|
+
if (opts.auditRenewal !== undefined) {
|
|
201
|
+
const { resolver: _, ...bounded } = opts;
|
|
202
|
+
if (!isObj(opts.auditRenewal) || !withinVerificationBudget({ cert, opts: bounded }, { maxBytes: 4_194_304 })) {
|
|
203
|
+
const reasons = ["audit renewal verification exceeds its JSON budget or has an invalid renewal policy"];
|
|
204
|
+
return { ok: false, reasons, status: null, fingerprint: null, credential: { ok: false, reasons, revocationFreshness: null }, pq: null, witnesses: null };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Section 5: after a key rotation the status document and revocation list are signed by the
|
|
208
|
+
// issuer's current DID, which the issuer document lists beside the certificate's (resolver).
|
|
209
|
+
// A retired key stays in `dids` (its certificates still verify) but signs no status: with
|
|
210
|
+
// statusDids, only a current status signer is accepted.
|
|
211
|
+
// Section 5a: with an issuer document that lists successions, a key the certificate's issuer
|
|
212
|
+
// key was succeeded by signs status without the resolver.
|
|
213
|
+
const lineage = opts.issuerDoc === undefined ? null : issuerLineage(opts.issuerDoc);
|
|
214
|
+
const issuerStatusDids = isObj(opts.issuerDoc) && "status_dids" in opts.issuerDoc
|
|
215
|
+
? Array.isArray(opts.issuerDoc.status_dids) ? opts.issuerDoc.status_dids.filter((x) => typeof x === "string") : []
|
|
216
|
+
: undefined;
|
|
217
|
+
const statusDids = opts.statusDids ?? issuerStatusDids;
|
|
218
|
+
const current = (did) => !statusDids || statusDids.includes(did);
|
|
219
|
+
const rotated = async (did) => (succeeds(lineage, cert?.issuer, did) && current(did)) ||
|
|
220
|
+
(typeof did === "string" && did !== cert?.issuer && !!statusDids?.includes(did) && !!opts.resolver &&
|
|
221
|
+
!!(await opts.resolver(did, String(cert?.issuer_domain ?? ""))));
|
|
222
|
+
const given = opts.revocationList;
|
|
223
|
+
const list = isRevocationList(given) ? given : undefined;
|
|
224
|
+
const otherList = list !== undefined && (await rotated(list.issuer));
|
|
225
|
+
let credential = await verifyCredential(cert, {
|
|
226
|
+
...(opts.resolver ? { resolver: opts.resolver } : {}),
|
|
227
|
+
...(list && !otherList ? { revocationList: list } : {}),
|
|
228
|
+
...(opts.now ? { now: opts.now } : {}),
|
|
229
|
+
});
|
|
230
|
+
if (list && otherList)
|
|
231
|
+
credential = rotatedListCheck(credential, cert, list, opts.now);
|
|
232
|
+
const listReasons = given === undefined ? [] : !list ? ["revocation list is not a revocation list (v1)"] : current(list.issuer) ? [] : ["revocation list is not signed by a current status key of the issuer"];
|
|
233
|
+
if (list)
|
|
234
|
+
listReasons.push(...compromisedKeyReasons(lineage, list.issuer, "revocation list"));
|
|
235
|
+
if (listReasons.length) {
|
|
236
|
+
const kept = credential.reasons.filter((r) => !r.startsWith("no revocation list supplied"));
|
|
237
|
+
credential = { ...credential, ok: false, reasons: [...kept, ...listReasons], ...(list ? {} : { revocationFreshness: null }) };
|
|
238
|
+
}
|
|
239
|
+
const reasons = [...credential.reasons];
|
|
240
|
+
const schemeOwner = opts.schemeOwner ?? opts.bodyRegister?.owner ?? opts.releases?.owner;
|
|
241
|
+
if (schemeOwner !== undefined)
|
|
242
|
+
reasons.push(...signingIdentityReasons(schemeOwner, [cert?.issuer], statusDids ?? []));
|
|
243
|
+
if (schemeOwner !== undefined && list)
|
|
244
|
+
reasons.push(...signingIdentityReasons(schemeOwner, [], [list.issuer]));
|
|
245
|
+
if (cert?.type === "credential")
|
|
246
|
+
reasons.push(...claimReasons(cert));
|
|
247
|
+
if (lineage)
|
|
248
|
+
reasons.push(...lineage.reasons, ...afterCompromise(lineage, cert?.issuer, cert?.issued_at, "certificate"));
|
|
249
|
+
const pqPolicy = opts.issuerDoc === undefined ? undefined : pqPolicyOf(opts.issuerDoc, opts.allowNoPq, String(cert?.issued_at));
|
|
250
|
+
const strict = opts.issuerPolicy === undefined ? null : issuerPolicyCheck(cert, opts.issuerDoc, opts.issuerPolicy, { allowNoPq: opts.allowNoPq, status: opts.status, revocationList: opts.revocationList });
|
|
251
|
+
if (strict)
|
|
252
|
+
reasons.push(...strict.reasons);
|
|
253
|
+
const requirePq = opts.requirePq === true || pqPolicy === "required" || strict?.requirePq === true;
|
|
254
|
+
let pq = null;
|
|
255
|
+
if (opts.pq) {
|
|
256
|
+
const r = verifyPqCosignature(cert, opts.pq);
|
|
257
|
+
pq = r.ok;
|
|
258
|
+
reasons.push(...r.reasons);
|
|
259
|
+
}
|
|
260
|
+
else if (requirePq) {
|
|
261
|
+
pq = false;
|
|
262
|
+
reasons.push(pqCommitment(cert)
|
|
263
|
+
? "no post-quantum co-signature supplied: a post-quantum signature is required"
|
|
264
|
+
: "certificate has no post-quantum co-signature: a post-quantum signature is required");
|
|
265
|
+
}
|
|
266
|
+
const claim = isObj(cert?.claim) ? cert.claim : {};
|
|
267
|
+
const bodies = claim.level !== 3 ? undefined : opts.bodyRegister ? "register" : opts.trustedBodies ? "local" : undefined;
|
|
268
|
+
if (claim.level === 3) {
|
|
269
|
+
if (!opts.audit)
|
|
270
|
+
reasons.push("level 3: no audit attestation supplied, the control audit was NOT checked");
|
|
271
|
+
else {
|
|
272
|
+
const renewal = opts.auditRenewal;
|
|
273
|
+
const renewalAt = isObj(renewal?.document) && isIso(renewal.document.issued_at) ? renewal.document.issued_at : "";
|
|
274
|
+
const reg = opts.bodyRegister ? registerTrust(renewal ? { ...cert, issued_at: renewalAt } : cert, opts.audit.body, opts.bodyRegister) : null;
|
|
275
|
+
if (reg)
|
|
276
|
+
reasons.push(...reg.reasons);
|
|
277
|
+
const trustedBodies = reg ? reg.trusted : opts.trustedBodies;
|
|
278
|
+
if (renewal) {
|
|
279
|
+
const originalReg = opts.bodyRegister ? registerTrust(cert, isObj(renewal.originalAudit) ? renewal.originalAudit.body : null, opts.bodyRegister) : null;
|
|
280
|
+
if (originalReg)
|
|
281
|
+
reasons.push(...originalReg.reasons);
|
|
282
|
+
reasons.push(...verifyAuditRenewal(renewal.document, {
|
|
283
|
+
...renewal, certificate: cert, audit: opts.audit, trustedBodies: trustedBodies ?? [],
|
|
284
|
+
originalTrustedBodies: originalReg ? originalReg.trusted : renewal.originalTrustedBodies ?? opts.trustedBodies ?? [], now: opts.now ?? new Date().toISOString(),
|
|
285
|
+
}).reasons);
|
|
286
|
+
}
|
|
287
|
+
else
|
|
288
|
+
reasons.push(...verifyAuditAttestation(opts.audit, {
|
|
289
|
+
...(trustedBodies ? { trustedBodies } : {}),
|
|
290
|
+
...(opts.now ? { now: opts.now } : {}),
|
|
291
|
+
certificate: cert,
|
|
292
|
+
}).reasons);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (opts.auditRenewal !== undefined && !(isObj(cert?.claim) && cert.claim.level === 3))
|
|
296
|
+
reasons.push("audit renewal is only valid for a Level 3 certificate");
|
|
297
|
+
// section 5a: a licence naming an earlier key of the issuer's verified lineage covers a later one
|
|
298
|
+
if (opts.licences)
|
|
299
|
+
reasons.push(...licenceReasons(cert, opts.licences.lineage === undefined && lineage?.ok ? { ...opts.licences, lineage: lineage.lineage } : opts.licences));
|
|
300
|
+
if (opts.releases !== undefined)
|
|
301
|
+
reasons.push(...releasePolicyReasons(cert, opts.releases));
|
|
302
|
+
let status = null;
|
|
303
|
+
let witnesses = null;
|
|
304
|
+
if (opts.status) {
|
|
305
|
+
const s = verifyStatus(opts.status, {
|
|
306
|
+
...(schemeOwner !== undefined ? { schemeOwner } : {}),
|
|
307
|
+
issuer: (await rotated(opts.status.issuer)) ? opts.status.issuer : cert?.issuer,
|
|
308
|
+
...(opts.now ? { now: opts.now } : {}),
|
|
309
|
+
...(opts.statusPq ? { pq: opts.statusPq } : {}),
|
|
310
|
+
...(requirePq ? { requirePq: true } : {}),
|
|
311
|
+
...(opts.statusWitness ? { witness: opts.statusWitness } : {}),
|
|
312
|
+
});
|
|
313
|
+
reasons.push(...s.reasons, ...compromisedKeyReasons(lineage, opts.status.issuer, "status document"));
|
|
314
|
+
if (!current(opts.status.issuer))
|
|
315
|
+
reasons.push("status document is not signed by a current status key of the issuer");
|
|
316
|
+
witnesses = s.witnesses ?? null;
|
|
317
|
+
status = (isObj(opts.status.entries) && opts.status.entries[credentialHash(cert)]) || null;
|
|
318
|
+
if (!status)
|
|
319
|
+
reasons.push("certificate is not listed in the status document");
|
|
320
|
+
else if (status.status !== "valid")
|
|
321
|
+
reasons.push(`certificate is ${status.status} (${status.reason}) since ${status.since}`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
reasons.push("no status document supplied: status was NOT checked");
|
|
325
|
+
}
|
|
326
|
+
let fingerprint = null;
|
|
327
|
+
if (opts.fingerprint) {
|
|
328
|
+
const claimed = isObj(cert?.claim) ? cert.claim.fingerprint : undefined;
|
|
329
|
+
fingerprint =
|
|
330
|
+
checkFingerprint(opts.fingerprint) && opts.fingerprint.id === claimed ? "match" : "mismatch";
|
|
331
|
+
if (fingerprint === "mismatch")
|
|
332
|
+
reasons.push("agent fingerprint does not match the certificate");
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
ok: reasons.length === 0,
|
|
336
|
+
reasons,
|
|
337
|
+
status,
|
|
338
|
+
fingerprint,
|
|
339
|
+
credential,
|
|
340
|
+
pq,
|
|
341
|
+
witnesses,
|
|
342
|
+
...(pqPolicy ? { pqPolicy } : {}),
|
|
343
|
+
...(bodies ? { bodies: bodies } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"en": {
|
|
3
|
+
"usage_v2": "certify CLI help v2\nusage: certify verify <cert.json> [--status <file>] [--issuer-doc <file>] [--fingerprint <file>] [--online <base url>] [--now <time>] [--lang en|ar] [--pq <file>|off]\n [--audit <file>] [--trusted-bodies <file>] [--body-register <file>] [--status-witness <file>] [--release-policy <file>] [--strict-policy <file>]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]\n--strict-policy pins issuer keys and PQ requirements independently. --pq off is incompatible with it. See spec/cli.md for policy file schemas. Legacy help: --help; current help: --help-v2.",
|
|
4
|
+
"bad_policy": "invalid verification input for {0}; see certify --help-v2",
|
|
5
|
+
"issuer_resolution": "issuer resolution failed ({0}); this is not proof that the key is unpublished",
|
|
6
|
+
"audit_ok": "attestation and trusted body verified",
|
|
7
|
+
"audit_bad": "attestation or trusted body invalid",
|
|
8
|
+
"witness": "Status witnesses",
|
|
9
|
+
"issuer_policy": "Pinned issuer policy",
|
|
10
|
+
"release_policy": "Release policy",
|
|
11
|
+
"policy_ok": "verified",
|
|
12
|
+
"policy_bad": "missing or invalid evidence",
|
|
13
|
+
"usage": "usage: certify verify <cert.json> [--status <status.json>] [--issuer-doc <file>] [--fingerprint <fp.json>] [--online <base url>] [--now <time>] [--lang en|ar]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]",
|
|
14
|
+
"error": "error: {0}",
|
|
15
|
+
"unknown_option": "unknown option {0}",
|
|
16
|
+
"missing_value": "{0} needs a value",
|
|
17
|
+
"bad_lang": "--lang must be en or ar",
|
|
18
|
+
"bad_now": "--now must be an ISO UTC timestamp",
|
|
19
|
+
"cannot_read": "cannot read {0}",
|
|
20
|
+
"not_json": "{0} is not JSON",
|
|
21
|
+
"not_status": "{0} holds neither a status document nor a {status, revocation_list} pair",
|
|
22
|
+
"not_tools": "{0} holds no list of tool definitions",
|
|
23
|
+
"not_text": "{0} is not UTF-8 text",
|
|
24
|
+
"online_failed": "could not fetch {0}",
|
|
25
|
+
"not_certificate": "Not a Zanii Certify certificate",
|
|
26
|
+
"certificate": "Certificate: {0}",
|
|
27
|
+
"holder": "Holder: {0}",
|
|
28
|
+
"profile": "Profile: {0}, level {1}",
|
|
29
|
+
"issuer": "Issuer: {0} ({1})",
|
|
30
|
+
"expires": "Expires: {0}",
|
|
31
|
+
"signature": "Signature",
|
|
32
|
+
"expiry": "Expiry",
|
|
33
|
+
"claim": "Certify claim",
|
|
34
|
+
"status": "Status",
|
|
35
|
+
"revocation": "Revocation list",
|
|
36
|
+
"issuer_domain": "Issuer domain",
|
|
37
|
+
"fingerprint": "Fingerprint",
|
|
38
|
+
"audit": "Control audit (level 3)",
|
|
39
|
+
"sig_ok": "valid",
|
|
40
|
+
"sig_bad": "invalid",
|
|
41
|
+
"exp_ok": "valid until {0}",
|
|
42
|
+
"exp_bad": "expired on {0}",
|
|
43
|
+
"claim_ok": "well formed",
|
|
44
|
+
"claim_bad": "malformed",
|
|
45
|
+
"status_valid": "valid since {0}",
|
|
46
|
+
"status_suspended": "suspended ({0}) since {1}",
|
|
47
|
+
"status_revoked": "revoked ({0}) since {1}",
|
|
48
|
+
"status_unlisted": "the certificate is not listed",
|
|
49
|
+
"status_stale": "the status document expired on {0}",
|
|
50
|
+
"status_invalid": "the status document is not a valid document of this issuer",
|
|
51
|
+
"rev_ok": "not revoked",
|
|
52
|
+
"rev_revoked": "revoked",
|
|
53
|
+
"rev_stale": "the list expired on {0}",
|
|
54
|
+
"rev_invalid": "the list is not a valid list of this issuer",
|
|
55
|
+
"issuer_ok": "{0} publishes the issuer key",
|
|
56
|
+
"issuer_bad": "{0} does not publish the issuer key",
|
|
57
|
+
"issuer_skip": "not checked (use --issuer-doc or --online)",
|
|
58
|
+
"fp_ok": "matches the certificate",
|
|
59
|
+
"fp_bad": "does not match the certificate",
|
|
60
|
+
"not_supplied": "not supplied",
|
|
61
|
+
"audit_skip": "not checked by this command (verifyCertificate with the audit attestation and trusted bodies)",
|
|
62
|
+
"vc": "VC proof",
|
|
63
|
+
"vc_ok": "valid (eddsa-jcs-2022), carries the certificate",
|
|
64
|
+
"vc_bad": "Not a valid Zanii Certify verifiable credential",
|
|
65
|
+
"not_pq": "{0} holds no post-quantum co-signature",
|
|
66
|
+
"pq": "Post-quantum signature",
|
|
67
|
+
"pq_ok": "valid (ML-DSA-65)",
|
|
68
|
+
"pq_bad": "invalid",
|
|
69
|
+
"pq_missing": "required by the issuer document, not supplied (use --pq)",
|
|
70
|
+
"pq_optout": "not checked: the issuer document requires it, opted out with --pq off",
|
|
71
|
+
"result_valid": "Result: VALID",
|
|
72
|
+
"result_invalid": "Result: INVALID",
|
|
73
|
+
"result_partial": "Result: NOT FULLY CHECKED"
|
|
74
|
+
},
|
|
75
|
+
"ar": {
|
|
76
|
+
"usage_v2": "مساعدة certify، الإصدار 2\nusage: certify verify <cert.json> [--status <file>] [--issuer-doc <file>] [--fingerprint <file>] [--online <base url>] [--now <time>] [--lang en|ar] [--pq <file>|off]\n [--audit <file>] [--trusted-bodies <file>] [--body-register <file>] [--status-witness <file>] [--release-policy <file>] [--strict-policy <file>]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]\nيثبت --strict-policy مفاتيح الجهة المصدرة ومتطلبات التوقيع المقاوم للحوسبة الكمية بصورة مستقلة، ولا يقبل --pq off. صيغ الملفات في spec/cli.md. المساعدة السابقة: --help؛ المساعدة الحالية: --help-v2.",
|
|
77
|
+
"bad_policy": "مدخل تحقق غير صالح للخيار {0}؛ راجع certify --help-v2",
|
|
78
|
+
"issuer_resolution": "تعذر التحقق من نطاق الجهة المصدرة ({0})؛ لا يثبت ذلك أن المفتاح غير منشور",
|
|
79
|
+
"audit_ok": "تم التحقق من شهادة التدقيق والجهة الموثوقة",
|
|
80
|
+
"audit_bad": "شهادة التدقيق أو الجهة الموثوقة غير صالحة",
|
|
81
|
+
"witness": "شهود الحالة",
|
|
82
|
+
"issuer_policy": "سياسة الجهة المصدرة المثبتة",
|
|
83
|
+
"release_policy": "سياسة الإصدار",
|
|
84
|
+
"policy_ok": "تم التحقق",
|
|
85
|
+
"policy_bad": "الأدلة غير متاحة أو غير صالحة",
|
|
86
|
+
"usage": "الاستخدام: certify verify <cert.json> [--status <status.json>] [--issuer-doc <file>] [--fingerprint <fp.json>] [--online <base url>] [--now <time>] [--lang en|ar]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]",
|
|
87
|
+
"error": "خطأ: {0}",
|
|
88
|
+
"unknown_option": "خيار غير معروف {0}",
|
|
89
|
+
"missing_value": "الخيار {0} يحتاج إلى قيمة",
|
|
90
|
+
"bad_lang": "قيمة --lang يجب أن تكون en أو ar",
|
|
91
|
+
"bad_now": "قيمة --now يجب أن تكون طابعا زمنيا ISO بتوقيت UTC",
|
|
92
|
+
"cannot_read": "تعذرت قراءة {0}",
|
|
93
|
+
"not_json": "الملف {0} ليس JSON",
|
|
94
|
+
"not_status": "الملف {0} لا يحتوي وثيقة حالة ولا زوج {status, revocation_list}",
|
|
95
|
+
"not_tools": "الملف {0} لا يحتوي قائمة تعريفات أدوات",
|
|
96
|
+
"not_text": "الملف {0} ليس نصا بترميز UTF-8",
|
|
97
|
+
"online_failed": "تعذر جلب {0}",
|
|
98
|
+
"not_certificate": "ليست شهادة من Zanii Certify",
|
|
99
|
+
"certificate": "الشهادة: {0}",
|
|
100
|
+
"holder": "الحامل: {0}",
|
|
101
|
+
"profile": "الملف القطاعي: {0}، المستوى {1}",
|
|
102
|
+
"issuer": "الجهة المصدرة: {0} ({1})",
|
|
103
|
+
"expires": "تنتهي في: {0}",
|
|
104
|
+
"signature": "التوقيع",
|
|
105
|
+
"expiry": "الصلاحية",
|
|
106
|
+
"claim": "بيانات الاعتماد",
|
|
107
|
+
"status": "الحالة",
|
|
108
|
+
"revocation": "قائمة الإلغاء",
|
|
109
|
+
"issuer_domain": "نطاق الجهة المصدرة",
|
|
110
|
+
"fingerprint": "البصمة",
|
|
111
|
+
"audit": "تدقيق الضوابط (المستوى 3)",
|
|
112
|
+
"sig_ok": "صالح",
|
|
113
|
+
"sig_bad": "غير صالح",
|
|
114
|
+
"exp_ok": "صالحة حتى {0}",
|
|
115
|
+
"exp_bad": "انتهت في {0}",
|
|
116
|
+
"claim_ok": "سليمة",
|
|
117
|
+
"claim_bad": "غير سليمة",
|
|
118
|
+
"status_valid": "سارية منذ {0}",
|
|
119
|
+
"status_suspended": "معلقة ({0}) منذ {1}",
|
|
120
|
+
"status_revoked": "ملغاة ({0}) منذ {1}",
|
|
121
|
+
"status_unlisted": "الشهادة غير مدرجة",
|
|
122
|
+
"status_stale": "انتهت وثيقة الحالة في {0}",
|
|
123
|
+
"status_invalid": "وثيقة الحالة ليست وثيقة صالحة لهذه الجهة المصدرة",
|
|
124
|
+
"rev_ok": "غير ملغاة",
|
|
125
|
+
"rev_revoked": "ملغاة",
|
|
126
|
+
"rev_stale": "انتهت القائمة في {0}",
|
|
127
|
+
"rev_invalid": "القائمة ليست قائمة صالحة لهذه الجهة المصدرة",
|
|
128
|
+
"issuer_ok": "النطاق {0} ينشر مفتاح الجهة المصدرة",
|
|
129
|
+
"issuer_bad": "النطاق {0} لا ينشر مفتاح الجهة المصدرة",
|
|
130
|
+
"issuer_skip": "لم يفحص (استخدم --issuer-doc أو --online)",
|
|
131
|
+
"fp_ok": "تطابق الشهادة",
|
|
132
|
+
"fp_bad": "لا تطابق الشهادة",
|
|
133
|
+
"not_supplied": "لم تقدم",
|
|
134
|
+
"audit_skip": "لا يتحقق منه هذا الأمر (verifyCertificate مع شهادة التدقيق والجهات الموثوقة)",
|
|
135
|
+
"vc": "إثبات الشهادة القابلة للتحقق",
|
|
136
|
+
"vc_ok": "صالح (eddsa-jcs-2022)، ويحمل الشهادة",
|
|
137
|
+
"vc_bad": "ليست شهادة Zanii Certify قابلة للتحقق صالحة",
|
|
138
|
+
"not_pq": "الملف {0} لا يحتوي توقيعا مشتركا مقاوما للحوسبة الكمية",
|
|
139
|
+
"pq": "التوقيع المقاوم للحوسبة الكمية",
|
|
140
|
+
"pq_ok": "صالح (ML-DSA-65)",
|
|
141
|
+
"pq_bad": "غير صالح",
|
|
142
|
+
"pq_missing": "تشترطه وثيقة الجهة المصدرة ولم يقدم (استخدم --pq)",
|
|
143
|
+
"pq_optout": "لم يتحقق منه: تشترطه وثيقة الجهة المصدرة، وتم التنازل عنه بالخيار --pq off",
|
|
144
|
+
"result_valid": "النتيجة: صالحة",
|
|
145
|
+
"result_invalid": "النتيجة: غير صالحة",
|
|
146
|
+
"result_partial": "النتيجة: لم يكتمل الفحص"
|
|
147
|
+
}
|
|
148
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** spec/cli.md section 4. */
|
|
3
|
+
export declare const EXIT: {
|
|
4
|
+
readonly valid: 0;
|
|
5
|
+
readonly invalid: 1;
|
|
6
|
+
readonly partial: 2;
|
|
7
|
+
readonly usage: 3;
|
|
8
|
+
readonly online: 4;
|
|
9
|
+
};
|
|
10
|
+
export interface CliIo {
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
out?: (text: string) => void;
|
|
13
|
+
err?: (text: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/** Run `certify` with `argv` (without the program name); returns the exit code. */
|
|
16
|
+
export declare function main(argv: readonly string[], io?: CliIo): Promise<number>;
|