@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,210 @@
|
|
|
1
|
+
// spec/live-evidence.md: signed periodic production metrics for a certified agent, from the
|
|
2
|
+
// deployer's Blackbox gateway identity, in Blackbox's signed form (`signed: {by, sig}`).
|
|
3
|
+
import { signCertificateWith, verifyCertificate as verifyBlackboxSigned } from "@zanii/blackbox";
|
|
4
|
+
import { verifySignedObject } from "@zanii/core";
|
|
5
|
+
import { credentialHash } from "@zanii/credentials";
|
|
6
|
+
import { MAX_SKEW_SECONDS } from "./beacon.js";
|
|
7
|
+
import { isDid, isInt, isIso, isObj, isSha, ms, sortedUnique } from "./shared.js";
|
|
8
|
+
import { signerDid, signWith } from "./signer.js";
|
|
9
|
+
import { verifyAssuranceStatus } from "./assurance-status.js";
|
|
10
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
11
|
+
const COUNT_MAX = 1_000_000_000;
|
|
12
|
+
const CONFIGURATION = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
13
|
+
const count = (v) => isInt(v, 0, COUNT_MAX);
|
|
14
|
+
const counts = (o, keys) => isObj(o) && Object.keys(o).length === keys.length && keys.every((k) => count(o[k]));
|
|
15
|
+
/** Section 1's field rules; the first broken rule, or null. */
|
|
16
|
+
function fieldError(e) {
|
|
17
|
+
const w = isObj(e.window) ? e.window : {};
|
|
18
|
+
if (!isSha(e.certificate))
|
|
19
|
+
return "certificate must be a certificate hash";
|
|
20
|
+
if (!isDid(e.agent))
|
|
21
|
+
return "agent must be a did:key";
|
|
22
|
+
if (!isObj(e.window) || Object.keys(w).length !== 2 || !isIso(w.from) || !isIso(w.to) || !(ms(w.from) < ms(w.to)))
|
|
23
|
+
return "window needs ISO UTC from and to, from before to";
|
|
24
|
+
if (!isIso(e.issued_at) || ms(e.issued_at) < ms(w.to))
|
|
25
|
+
return "issued_at must be an ISO UTC timestamp, not before window.to";
|
|
26
|
+
if (![e.sessions, e.guard_blocks, e.budget_breaches, e.near_misses].every(count))
|
|
27
|
+
return "counts must be integers 0-1000000000";
|
|
28
|
+
if (!counts(e.claims, ["satisfied", "failed", "unverified"]))
|
|
29
|
+
return "claims needs satisfied, failed and unverified counts";
|
|
30
|
+
if (!counts(e.findings, ["advisory", "caution", "warning"]))
|
|
31
|
+
return "findings needs advisory, caution and warning counts";
|
|
32
|
+
if (!counts(e.incidents, ["serious", "minor"]))
|
|
33
|
+
return "incidents needs serious and minor counts";
|
|
34
|
+
const c = e.configurations;
|
|
35
|
+
if (!Array.isArray(c) || c.length > 100 || !c.every((x) => typeof x === "string" && CONFIGURATION.test(x)) || sortedUnique(c).join("\n") !== c.join("\n"))
|
|
36
|
+
return "configurations must be at most 100 sorted, distinct configuration ids";
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/** Sign live evidence as the deployer's Blackbox gateway (`signer` holds the gateway key). */
|
|
40
|
+
export async function buildLiveEvidence(fields, signer) {
|
|
41
|
+
const unsigned = {
|
|
42
|
+
v: 1,
|
|
43
|
+
type: "certify.live-evidence",
|
|
44
|
+
certificate: fields.certificate,
|
|
45
|
+
agent: fields.agent,
|
|
46
|
+
window: fields.window,
|
|
47
|
+
sessions: fields.sessions,
|
|
48
|
+
guard_blocks: fields.guard_blocks,
|
|
49
|
+
claims: fields.claims,
|
|
50
|
+
budget_breaches: fields.budget_breaches,
|
|
51
|
+
findings: fields.findings,
|
|
52
|
+
incidents: fields.incidents,
|
|
53
|
+
near_misses: fields.near_misses,
|
|
54
|
+
configurations: Array.isArray(fields.configurations) ? sortedUnique(fields.configurations) : fields.configurations,
|
|
55
|
+
issued_at: fields.issuedAt,
|
|
56
|
+
};
|
|
57
|
+
const err = fieldError(unsigned);
|
|
58
|
+
if (err)
|
|
59
|
+
throw new Error(err);
|
|
60
|
+
return signCertificateWith(unsigned, signerDid(signer), signer.sign);
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------- 4. gateway enrolment
|
|
63
|
+
const PLATFORM = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
64
|
+
function enrolmentError(g) {
|
|
65
|
+
if (!isSha(g.certificate))
|
|
66
|
+
return "certificate must be a certificate hash";
|
|
67
|
+
if (!isDid(g.gateway))
|
|
68
|
+
return "gateway must be a did:key";
|
|
69
|
+
const a = g.attestation;
|
|
70
|
+
if (a !== null && (!isObj(a) || Object.keys(a).length !== 3 || typeof a.platform !== "string" || !PLATFORM.test(a.platform) || !isSha(a.document) || !isIso(a.verified_at)))
|
|
71
|
+
return "attestation is null or {platform, document: sha256, verified_at}";
|
|
72
|
+
if (!isIso(g.issued_at) || !isIso(g.valid_until) || !(ms(g.valid_until) > ms(g.issued_at)))
|
|
73
|
+
return "issued_at and valid_until must be ISO timestamps, valid_until after issued_at";
|
|
74
|
+
if (isObj(a) && ms(a.verified_at) > ms(g.issued_at))
|
|
75
|
+
return "the attestation is verified after the enrolment is issued";
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** Section 4: the lab enrols the deployer's gateway key for one certificate (`signer` is the lab key). */
|
|
79
|
+
export async function buildGatewayEnrolment(f, signer) {
|
|
80
|
+
const a = f.attestation ?? null;
|
|
81
|
+
const unsigned = {
|
|
82
|
+
v: 1,
|
|
83
|
+
type: "certify.gateway-enrolment",
|
|
84
|
+
certificate: f.certificate,
|
|
85
|
+
gateway: f.gateway,
|
|
86
|
+
attestation: a === null ? null : { platform: a.platform, document: a.document, verified_at: a.verified_at },
|
|
87
|
+
issued_at: f.issuedAt,
|
|
88
|
+
valid_until: f.validUntil,
|
|
89
|
+
};
|
|
90
|
+
const err = enrolmentError(unsigned);
|
|
91
|
+
if (err)
|
|
92
|
+
throw new Error(err);
|
|
93
|
+
return signWith(unsigned, signer);
|
|
94
|
+
}
|
|
95
|
+
/** Section 4: signed by a trusted lab (`labs`), well formed and (with `at`) in force at that time. */
|
|
96
|
+
export function verifyGatewayEnrolment(doc, opts = {}) {
|
|
97
|
+
const g = isObj(doc) ? doc : {};
|
|
98
|
+
if (g.v !== 1 || g.type !== "certify.gateway-enrolment" || Object.keys(g).length !== 9)
|
|
99
|
+
return { ok: false, reasons: ["not a gateway enrolment (v1)"] };
|
|
100
|
+
const reasons = [];
|
|
101
|
+
if (!verifySignedObject(g))
|
|
102
|
+
reasons.push("bad gateway enrolment signature");
|
|
103
|
+
const err = enrolmentError(g);
|
|
104
|
+
if (err)
|
|
105
|
+
reasons.push(`gateway enrolment is malformed: ${err}`);
|
|
106
|
+
if (!opts.labs)
|
|
107
|
+
reasons.push("no trusted labs supplied: the enrolment signer was NOT checked");
|
|
108
|
+
else if (!opts.labs.includes(g.signer))
|
|
109
|
+
reasons.push("gateway enrolment is not signed by a trusted lab");
|
|
110
|
+
if (opts.at !== undefined && !(ms(g.issued_at) <= ms(opts.at) && ms(opts.at) <= ms(g.valid_until)))
|
|
111
|
+
reasons.push("gateway enrolment is not in force at the evidence time");
|
|
112
|
+
return { ok: reasons.length === 0, reasons };
|
|
113
|
+
}
|
|
114
|
+
/** Pure acceptance gate; persist a new report watermark atomically with the accepted report. */
|
|
115
|
+
export function liveEvidenceFreshnessReasons(evidence, opts) {
|
|
116
|
+
if (!isObj(opts) || !isIso(opts.now) || !isIso(opts.after) || !isInt(opts.maxEvidenceAgeSeconds, 1, 300) || ms(opts.after) > ms(opts.now))
|
|
117
|
+
return ["strict gateway evidence needs a retained report watermark and bounded freshness policy"];
|
|
118
|
+
if (!withinVerificationBudget(evidence, { maxBytes: 65_536, maxNodes: 1000, maxDepth: 8 }))
|
|
119
|
+
return ["strict gateway evidence exceeds its JSON budget or has an invalid status policy"];
|
|
120
|
+
const e = isObj(evidence) ? evidence : {}, now = ms(opts.now);
|
|
121
|
+
return !isIso(e.issued_at) || !isObj(e.window) || !isIso(e.window.to) || ms(e.issued_at) <= ms(opts.after) || now - ms(e.issued_at) >= opts.maxEvidenceAgeSeconds * 1000 || now - ms(e.window.to) >= opts.maxEvidenceAgeSeconds * 1000 || ms(e.issued_at) > now + 5_000
|
|
122
|
+
? ["live evidence is stale, replayed or from the future"] : [];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Verify live evidence: Blackbox signed form by `gateway` (the deployer's registered gateway
|
|
126
|
+
* DID), well formed, not from the future, and (with `certificate`) for that certificate and holder.
|
|
127
|
+
* With `enrolment` or `requireEnrolment` (section 4), also an enrolment of the signing gateway for
|
|
128
|
+
* the evidence's certificate in force at its `issued_at`; `attested` says whether that gateway is.
|
|
129
|
+
*/
|
|
130
|
+
export function verifyLiveEvidence(evidence, opts = {}) {
|
|
131
|
+
const enrolled = opts.enrolment !== undefined || opts.requireEnrolment === true || opts.gatewayStatus !== undefined;
|
|
132
|
+
if (opts.gatewayStatus !== undefined && (!withinVerificationBudget({ evidence, opts }, { maxBytes: 4_194_304 }) || !isObj(opts.gatewayStatus)))
|
|
133
|
+
return { ok: false, reasons: ["strict gateway evidence exceeds its JSON budget or has an invalid status policy"], attested: false };
|
|
134
|
+
const e = isObj(evidence) ? evidence : {};
|
|
135
|
+
if (e.v !== 1 || e.type !== "certify.live-evidence" || Object.keys(e).length !== 15)
|
|
136
|
+
return { ok: false, reasons: ["not live evidence (v1)"], ...(enrolled ? { attested: false } : {}) };
|
|
137
|
+
const reasons = [];
|
|
138
|
+
const g = isObj(opts.enrolment) ? opts.enrolment : null;
|
|
139
|
+
const gateway = opts.gateway ?? (g && isDid(g.gateway) ? g.gateway : undefined);
|
|
140
|
+
const sig = verifyBlackboxSigned(e, gateway);
|
|
141
|
+
if (!sig.ok)
|
|
142
|
+
reasons.push(sig.reason === "wrong_signer" ? "live evidence is not signed by the expected gateway" : "bad live evidence signature");
|
|
143
|
+
if (!gateway)
|
|
144
|
+
reasons.push("no gateway identity supplied: the signer was NOT checked");
|
|
145
|
+
const err = fieldError(e);
|
|
146
|
+
if (err)
|
|
147
|
+
reasons.push(`live evidence is malformed: ${err}`);
|
|
148
|
+
const now = opts.now ? ms(opts.now) : Date.now();
|
|
149
|
+
const policy = opts.gatewayStatus;
|
|
150
|
+
if (policy !== undefined) {
|
|
151
|
+
reasons.push(...liveEvidenceFreshnessReasons(evidence, { after: policy.after, maxEvidenceAgeSeconds: policy.maxEvidenceAgeSeconds, now: opts.now ?? new Date(now).toISOString() }));
|
|
152
|
+
reasons.push(...verifyAssuranceStatus(policy.document, { ...policy, purpose: "gateway-enrolment", artifact: opts.enrolment, now: opts.now ?? new Date(now).toISOString() }).reasons);
|
|
153
|
+
}
|
|
154
|
+
if (ms(e.issued_at) > now + MAX_SKEW_SECONDS * 1000)
|
|
155
|
+
reasons.push("live evidence issued_at is in the future");
|
|
156
|
+
if (opts.certificate) {
|
|
157
|
+
if (e.certificate !== credentialHash(opts.certificate))
|
|
158
|
+
reasons.push("live evidence names a different certificate");
|
|
159
|
+
if (e.agent !== opts.certificate.holder)
|
|
160
|
+
reasons.push("live evidence agent is not the certificate holder");
|
|
161
|
+
}
|
|
162
|
+
if (!enrolled)
|
|
163
|
+
return { ok: reasons.length === 0, reasons };
|
|
164
|
+
if (opts.enrolment === undefined) {
|
|
165
|
+
reasons.push("no gateway enrolment supplied: live evidence is accepted only from an enrolled gateway");
|
|
166
|
+
return { ok: false, reasons, attested: false };
|
|
167
|
+
}
|
|
168
|
+
reasons.push(...verifyGatewayEnrolment(opts.enrolment, { ...(opts.labs ? { labs: opts.labs } : {}), at: String(e.issued_at) }).reasons);
|
|
169
|
+
if (g && isObj(e.signed) && g.gateway !== e.signed.by)
|
|
170
|
+
reasons.push("live evidence is not signed by the enrolled gateway");
|
|
171
|
+
if (g && g.certificate !== e.certificate)
|
|
172
|
+
reasons.push("the gateway enrolment is for a different certificate");
|
|
173
|
+
const ok = reasons.length === 0;
|
|
174
|
+
return { ok, reasons, attested: ok && g !== null && g.attestation !== null };
|
|
175
|
+
}
|
|
176
|
+
export const LIVE_TRIGGERS = [
|
|
177
|
+
"SERIOUS_INCIDENT",
|
|
178
|
+
"GUARD_BLOCKS",
|
|
179
|
+
"CONTRADICTED_CLAIMS",
|
|
180
|
+
"BUDGET_BREACHES",
|
|
181
|
+
"CONFIGURATION_CHANGED",
|
|
182
|
+
];
|
|
183
|
+
/** Section 3: used for every threshold a profile's `live_evidence` does not set. */
|
|
184
|
+
export const LIVE_DEFAULTS = {
|
|
185
|
+
max_guard_block_ppm: 10_000,
|
|
186
|
+
max_contradicted_claim_ppm: 10_000,
|
|
187
|
+
max_budget_breaches: 0,
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Section 3: the re-test reasons in verified live evidence, in LIVE_TRIGGERS order; empty when none.
|
|
191
|
+
* Integer comparisons only (`a * 1e6 > max * b` rather than a division).
|
|
192
|
+
*/
|
|
193
|
+
export function liveEvidenceTriggers(evidence, profile, certificate) {
|
|
194
|
+
const t = { ...LIVE_DEFAULTS, ...(isObj(profile?.live_evidence) ? profile.live_evidence : {}) };
|
|
195
|
+
const e = evidence;
|
|
196
|
+
const checked = e.claims.satisfied + e.claims.failed;
|
|
197
|
+
const out = [];
|
|
198
|
+
if (e.incidents.serious > 0)
|
|
199
|
+
out.push("SERIOUS_INCIDENT");
|
|
200
|
+
if (e.sessions > 0 && e.guard_blocks * 1_000_000 > t.max_guard_block_ppm * e.sessions)
|
|
201
|
+
out.push("GUARD_BLOCKS");
|
|
202
|
+
if (checked > 0 && e.claims.failed * 1_000_000 > t.max_contradicted_claim_ppm * checked)
|
|
203
|
+
out.push("CONTRADICTED_CLAIMS");
|
|
204
|
+
if (e.budget_breaches > t.max_budget_breaches)
|
|
205
|
+
out.push("BUDGET_BREACHES");
|
|
206
|
+
const configuration = isObj(certificate?.claim) ? certificate.claim.configuration : undefined;
|
|
207
|
+
if (certificate && e.configurations.some((c) => c !== configuration))
|
|
208
|
+
out.push("CONFIGURATION_CHANGED");
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const AWS_NITRO_GOV_ROOT_PEM = "-----BEGIN CERTIFICATE-----\nMIICIDCCAaWgAwIBAgIQP+wUYfyWFFRko9PR00zhZzAKBggqhkjOPQQDAzBQMQsw\nCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQLDANBV1MxIjAgBgNV\nBAMMGWF3cy11cy1nb3Yubml0cm8tZW5jbGF2ZXMwIBcNMjAwOTEwMTIwMzQ2WhgP\nMjA1MDA5MTAxMzAzNDZaMFAxCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZBbWF6b24x\nDDAKBgNVBAsMA0FXUzEiMCAGA1UEAwwZYXdzLXVzLWdvdi5uaXRyby1lbmNsYXZl\nczB2MBAGByqGSM49AgEGBSuBBAAiA2IABCzkRJcZVx7Sg2yXXkl0Nqj9o1ECZNAh\n0L8/90ATZXAaS1rxA1ti1F3wE86PGsh2UiQIYXiMu81l5kO7775gPuLsgYcGMO/J\n0t08BHI8s3+JmjxTlA+/UyAqEmj7fD5CbKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd\nBgNVHQ4EFgQUUKIzFk2FAlhihuQexsqOxZ5ZjF0wDgYDVR0PAQH/BAQDAgGGMAoG\nCCqGSM49BAMDA2kAMGYCMQD9bO9epcf5kMSdsHcyNJXs4bo07wvTIOwnxN41t5eE\nSDyXtUei++RebAbI9Viap2gCMQC7PVZ6Kpg0+N9k+DDpksoJv7gx6YwCqKsmTfU/\nWigyQlpyJUrWapqk0afDA4lef14=\n-----END CERTIFICATE-----\n";
|
|
2
|
+
/** SHA-256 of the exact DER bytes independently copied from the official GovCloud page above. */
|
|
3
|
+
export declare const AWS_NITRO_GOV_ROOT_SHA256 = "20:BC:D3:0A:3E:16:8E:A0:37:89:E1:C4:59:12:BC:12:3C:4D:E3:C9:77:B8:A6:E1:C1:25:C7:92:9D:66:8B:3B";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Official AWS GovCloud HTML certificate (not PDF OCR), retrieved 2026-10-24:
|
|
2
|
+
// https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ec2.html
|
|
3
|
+
// Root selection is trusted configuration; an attestation document never chooses its trust anchor.
|
|
4
|
+
export const AWS_NITRO_GOV_ROOT_PEM = `-----BEGIN CERTIFICATE-----
|
|
5
|
+
MIICIDCCAaWgAwIBAgIQP+wUYfyWFFRko9PR00zhZzAKBggqhkjOPQQDAzBQMQsw
|
|
6
|
+
CQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQLDANBV1MxIjAgBgNV
|
|
7
|
+
BAMMGWF3cy11cy1nb3Yubml0cm8tZW5jbGF2ZXMwIBcNMjAwOTEwMTIwMzQ2WhgP
|
|
8
|
+
MjA1MDA5MTAxMzAzNDZaMFAxCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZBbWF6b24x
|
|
9
|
+
DDAKBgNVBAsMA0FXUzEiMCAGA1UEAwwZYXdzLXVzLWdvdi5uaXRyby1lbmNsYXZl
|
|
10
|
+
czB2MBAGByqGSM49AgEGBSuBBAAiA2IABCzkRJcZVx7Sg2yXXkl0Nqj9o1ECZNAh
|
|
11
|
+
0L8/90ATZXAaS1rxA1ti1F3wE86PGsh2UiQIYXiMu81l5kO7775gPuLsgYcGMO/J
|
|
12
|
+
0t08BHI8s3+JmjxTlA+/UyAqEmj7fD5CbKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd
|
|
13
|
+
BgNVHQ4EFgQUUKIzFk2FAlhihuQexsqOxZ5ZjF0wDgYDVR0PAQH/BAQDAgGGMAoG
|
|
14
|
+
CCqGSM49BAMDA2kAMGYCMQD9bO9epcf5kMSdsHcyNJXs4bo07wvTIOwnxN41t5eE
|
|
15
|
+
SDyXtUei++RebAbI9Viap2gCMQC7PVZ6Kpg0+N9k+DDpksoJv7gx6YwCqKsmTfU/
|
|
16
|
+
WigyQlpyJUrWapqk0afDA4lef14=
|
|
17
|
+
-----END CERTIFICATE-----
|
|
18
|
+
`;
|
|
19
|
+
/** SHA-256 of the exact DER bytes independently copied from the official GovCloud page above. */
|
|
20
|
+
export const AWS_NITRO_GOV_ROOT_SHA256 = "20:BC:D3:0A:3E:16:8E:A0:37:89:E1:C4:59:12:BC:12:3C:4D:E3:C9:77:B8:A6:E1:C1:25:C7:92:9D:66:8B:3B";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { X509Certificate } from "node:crypto";
|
|
2
|
+
type Constraints = {
|
|
3
|
+
ca: boolean;
|
|
4
|
+
pathLen?: number;
|
|
5
|
+
critical: boolean;
|
|
6
|
+
};
|
|
7
|
+
type Usage = {
|
|
8
|
+
signature: boolean;
|
|
9
|
+
certificates: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function nitroCertificateConstraints(cert: X509Certificate): {
|
|
12
|
+
basic: Constraints | null;
|
|
13
|
+
usage: Usage | null;
|
|
14
|
+
};
|
|
15
|
+
export declare function nitroCertificatePathReasons(certs: X509Certificate[]): string[];
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export function nitroCertificateConstraints(cert) {
|
|
2
|
+
const bytes = cert.raw;
|
|
3
|
+
if (bytes.length < 1 || bytes.length > 1024)
|
|
4
|
+
throw new Error("certificate exceeds its 1024-byte bound");
|
|
5
|
+
let nodes = 0;
|
|
6
|
+
function parts(start, end, depth) {
|
|
7
|
+
if (depth > 8 || start < 0 || end > bytes.length || start > end)
|
|
8
|
+
throw new Error("invalid DER bounds");
|
|
9
|
+
const out = [];
|
|
10
|
+
let cursor = start;
|
|
11
|
+
while (cursor < end) {
|
|
12
|
+
if (++nodes > 256 || end - cursor < 2)
|
|
13
|
+
throw new Error("invalid DER node budget");
|
|
14
|
+
const tag = bytes[cursor++], first = bytes[cursor++];
|
|
15
|
+
if ((tag & 31) === 31)
|
|
16
|
+
throw new Error("unsupported DER tag");
|
|
17
|
+
let size = first;
|
|
18
|
+
if (first & 128) {
|
|
19
|
+
const n = first & 127;
|
|
20
|
+
if (!n || n > 2 || n > end - cursor || bytes[cursor] === 0)
|
|
21
|
+
throw new Error("invalid DER length");
|
|
22
|
+
size = 0;
|
|
23
|
+
for (let i = 0; i < n; i++)
|
|
24
|
+
size = size * 256 + bytes[cursor++];
|
|
25
|
+
if (size < 128)
|
|
26
|
+
throw new Error("non-minimal DER length");
|
|
27
|
+
}
|
|
28
|
+
if (size > end - cursor)
|
|
29
|
+
throw new Error("truncated DER value");
|
|
30
|
+
out.push({ tag, start: cursor, end: cursor + size });
|
|
31
|
+
cursor += size;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function children(part, tag, depth) {
|
|
36
|
+
if (part.tag !== tag)
|
|
37
|
+
throw new Error("invalid DER structure");
|
|
38
|
+
return parts(part.start, part.end, depth);
|
|
39
|
+
}
|
|
40
|
+
function content(part, tag) { if (part.tag !== tag)
|
|
41
|
+
throw new Error("invalid DER field"); return bytes.subarray(part.start, part.end); }
|
|
42
|
+
function boolean(part) { const b = content(part, 1); if (b.length !== 1 || b[0] !== 0 && b[0] !== 255)
|
|
43
|
+
throw new Error("invalid DER boolean"); return b[0] === 255; }
|
|
44
|
+
const outer = parts(0, bytes.length, 0);
|
|
45
|
+
if (outer.length !== 1)
|
|
46
|
+
throw new Error("invalid DER certificate");
|
|
47
|
+
const certificate = children(outer[0], 48, 1);
|
|
48
|
+
if (certificate.length !== 3)
|
|
49
|
+
throw new Error("invalid DER certificate");
|
|
50
|
+
const tbs = children(certificate[0], 48, 2), containers = tbs.filter(p => p.tag === 163);
|
|
51
|
+
if (containers.length > 1)
|
|
52
|
+
throw new Error("duplicate certificate extensions");
|
|
53
|
+
if (!containers.length)
|
|
54
|
+
return { basic: null, usage: null };
|
|
55
|
+
const container = children(containers[0], 163, 3);
|
|
56
|
+
if (container.length !== 1)
|
|
57
|
+
throw new Error("invalid certificate extensions");
|
|
58
|
+
const extensions = children(container[0], 48, 4);
|
|
59
|
+
if (extensions.length > 32)
|
|
60
|
+
throw new Error("certificate exceeds its 32-extension bound");
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
let basic = null, usage = null;
|
|
63
|
+
for (const extension of extensions) {
|
|
64
|
+
const fields = children(extension, 48, 5);
|
|
65
|
+
if (fields.length < 2 || fields.length > 3)
|
|
66
|
+
throw new Error("invalid certificate extension");
|
|
67
|
+
const oidBytes = content(fields[0], 6);
|
|
68
|
+
if (!oidBytes.length || oidBytes.length > 32 || oidBytes.at(-1) & 128 || oidBytes.some((b, i) => b === 128 && (i === 0 || !(oidBytes[i - 1] & 128))))
|
|
69
|
+
throw new Error("invalid DER extension identifier");
|
|
70
|
+
const oid = oidBytes.toString("hex");
|
|
71
|
+
if (seen.has(oid))
|
|
72
|
+
throw new Error("duplicate certificate extension");
|
|
73
|
+
seen.add(oid);
|
|
74
|
+
const critical = fields.length === 3 ? boolean(fields[1]) : false, value = fields.at(-1);
|
|
75
|
+
content(value, 4);
|
|
76
|
+
if (oid === "551d13") {
|
|
77
|
+
const sequence = parts(value.start, value.end, 6);
|
|
78
|
+
if (sequence.length !== 1)
|
|
79
|
+
throw new Error("invalid basic constraints");
|
|
80
|
+
const values = children(sequence[0], 48, 7);
|
|
81
|
+
let index = 0;
|
|
82
|
+
const ca = values[0]?.tag === 1 ? boolean(values[index++]) : false;
|
|
83
|
+
let pathLen;
|
|
84
|
+
if (values[index]) {
|
|
85
|
+
const n = content(values[index++], 2);
|
|
86
|
+
if (!ca || !n.length || n.length > 4 || n[0] & 128 || n.length > 1 && n[0] === 0 && !(n[1] & 128))
|
|
87
|
+
throw new Error("invalid CA path length");
|
|
88
|
+
pathLen = 0;
|
|
89
|
+
for (const b of n)
|
|
90
|
+
pathLen = pathLen * 256 + b;
|
|
91
|
+
}
|
|
92
|
+
if (index !== values.length)
|
|
93
|
+
throw new Error("invalid basic constraints");
|
|
94
|
+
basic = { ca, critical, ...(pathLen !== undefined ? { pathLen } : {}) };
|
|
95
|
+
}
|
|
96
|
+
else if (oid === "551d0f") {
|
|
97
|
+
const bitString = parts(value.start, value.end, 6);
|
|
98
|
+
if (bitString.length !== 1)
|
|
99
|
+
throw new Error("invalid key usage");
|
|
100
|
+
const bits = content(bitString[0], 3), unused = bits[0];
|
|
101
|
+
if (bits.length < 2 || bits.length > 3 || unused === undefined || unused > 7 || bits.at(-1) & ((1 << unused) - 1) || bits.length === 3 && (bits[2] & 127 || unused !== 7) || !bits.subarray(1).some(b => b !== 0))
|
|
102
|
+
throw new Error("invalid key usage bits");
|
|
103
|
+
usage = { signature: !!(bits[1] & 128), certificates: !!(bits[1] & 4) };
|
|
104
|
+
}
|
|
105
|
+
else if (critical || ["551d1e", "551d20", "551d21", "551d24", "551d25", "551d36"].includes(oid)) {
|
|
106
|
+
throw new Error("unsupported certificate constraint or critical extension");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { basic, usage };
|
|
110
|
+
}
|
|
111
|
+
export function nitroCertificatePathReasons(certs) {
|
|
112
|
+
const reasons = [];
|
|
113
|
+
for (let i = 0; i < certs.length; i++) {
|
|
114
|
+
const certificate = certs[i];
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = nitroCertificateConstraints(certificate);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
reasons.push(`certificate ${i} has malformed or unsupported constraints`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const { basic, usage } = parsed, leaf = i === certs.length - 1;
|
|
124
|
+
if (leaf) {
|
|
125
|
+
if (certificate.ca || basic?.ca || basic?.pathLen !== undefined)
|
|
126
|
+
reasons.push(`certificate ${i} is not an end-entity signing certificate`);
|
|
127
|
+
if (!usage || !usage.signature || usage.certificates)
|
|
128
|
+
reasons.push(`certificate ${i} key usage does not permit attestation signing`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (!basic?.ca || !basic.critical)
|
|
132
|
+
reasons.push(`certificate ${i} lacks critical CA basic constraints`);
|
|
133
|
+
if (!usage || !usage.certificates)
|
|
134
|
+
reasons.push(`certificate ${i} key usage does not permit certificate signing`);
|
|
135
|
+
const below = certs.slice(i + 1, -1).filter(c => c.subject !== c.issuer).length;
|
|
136
|
+
if (basic?.pathLen !== undefined && below > basic.pathLen)
|
|
137
|
+
reasons.push(`certificate ${i} path length constraint is exceeded`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return reasons;
|
|
141
|
+
}
|
package/dist/nitro.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { X509Certificate } from "node:crypto";
|
|
2
|
+
import type { Attestation, AttestCheck, QuoteVerifier } from "@zanii/attest";
|
|
3
|
+
export { AWS_NITRO_GOV_ROOT_PEM, AWS_NITRO_GOV_ROOT_SHA256 } from "./nitro-roots.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Section 2: the AWS Nitro Enclaves Root-G1 certificate, from
|
|
6
|
+
* https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip, whose SHA-256
|
|
7
|
+
* fingerprint AWS publishes at https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html
|
|
8
|
+
* (CN=aws.nitro-enclaves, C=US, O=Amazon, OU=AWS, valid 2019-10-28 to 2049-10-28).
|
|
9
|
+
*/
|
|
10
|
+
export declare const AWS_NITRO_ROOT_PEM = "-----BEGIN CERTIFICATE-----\nMIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD\nVQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4\nMTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL\nDANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG\nBSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb\n48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE\nh8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF\nR+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC\nMQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW\nrfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N\nIwLz3/Y=\n-----END CERTIFICATE-----\n";
|
|
11
|
+
/** The fingerprint AWS publishes for the root (verify-root.html), as `X509Certificate.fingerprint256` writes it. */
|
|
12
|
+
export declare const AWS_NITRO_ROOT_SHA256 = "64:1A:03:21:A3:E2:44:EF:E4:56:46:31:95:D6:06:31:7E:D7:CD:CC:3C:17:56:E0:98:93:F3:C6:8F:79:BB:5B";
|
|
13
|
+
export type NitroPartition = "aws" | "aws-us-gov";
|
|
14
|
+
/** Explicit operator policy only. Never select a root from the quote's subject, bundle or request. */
|
|
15
|
+
export declare function nitroRootForPartition(partition?: NitroPartition): string;
|
|
16
|
+
/** Section 5: default freshness window, and the allowed skew between the document and the attestation claim. */
|
|
17
|
+
export declare const NITRO_MAX_AGE_SECONDS = 300;
|
|
18
|
+
export declare const NITRO_MAX_SKEW_SECONDS = 300;
|
|
19
|
+
export interface NitroDocument {
|
|
20
|
+
moduleId: string;
|
|
21
|
+
digest: "SHA384";
|
|
22
|
+
/** Milliseconds since the UNIX epoch (UTC). */
|
|
23
|
+
timestamp: number;
|
|
24
|
+
/** PCR index to lowercase hex. */
|
|
25
|
+
pcrs: Record<number, string>;
|
|
26
|
+
certificate: Uint8Array;
|
|
27
|
+
cabundle: Uint8Array[];
|
|
28
|
+
publicKey: Uint8Array | null;
|
|
29
|
+
userData: Uint8Array | null;
|
|
30
|
+
nonce: Uint8Array | null;
|
|
31
|
+
}
|
|
32
|
+
export interface NitroCheck extends AttestCheck {
|
|
33
|
+
document: NitroDocument | null;
|
|
34
|
+
}
|
|
35
|
+
export interface NitroOptions {
|
|
36
|
+
/** The trust anchor (PEM or certificate); the pinned AWS root by default. Tests pass their own. */
|
|
37
|
+
root?: string | X509Certificate;
|
|
38
|
+
/** The verifier's clock, ISO UTC; `Date.now()` by default. */
|
|
39
|
+
now?: string;
|
|
40
|
+
/** Oldest acceptable document, in seconds before `now`; `Infinity` turns the check off. */
|
|
41
|
+
maxAgeSeconds?: number;
|
|
42
|
+
/** The expected nonce: bytes, or a string compared as its UTF-8 bytes. */
|
|
43
|
+
nonce?: Uint8Array | string;
|
|
44
|
+
/** Expected PCR values by index (PCR0, PCR1, PCR2, PCR8, ...), hex. */
|
|
45
|
+
pcrs?: Readonly<Record<number, string>>;
|
|
46
|
+
}
|
|
47
|
+
/** RFC 9052 section 4.4: Sig_structure = ["Signature1", body_protected, external_aad (empty), payload]. */
|
|
48
|
+
export declare function sigStructure(protectedBytes: Uint8Array, payload: Uint8Array): Uint8Array;
|
|
49
|
+
/** Sections 3 and 4: decode the COSE_Sign1 envelope and its attestation document; throws on any malformed part. */
|
|
50
|
+
export declare function parseNitroDocument(bytes: Uint8Array): {
|
|
51
|
+
protectedBytes: Uint8Array;
|
|
52
|
+
payload: Uint8Array;
|
|
53
|
+
signature: Uint8Array;
|
|
54
|
+
document: NitroDocument;
|
|
55
|
+
};
|
|
56
|
+
/** Section 4 and 5: the document's envelope, signature, chain, freshness, nonce and PCRs. */
|
|
57
|
+
export declare function verifyNitroDocument(bytes: Uint8Array, opts?: NitroOptions): NitroCheck;
|
|
58
|
+
/** The `measurement` a Nitro attestation claims: PCR0 (the enclave image), `sha384:<hex>`. */
|
|
59
|
+
export declare const nitroMeasurement: (doc: NitroDocument) => string;
|
|
60
|
+
export type NitroQuoteOptions = Omit<NitroOptions, "nonce" | "now"> & {
|
|
61
|
+
/** The raw attestation document whose SHA-256 is `quoteHash`, or nothing. */
|
|
62
|
+
quote: (quoteHash: string) => Uint8Array | null | undefined | Promise<Uint8Array | null | undefined>;
|
|
63
|
+
now?: () => string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Section 6: a @zanii/attest QuoteVerifier for platform `nitro`. The Attestation carries only the
|
|
67
|
+
* quote's hash, so `quote` returns the raw document by `quoteHash` (from the deployer's upload or
|
|
68
|
+
* a store). It holds when the document hashes to `quoteHash`, verifies (section 5) with
|
|
69
|
+
* `nonce` = the attestation's nonce, its PCR0 is the claimed `measurement`, and its timestamp is
|
|
70
|
+
* within 300 seconds of `attestedAt`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function nitroQuoteVerifier(opts: NitroQuoteOptions): QuoteVerifier;
|
|
73
|
+
/** `nitroQuoteVerifier` with its reasons. */
|
|
74
|
+
export declare function checkNitroAttestation(a: Attestation, opts: NitroQuoteOptions): Promise<NitroCheck>;
|