@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/licence.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// spec/licensing.md: scheme licences signed by the scheme owner, the owner's signed licence status
|
|
2
|
+
// log (suspension, reinstatement, withdrawal), the licence check behind `verifyCertificate`'s
|
|
3
|
+
// `licences` option, and royalty statements signed by the licensee and countersigned by the owner.
|
|
4
|
+
import { jcsHash, verifySignedObject } from "@zanii/core";
|
|
5
|
+
import { bodySignature, signersOf, withSignatures } from "./board.js";
|
|
6
|
+
import { isDid, isInt, isIso, isObj, isSha, ms, SLUG, sortedUnique } from "./shared.js";
|
|
7
|
+
import { signerDid, signWith } from "./signer.js";
|
|
8
|
+
import { logPositionOk } from "./status.js";
|
|
9
|
+
import { verifyLicenceOwnerHistory, verifyLicenceWithOwnerHistory } from "./licence-owner.js";
|
|
10
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
11
|
+
const COUNTRY = /^[A-Z]{2}$/;
|
|
12
|
+
const CURRENCY = /^[A-Z]{3}$/;
|
|
13
|
+
const PROFILE_REF = new RegExp(`^${SLUG.source.slice(1, -1)}/[1-9][0-9]*$`);
|
|
14
|
+
/** Section 1: the largest royalty per certificate, and section 4 the most certificates in one statement (their product stays a safe integer). */
|
|
15
|
+
export const MAX_ROYALTY_MINOR = 1_000_000_000;
|
|
16
|
+
export const MAX_STATEMENT_CERTIFICATES = 1_000_000;
|
|
17
|
+
const sortedDistinct = (xs) => sortedUnique(xs).join("\n") === xs.join("\n");
|
|
18
|
+
/** A scope list (licences, the body register): 1 to 256 `{profile, levels}`, sorted by profile, each once. */
|
|
19
|
+
export function scopeError(s) {
|
|
20
|
+
if (!Array.isArray(s) || s.length < 1 || s.length > 256)
|
|
21
|
+
return "scope must list 1 to 256 profiles";
|
|
22
|
+
for (const e of s) {
|
|
23
|
+
if (!isObj(e) || Object.keys(e).length !== 2 || typeof e.profile !== "string" || !PROFILE_REF.test(e.profile))
|
|
24
|
+
return "each scope entry is {profile: <slug>/<version>, levels}";
|
|
25
|
+
const lv = e.levels;
|
|
26
|
+
if (!Array.isArray(lv) || lv.length < 1 || !lv.every((x) => isInt(x, 1, 3)) || lv.some((x, i) => i > 0 && !(x > lv[i - 1])))
|
|
27
|
+
return "scope levels must be sorted, distinct levels from 1 to 3";
|
|
28
|
+
}
|
|
29
|
+
if (!sortedDistinct(s.map((e) => e.profile)))
|
|
30
|
+
return "scope must be sorted by profile, each profile once";
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** Sorted by profile, each profile's levels sorted and distinct (the shape `scopeError` accepts). */
|
|
34
|
+
export const normaliseScope = (scope) => Array.isArray(scope)
|
|
35
|
+
? [...scope]
|
|
36
|
+
.map((e) => ({ profile: e?.profile, levels: Array.isArray(e?.levels) ? [...new Set(e.levels)].sort((a, b) => a - b) : e?.levels }))
|
|
37
|
+
.sort((a, b) => (a.profile < b.profile ? -1 : a.profile > b.profile ? 1 : 0))
|
|
38
|
+
: scope;
|
|
39
|
+
function licenceError(l) {
|
|
40
|
+
if (!isDid(l.licensee))
|
|
41
|
+
return "licensee must be a did:key";
|
|
42
|
+
if (typeof l.legal_name !== "string" || l.legal_name.length < 1 || l.legal_name.length > 200)
|
|
43
|
+
return "legal_name must be 1 to 200 characters";
|
|
44
|
+
const c = l.countries;
|
|
45
|
+
if (!Array.isArray(c) || c.length < 1 || c.length > 250 || !c.every((x) => typeof x === "string" && COUNTRY.test(x)) || !sortedDistinct(c))
|
|
46
|
+
return "countries must be 1 to 250 sorted, distinct ISO 3166-1 alpha-2 codes";
|
|
47
|
+
const se = scopeError(l.scope);
|
|
48
|
+
if (se)
|
|
49
|
+
return se;
|
|
50
|
+
if (!isIso(l.valid_from) || !isIso(l.valid_until) || !(ms(l.valid_until) > ms(l.valid_from)))
|
|
51
|
+
return "valid_from and valid_until must be ISO timestamps, valid_until after valid_from";
|
|
52
|
+
const r = isObj(l.royalty) ? l.royalty : {};
|
|
53
|
+
if (!isObj(l.royalty) || Object.keys(r).length !== 2 || !isInt(r.per_certificate_minor, 0, MAX_ROYALTY_MINOR) || typeof r.currency !== "string" || !CURRENCY.test(r.currency))
|
|
54
|
+
return `royalty must be {per_certificate_minor: an integer from 0 to ${MAX_ROYALTY_MINOR}, currency: ISO 4217}`;
|
|
55
|
+
if (!isIso(l.issued_at))
|
|
56
|
+
return "issued_at must be an ISO timestamp";
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Section 1: a licence, signed by the scheme owner (`signer`). */
|
|
60
|
+
export async function buildLicence(f, signer) {
|
|
61
|
+
const scope = normaliseScope(f.scope);
|
|
62
|
+
const unsigned = {
|
|
63
|
+
v: 1,
|
|
64
|
+
type: "certify.licence",
|
|
65
|
+
licensee: f.licensee,
|
|
66
|
+
legal_name: f.legalName,
|
|
67
|
+
countries: Array.isArray(f.countries) ? sortedUnique(f.countries) : f.countries,
|
|
68
|
+
scope,
|
|
69
|
+
valid_from: f.validFrom,
|
|
70
|
+
valid_until: f.validUntil,
|
|
71
|
+
royalty: { per_certificate_minor: f.royalty?.per_certificate_minor, currency: f.royalty?.currency },
|
|
72
|
+
issued_at: f.issuedAt,
|
|
73
|
+
};
|
|
74
|
+
const err = licenceError(unsigned);
|
|
75
|
+
if (err)
|
|
76
|
+
throw new Error(err);
|
|
77
|
+
return signWith(unsigned, signer);
|
|
78
|
+
}
|
|
79
|
+
const isLicence = (l) => isObj(l) && l.v === 1 && l.type === "certify.licence" && Object.keys(l).length === 12;
|
|
80
|
+
/** Section 1: signed, well formed and (with `owner`) signed by the scheme owner. */
|
|
81
|
+
export function verifyLicence(doc, opts = {}) {
|
|
82
|
+
if (!isLicence(doc))
|
|
83
|
+
return { ok: false, reasons: ["not a licence (v1)"] };
|
|
84
|
+
const reasons = [];
|
|
85
|
+
if (!verifySignedObject(doc))
|
|
86
|
+
reasons.push("bad licence signature");
|
|
87
|
+
const err = licenceError(doc);
|
|
88
|
+
if (err)
|
|
89
|
+
reasons.push(`licence is malformed: ${err}`);
|
|
90
|
+
if (opts.owner === undefined)
|
|
91
|
+
reasons.push("no trusted scheme owner supplied: the licence signer was NOT checked");
|
|
92
|
+
else if (doc.signer !== opts.owner)
|
|
93
|
+
reasons.push("licence is not signed by the scheme owner");
|
|
94
|
+
return { ok: reasons.length === 0, reasons };
|
|
95
|
+
}
|
|
96
|
+
/** Section 3: is `issuer` the licensee, or a key the licensee's key was succeeded by in `lineage` (oldest first)? */
|
|
97
|
+
export const licensedKey = (licensee, issuer, lineage = []) => licensee === issuer || (typeof licensee === "string" && typeof issuer === "string" && lineage.indexOf(licensee) >= 0 && lineage.indexOf(issuer) > lineage.indexOf(licensee));
|
|
98
|
+
/**
|
|
99
|
+
* Section 3: what a licence must cover for a certificate: its issuer (the licensee, or a later key
|
|
100
|
+
* of the licensee's verified `lineage`), profile, level, country and issue time.
|
|
101
|
+
*/
|
|
102
|
+
export function licenceCoverage(licence, cert, lineage = []) {
|
|
103
|
+
const claim = isObj(cert?.claim) ? cert.claim : {};
|
|
104
|
+
const applicant = isObj(claim.applicant) ? claim.applicant : {};
|
|
105
|
+
const reasons = [];
|
|
106
|
+
if (!licensedKey(licence.licensee, cert?.issuer, lineage))
|
|
107
|
+
reasons.push("licence is for another licensee than the certificate issuer");
|
|
108
|
+
const scope = (Array.isArray(licence.scope) ? licence.scope : []).find((e) => isObj(e) && e.profile === claim.profile);
|
|
109
|
+
if (!scope || !Array.isArray(scope.levels) || !scope.levels.includes(claim.level))
|
|
110
|
+
reasons.push(`licence does not cover profile ${String(claim.profile)} at level ${String(claim.level)}`);
|
|
111
|
+
if (!Array.isArray(licence.countries) || !licence.countries.includes(applicant.country))
|
|
112
|
+
reasons.push(`licence does not cover country ${String(applicant.country)}`);
|
|
113
|
+
const at = ms(cert?.issued_at);
|
|
114
|
+
if (!(ms(licence.valid_from) <= at && at <= ms(licence.valid_until)))
|
|
115
|
+
reasons.push("certificate was issued outside the licence's validity");
|
|
116
|
+
return reasons;
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------- 2. licence status log
|
|
119
|
+
export const LICENCE_ACTIONS = ["suspend", "reinstate", "withdraw"];
|
|
120
|
+
export const LICENCE_REASONS = [
|
|
121
|
+
"ROYALTY_UNPAID",
|
|
122
|
+
"SCOPE_BREACH",
|
|
123
|
+
"MARK_MISUSE",
|
|
124
|
+
"AUDIT_FINDING",
|
|
125
|
+
"ACCREDITATION_LOST",
|
|
126
|
+
"LICENSEE_REQUEST",
|
|
127
|
+
"OTHER",
|
|
128
|
+
];
|
|
129
|
+
function statusError(s) {
|
|
130
|
+
if (!logPositionOk(s.seq, s.prev))
|
|
131
|
+
return "seq and prev are not a log position";
|
|
132
|
+
if (!isSha(s.licence))
|
|
133
|
+
return "licence must be a licence hash";
|
|
134
|
+
if (!LICENCE_ACTIONS.includes(s.action))
|
|
135
|
+
return "action must be suspend, reinstate or withdraw";
|
|
136
|
+
if (s.action === "reinstate" ? s.reason !== null : !LICENCE_REASONS.includes(s.reason))
|
|
137
|
+
return s.action === "reinstate" ? "reinstate takes no reason" : `${String(s.action)} needs a reason code`;
|
|
138
|
+
if (!isIso(s.at))
|
|
139
|
+
return "at must be an ISO timestamp";
|
|
140
|
+
if ("for_cause" in s || "effective_at" in s) {
|
|
141
|
+
if (s.for_cause !== true || !isIso(s.effective_at))
|
|
142
|
+
return "for_cause is true with an ISO effective_at";
|
|
143
|
+
if (s.action === "reinstate")
|
|
144
|
+
return "reinstate is never for cause";
|
|
145
|
+
if (!(ms(s.effective_at) <= ms(s.at)))
|
|
146
|
+
return "effective_at must not be after at";
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
export { statusError as licenceStatusFieldError };
|
|
151
|
+
/**
|
|
152
|
+
* Section 2: one licence status change, signed by the scheme owner. With `effectiveAt` (section 2a)
|
|
153
|
+
* a suspension or withdrawal is for cause from that time.
|
|
154
|
+
*/
|
|
155
|
+
export async function buildLicenceStatus(f, signer) {
|
|
156
|
+
const unsigned = {
|
|
157
|
+
v: 1,
|
|
158
|
+
type: "certify.licence-status",
|
|
159
|
+
seq: f.seq,
|
|
160
|
+
prev: f.prev,
|
|
161
|
+
licence: f.licence,
|
|
162
|
+
action: f.action,
|
|
163
|
+
reason: f.reason,
|
|
164
|
+
at: f.at,
|
|
165
|
+
...(f.effectiveAt !== undefined ? { for_cause: true, effective_at: f.effectiveAt } : {}),
|
|
166
|
+
};
|
|
167
|
+
const err = statusError(unsigned);
|
|
168
|
+
if (err)
|
|
169
|
+
throw new Error(err);
|
|
170
|
+
return signWith(unsigned, signer);
|
|
171
|
+
}
|
|
172
|
+
const NEXT = {
|
|
173
|
+
suspend: { from: ["active"], to: "suspended" },
|
|
174
|
+
reinstate: { from: ["suspended"], to: "active" },
|
|
175
|
+
withdraw: { from: ["active", "suspended"], to: "withdrawn" },
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Section 2: one log, signed by one key (with `owner`, the scheme owner), at seq i naming entry
|
|
179
|
+
* i - 1, `at` never going back, and every change allowed from the licence's state (withdrawal is final).
|
|
180
|
+
*/
|
|
181
|
+
export function verifyLicenceLog(log, opts = {}) {
|
|
182
|
+
if (!Array.isArray(log) || log.length === 0)
|
|
183
|
+
return { ok: false, reasons: ["the licence status log is empty"] };
|
|
184
|
+
const reasons = [];
|
|
185
|
+
const state = new Map();
|
|
186
|
+
log.forEach((s, i) => {
|
|
187
|
+
const prev = i > 0 ? log[i - 1] : null;
|
|
188
|
+
if (!isObj(s) || s.v !== 1 || s.type !== "certify.licence-status" || Object.keys(s).length !== ("for_cause" in s || "effective_at" in s ? 12 : 10) || !verifySignedObject(s)) {
|
|
189
|
+
reasons.push(`entry ${i} is not a signed licence status entry`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const err = statusError(s);
|
|
193
|
+
if (err) {
|
|
194
|
+
reasons.push(`entry ${i} is malformed: ${err}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (s.seq !== i)
|
|
198
|
+
reasons.push(`entry ${i} is not at position ${i}`);
|
|
199
|
+
if (s.prev !== (i > 0 ? jcsHash(prev) : null))
|
|
200
|
+
reasons.push(`entry ${i} does not name the previous entry`);
|
|
201
|
+
if (i > 0 && isObj(log[0]) && s.signer !== log[0].signer)
|
|
202
|
+
reasons.push(`entry ${i} is signed by another key`);
|
|
203
|
+
if (i > 0 && isObj(prev) && !(ms(s.at) >= ms(prev.at)))
|
|
204
|
+
reasons.push(`entry ${i} is older than the previous entry`);
|
|
205
|
+
const now = state.get(s.licence) ?? "active";
|
|
206
|
+
const rule = NEXT[s.action];
|
|
207
|
+
if (!rule.from.includes(now))
|
|
208
|
+
reasons.push(`entry ${i} cannot ${String(s.action)} a licence that is ${now}`);
|
|
209
|
+
else
|
|
210
|
+
state.set(s.licence, rule.to);
|
|
211
|
+
});
|
|
212
|
+
const signer = isObj(log[0]) ? log[0].signer : undefined;
|
|
213
|
+
if (opts.owner === undefined)
|
|
214
|
+
reasons.push("no trusted scheme owner supplied: the licence status signer was NOT checked");
|
|
215
|
+
else if (signer !== opts.owner)
|
|
216
|
+
reasons.push("the licence status log is not signed by the scheme owner");
|
|
217
|
+
return { ok: reasons.length === 0, reasons };
|
|
218
|
+
}
|
|
219
|
+
/** Section 2: a licence's state at `at` from a verified log (changes at or before `at` apply). */
|
|
220
|
+
export function licenceStateAt(log, licence, at) {
|
|
221
|
+
let state = "active";
|
|
222
|
+
for (const s of log)
|
|
223
|
+
if (s.licence === licence && ms(s.at) <= ms(at))
|
|
224
|
+
state = NEXT[s.action].to;
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
227
|
+
/** Section 3: the reasons a certificate is not covered by a licence that was valid when it was issued. */
|
|
228
|
+
export function licenceReasons(cert, opts) {
|
|
229
|
+
if (opts.ownerHistory !== undefined && (!Array.isArray(opts.licences) || opts.licences.length > 50_000 || !withinVerificationBudget({ cert, licences: opts.licences, ownerHistory: opts.ownerHistory })))
|
|
230
|
+
return ["rotated-owner certificate evidence exceeds the JSON budget"];
|
|
231
|
+
const claim = isObj(cert?.claim) ? cert.claim : {};
|
|
232
|
+
if (!("licence" in claim))
|
|
233
|
+
return opts.ownIssuers?.includes(cert?.issuer) ? [] : ["certificate carries no licence"];
|
|
234
|
+
const licence = (Array.isArray(opts.licences) ? opts.licences : []).find((l) => isObj(l) && jcsHash(l) === claim.licence);
|
|
235
|
+
if (!isObj(licence))
|
|
236
|
+
return ["the certificate's licence was not supplied"];
|
|
237
|
+
const rotated = opts.ownerHistory === undefined ? null : verifyLicenceWithOwnerHistory(licence, { ...opts.ownerHistory, root: opts.owner });
|
|
238
|
+
const reasons = [...(rotated ?? verifyLicence(licence, { owner: opts.owner })).reasons, ...licenceCoverage(licence, cert, opts.lineage)];
|
|
239
|
+
if (opts.ownerHistory !== undefined) {
|
|
240
|
+
if (opts.log !== undefined)
|
|
241
|
+
reasons.push("choose ownerHistory or the legacy licence log, not both");
|
|
242
|
+
if (rotated?.ok) {
|
|
243
|
+
const log = opts.ownerHistory.log;
|
|
244
|
+
const state = licenceStateAt(log, claim.licence, String(cert.issued_at));
|
|
245
|
+
if (state !== "active")
|
|
246
|
+
reasons.push(`licence was ${state} when the certificate was issued`);
|
|
247
|
+
reasons.push(...forCauseReasons(cert, { ...opts, log }));
|
|
248
|
+
}
|
|
249
|
+
return reasons;
|
|
250
|
+
}
|
|
251
|
+
if (opts.log === undefined)
|
|
252
|
+
reasons.push("no licence status log supplied: licence suspension and withdrawal were NOT checked");
|
|
253
|
+
else {
|
|
254
|
+
const check = verifyLicenceLog(opts.log, { owner: opts.owner });
|
|
255
|
+
reasons.push(...check.reasons);
|
|
256
|
+
if (check.ok) {
|
|
257
|
+
const state = licenceStateAt(opts.log, claim.licence, String(cert.issued_at));
|
|
258
|
+
if (state !== "active")
|
|
259
|
+
reasons.push(`licence was ${state} when the certificate was issued`);
|
|
260
|
+
reasons.push(...forCauseReasons(cert, opts));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return reasons;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Section 2a: a for-cause entry on any licence of the certificate's issuer (the certificate's own
|
|
267
|
+
* licence and every supplied licence of the same licensee) refuses certificates issued from its
|
|
268
|
+
* `effective_at` until the licence is reinstated (for ever after a withdrawal). Needs a verified log.
|
|
269
|
+
*/
|
|
270
|
+
function forCauseReasons(cert, opts) {
|
|
271
|
+
const claim = isObj(cert?.claim) ? cert.claim : {};
|
|
272
|
+
const own = new Set([claim.licence]);
|
|
273
|
+
for (const l of Array.isArray(opts.licences) ? opts.licences : [])
|
|
274
|
+
if (isObj(l) && licensedKey(l.licensee, cert?.issuer, opts.lineage))
|
|
275
|
+
own.add(jcsHash(l));
|
|
276
|
+
const log = (opts.log ?? []);
|
|
277
|
+
const at = ms(cert?.issued_at);
|
|
278
|
+
const reasons = [];
|
|
279
|
+
log.forEach((e, i) => {
|
|
280
|
+
if (e.for_cause !== true || !own.has(e.licence))
|
|
281
|
+
return;
|
|
282
|
+
const back = log.slice(i + 1).find((x) => x.licence === e.licence && x.action === "reinstate");
|
|
283
|
+
if (ms(e.effective_at) <= at && (!back || at < ms(back.at)))
|
|
284
|
+
reasons.push(`licence was ${e.action === "withdraw" ? "withdrawn" : "suspended"} for cause from ${e.effective_at}, before the certificate was issued`);
|
|
285
|
+
});
|
|
286
|
+
return reasons;
|
|
287
|
+
}
|
|
288
|
+
/** Section 4: `jcsHash({items})` of the sorted, distinct certificate hashes. */
|
|
289
|
+
export const certificateListHash = (certificates) => jcsHash({ items: sortedUnique(certificates) });
|
|
290
|
+
function statementError(s) {
|
|
291
|
+
if (!isSha(s.licence))
|
|
292
|
+
return "licence must be a licence hash";
|
|
293
|
+
if (!isDid(s.licensee))
|
|
294
|
+
return "licensee must be a did:key";
|
|
295
|
+
const p = isObj(s.period) ? s.period : {};
|
|
296
|
+
if (!isObj(s.period) || Object.keys(p).length !== 2 || !isIso(p.from) || !isIso(p.until) || !(ms(p.until) > ms(p.from)))
|
|
297
|
+
return "period must be {from, until}, ISO timestamps, until after from";
|
|
298
|
+
if (!isInt(s.certificates, 0, MAX_STATEMENT_CERTIFICATES))
|
|
299
|
+
return `certificates must be an integer from 0 to ${MAX_STATEMENT_CERTIFICATES}`;
|
|
300
|
+
if (!isSha(s.certificates_sha256))
|
|
301
|
+
return "certificates_sha256 must be a sha256 hash";
|
|
302
|
+
if (!isInt(s.amount_minor, 0, Number.MAX_SAFE_INTEGER))
|
|
303
|
+
return "amount_minor must be a non-negative integer";
|
|
304
|
+
if (typeof s.currency !== "string" || !CURRENCY.test(s.currency))
|
|
305
|
+
return "currency must be an ISO 4217 code";
|
|
306
|
+
if (!isIso(s.issued_at))
|
|
307
|
+
return "issued_at must be an ISO timestamp";
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
/** Section 4: the statement signed by the licensee (`signer`, the licence's licensee key). */
|
|
311
|
+
export async function buildRoyaltyStatement(f, signer) {
|
|
312
|
+
const l = f.licence;
|
|
313
|
+
if (!isLicence(l) || licenceError(l))
|
|
314
|
+
throw new Error("licence is not a licence (v1)");
|
|
315
|
+
if (signerDid(signer) !== l.licensee)
|
|
316
|
+
throw new Error("a royalty statement is signed by the licence's licensee");
|
|
317
|
+
if (!Array.isArray(f.certificates) || !f.certificates.every(isSha))
|
|
318
|
+
throw new Error("certificates must be certificate hashes");
|
|
319
|
+
const certs = sortedUnique(f.certificates);
|
|
320
|
+
const body = {
|
|
321
|
+
v: 1,
|
|
322
|
+
type: "certify.royalty-statement",
|
|
323
|
+
licence: jcsHash(l),
|
|
324
|
+
licensee: l.licensee,
|
|
325
|
+
period: { from: f.period?.from, until: f.period?.until },
|
|
326
|
+
certificates: certs.length,
|
|
327
|
+
certificates_sha256: certificateListHash(certs),
|
|
328
|
+
amount_minor: certs.length * l.royalty.per_certificate_minor,
|
|
329
|
+
currency: l.royalty.currency,
|
|
330
|
+
issued_at: f.issuedAt,
|
|
331
|
+
};
|
|
332
|
+
const err = statementError(body);
|
|
333
|
+
if (err)
|
|
334
|
+
throw new Error(err);
|
|
335
|
+
return withSignatures(body, [await bodySignature(body, signer)]);
|
|
336
|
+
}
|
|
337
|
+
/** Section 4: the scheme owner's countersignature, added beside the licensee's. */
|
|
338
|
+
export async function countersignRoyaltyStatement(statement, signer) {
|
|
339
|
+
const { signatures, ...body } = statement;
|
|
340
|
+
if (!Array.isArray(signatures))
|
|
341
|
+
throw new Error("not a royalty statement (v1)");
|
|
342
|
+
return withSignatures(body, [...signatures, await bodySignature(body, signer)]);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Section 4: signed by its licensee, countersigned by `owner`, and (with `licence`) the amount is
|
|
346
|
+
* the certificate count times the licence royalty in its currency; with `certificates`, the count
|
|
347
|
+
* and list hash are those of the supplied hashes.
|
|
348
|
+
*/
|
|
349
|
+
export function verifyRoyaltyStatement(doc, opts = {}) {
|
|
350
|
+
let authorityOwner = opts.owner;
|
|
351
|
+
const authorityReasons = [];
|
|
352
|
+
if (opts.ownerHistory !== undefined) {
|
|
353
|
+
if (!withinVerificationBudget({ doc, opts }) || !isObj(opts.ownerHistory) || Object.keys(opts).some(k => !["owner", "licence", "certificates", "ownerHistory"].includes(k)))
|
|
354
|
+
return { ok: false, reasons: ["invalid rotated-owner royalty policy or evidence bounds"] };
|
|
355
|
+
const history = verifyLicenceOwnerHistory(opts.ownerHistory.log, { root: opts.owner, checkpoint: opts.ownerHistory.checkpoint });
|
|
356
|
+
if (!history.ok)
|
|
357
|
+
return { ok: false, reasons: history.reasons };
|
|
358
|
+
authorityReasons.push(...verifyLicenceWithOwnerHistory(opts.licence, { root: opts.owner, ...opts.ownerHistory }).reasons);
|
|
359
|
+
authorityOwner = history.currentOwner;
|
|
360
|
+
}
|
|
361
|
+
const s = isObj(doc) ? doc : {};
|
|
362
|
+
if (s.v !== 1 || s.type !== "certify.royalty-statement" || Object.keys(s).length !== 11)
|
|
363
|
+
return { ok: false, reasons: ["not a royalty statement (v1)"] };
|
|
364
|
+
const reasons = [...authorityReasons];
|
|
365
|
+
const err = statementError(s);
|
|
366
|
+
if (err)
|
|
367
|
+
reasons.push(`royalty statement is malformed: ${err}`);
|
|
368
|
+
const sig = signersOf(s);
|
|
369
|
+
if (!sig.wellFormed)
|
|
370
|
+
reasons.push("royalty statement carries a signature that is malformed or invalid");
|
|
371
|
+
if (!sig.signers.includes(s.licensee))
|
|
372
|
+
reasons.push("royalty statement is not signed by its licensee");
|
|
373
|
+
if (authorityOwner === undefined)
|
|
374
|
+
reasons.push("no trusted scheme owner supplied: the countersignature was NOT checked");
|
|
375
|
+
else {
|
|
376
|
+
if (!sig.signers.includes(authorityOwner))
|
|
377
|
+
reasons.push("royalty statement is not countersigned by the scheme owner");
|
|
378
|
+
if (sig.signers.some((d) => d !== s.licensee && d !== authorityOwner))
|
|
379
|
+
reasons.push("royalty statement carries a signature by neither the licensee nor the scheme owner");
|
|
380
|
+
}
|
|
381
|
+
if (opts.licence === undefined)
|
|
382
|
+
reasons.push("no licence supplied: the royalty amount was NOT checked");
|
|
383
|
+
else {
|
|
384
|
+
const l = isObj(opts.licence) ? opts.licence : {};
|
|
385
|
+
const r = isObj(l.royalty) ? l.royalty : {};
|
|
386
|
+
if (jcsHash(opts.licence) !== s.licence)
|
|
387
|
+
reasons.push("royalty statement is for another licence");
|
|
388
|
+
else if (l.licensee !== s.licensee)
|
|
389
|
+
reasons.push("royalty statement licensee is not the licence's");
|
|
390
|
+
else if (r.currency !== s.currency || !isInt(r.per_certificate_minor, 0, MAX_ROYALTY_MINOR) || !isInt(s.certificates, 0, MAX_STATEMENT_CERTIFICATES) ||
|
|
391
|
+
s.certificates * r.per_certificate_minor !== s.amount_minor)
|
|
392
|
+
reasons.push("royalty amount is not the certificate count times the licence royalty");
|
|
393
|
+
}
|
|
394
|
+
if (opts.certificates !== undefined) {
|
|
395
|
+
const certs = sortedUnique(opts.certificates);
|
|
396
|
+
if (certs.length !== s.certificates || certificateListHash(certs) !== s.certificates_sha256)
|
|
397
|
+
reasons.push("royalty statement certificates are not the supplied certificates");
|
|
398
|
+
}
|
|
399
|
+
return { ok: reasons.length === 0, reasons };
|
|
400
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type Credential } from "@zanii/credentials";
|
|
2
|
+
import { type Signer } from "./signer.ts";
|
|
3
|
+
import { type AssuranceStatusPolicy } from "./assurance-status.ts";
|
|
4
|
+
export interface LiveEvidence {
|
|
5
|
+
v: 1;
|
|
6
|
+
type: "certify.live-evidence";
|
|
7
|
+
certificate: string;
|
|
8
|
+
agent: string;
|
|
9
|
+
window: {
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
};
|
|
13
|
+
/** Blackbox sessions of the agent in the window. */
|
|
14
|
+
sessions: number;
|
|
15
|
+
/** Guard decisions with verdict `block` (spec guard.md section 8 of Blackbox). */
|
|
16
|
+
guard_blocks: number;
|
|
17
|
+
/** Summed Blackbox summary `claims`: `failed` is a claim contradicted by the reference. */
|
|
18
|
+
claims: {
|
|
19
|
+
satisfied: number;
|
|
20
|
+
failed: number;
|
|
21
|
+
unverified: number;
|
|
22
|
+
};
|
|
23
|
+
budget_breaches: number;
|
|
24
|
+
/** Summed Blackbox summary `findings` by severity. */
|
|
25
|
+
findings: {
|
|
26
|
+
advisory: number;
|
|
27
|
+
caution: number;
|
|
28
|
+
warning: number;
|
|
29
|
+
};
|
|
30
|
+
/** Incidents in the window: `serious` as the occurrence frameworks define it, `minor` the rest. */
|
|
31
|
+
incidents: {
|
|
32
|
+
serious: number;
|
|
33
|
+
minor: number;
|
|
34
|
+
};
|
|
35
|
+
near_misses: number;
|
|
36
|
+
/** Sorted, distinct Blackbox configuration ids the gateway saw (fingerprint.configuration). */
|
|
37
|
+
configurations: string[];
|
|
38
|
+
issued_at: string;
|
|
39
|
+
signed: {
|
|
40
|
+
by: string;
|
|
41
|
+
sig: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export type LiveEvidenceInput = Omit<LiveEvidence, "v" | "type" | "issued_at" | "signed" | "configurations"> & {
|
|
45
|
+
configurations: readonly string[];
|
|
46
|
+
issuedAt: string;
|
|
47
|
+
};
|
|
48
|
+
/** Sign live evidence as the deployer's Blackbox gateway (`signer` holds the gateway key). */
|
|
49
|
+
export declare function buildLiveEvidence(fields: LiveEvidenceInput, signer: Signer): Promise<LiveEvidence>;
|
|
50
|
+
/** Section 4: the platform attestation the lab verified for the gateway (e.g. an AWS Nitro Enclaves quote). */
|
|
51
|
+
export interface GatewayAttestation {
|
|
52
|
+
platform: string;
|
|
53
|
+
/** sha256 of the attestation document the lab verified. */
|
|
54
|
+
document: string;
|
|
55
|
+
verified_at: string;
|
|
56
|
+
}
|
|
57
|
+
export interface GatewayEnrolment {
|
|
58
|
+
v: 1;
|
|
59
|
+
type: "certify.gateway-enrolment";
|
|
60
|
+
certificate: string;
|
|
61
|
+
gateway: string;
|
|
62
|
+
attestation: GatewayAttestation | null;
|
|
63
|
+
issued_at: string;
|
|
64
|
+
valid_until: string;
|
|
65
|
+
signer: string;
|
|
66
|
+
sig: string;
|
|
67
|
+
}
|
|
68
|
+
export interface GatewayEnrolmentInput {
|
|
69
|
+
certificate: string;
|
|
70
|
+
gateway: string;
|
|
71
|
+
attestation?: GatewayAttestation | null;
|
|
72
|
+
issuedAt: string;
|
|
73
|
+
validUntil: string;
|
|
74
|
+
}
|
|
75
|
+
/** Section 4: the lab enrols the deployer's gateway key for one certificate (`signer` is the lab key). */
|
|
76
|
+
export declare function buildGatewayEnrolment(f: GatewayEnrolmentInput, signer: Signer): Promise<GatewayEnrolment>;
|
|
77
|
+
/** Section 4: signed by a trusted lab (`labs`), well formed and (with `at`) in force at that time. */
|
|
78
|
+
export declare function verifyGatewayEnrolment(doc: unknown, opts?: {
|
|
79
|
+
labs?: readonly string[];
|
|
80
|
+
at?: string;
|
|
81
|
+
}): {
|
|
82
|
+
ok: boolean;
|
|
83
|
+
reasons: string[];
|
|
84
|
+
};
|
|
85
|
+
export interface LiveEvidenceVerifyOptions {
|
|
86
|
+
gateway?: string;
|
|
87
|
+
certificate?: Credential;
|
|
88
|
+
now?: string;
|
|
89
|
+
/** Section 4: accept the evidence only from a gateway the lab enrolled for its certificate. */
|
|
90
|
+
requireEnrolment?: boolean;
|
|
91
|
+
/** Section 4: the gateway's enrolment; its gateway is the expected signer when `gateway` is not given. */
|
|
92
|
+
enrolment?: unknown;
|
|
93
|
+
/** Section 4: the lab DIDs whose enrolments this verifier trusts. */
|
|
94
|
+
labs?: readonly string[];
|
|
95
|
+
/** Fresh authority status plus the last durably accepted report timestamp. */
|
|
96
|
+
gatewayStatus?: AssuranceStatusPolicy & {
|
|
97
|
+
after: string;
|
|
98
|
+
maxEvidenceAgeSeconds: number;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Pure acceptance gate; persist a new report watermark atomically with the accepted report. */
|
|
102
|
+
export declare function liveEvidenceFreshnessReasons(evidence: unknown, opts: {
|
|
103
|
+
after: string;
|
|
104
|
+
now: string;
|
|
105
|
+
maxEvidenceAgeSeconds: number;
|
|
106
|
+
}): string[];
|
|
107
|
+
/**
|
|
108
|
+
* Verify live evidence: Blackbox signed form by `gateway` (the deployer's registered gateway
|
|
109
|
+
* DID), well formed, not from the future, and (with `certificate`) for that certificate and holder.
|
|
110
|
+
* With `enrolment` or `requireEnrolment` (section 4), also an enrolment of the signing gateway for
|
|
111
|
+
* the evidence's certificate in force at its `issued_at`; `attested` says whether that gateway is.
|
|
112
|
+
*/
|
|
113
|
+
export declare function verifyLiveEvidence(evidence: unknown, opts?: LiveEvidenceVerifyOptions): {
|
|
114
|
+
ok: boolean;
|
|
115
|
+
reasons: string[];
|
|
116
|
+
attested?: boolean;
|
|
117
|
+
};
|
|
118
|
+
export declare const LIVE_TRIGGERS: readonly ["SERIOUS_INCIDENT", "GUARD_BLOCKS", "CONTRADICTED_CLAIMS", "BUDGET_BREACHES", "CONFIGURATION_CHANGED"];
|
|
119
|
+
export type LiveTrigger = (typeof LIVE_TRIGGERS)[number];
|
|
120
|
+
export interface LiveThresholds {
|
|
121
|
+
/** Guard blocks per session, in ppm, above which a re-test is due. */
|
|
122
|
+
max_guard_block_ppm: number;
|
|
123
|
+
/** Contradicted claims per checked claim (satisfied + failed), in ppm. */
|
|
124
|
+
max_contradicted_claim_ppm: number;
|
|
125
|
+
max_budget_breaches: number;
|
|
126
|
+
}
|
|
127
|
+
/** Section 3: used for every threshold a profile's `live_evidence` does not set. */
|
|
128
|
+
export declare const LIVE_DEFAULTS: LiveThresholds;
|
|
129
|
+
/**
|
|
130
|
+
* Section 3: the re-test reasons in verified live evidence, in LIVE_TRIGGERS order; empty when none.
|
|
131
|
+
* Integer comparisons only (`a * 1e6 > max * b` rather than a division).
|
|
132
|
+
*/
|
|
133
|
+
export declare function liveEvidenceTriggers(evidence: LiveEvidence, profile: {
|
|
134
|
+
live_evidence?: Partial<LiveThresholds>;
|
|
135
|
+
}, certificate?: Credential): LiveTrigger[];
|