@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/audit.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// spec/audit.md: the control audit attestation a Level 3 certificate references by hash, signed by
|
|
2
|
+
// the accredited certification body that ran the audit.
|
|
3
|
+
import { jcsHash, verifySignedObject } from "@zanii/core";
|
|
4
|
+
import { isDid, isIso, isObj, ms, sortedUnique } from "./shared.js";
|
|
5
|
+
import { signerDid, signWith } from "./signer.js";
|
|
6
|
+
export const AUDIT_OUTCOMES = ["conforming", "minor_nonconformities", "major_nonconformities"];
|
|
7
|
+
/** A clause or Annex A control id, by number only (`8.2`, `A.6.2.4`). */
|
|
8
|
+
export const CONTROL = /^(A\.)?[0-9]{1,3}(\.[0-9]{1,3}){0,4}$/;
|
|
9
|
+
const STANDARD = /^[A-Za-z0-9][A-Za-z0-9 ./:_-]{0,99}$/;
|
|
10
|
+
const COUNTRY = /^[A-Z]{2}$/;
|
|
11
|
+
const text = (v, max) => typeof v === "string" && v.length >= 1 && v.length <= max;
|
|
12
|
+
/** Section 1's field rules; the first broken rule, or null. `controls` must already be sorted. */
|
|
13
|
+
function fieldError(a) {
|
|
14
|
+
const ap = isObj(a.applicant) ? a.applicant : {};
|
|
15
|
+
const sc = isObj(a.scope) ? a.scope : {};
|
|
16
|
+
if (!isDid(a.holder))
|
|
17
|
+
return "holder must be a did:key";
|
|
18
|
+
if (!isObj(a.applicant) || Object.keys(ap).length !== 2 || !text(ap.name, 200) || typeof ap.country !== "string" || !COUNTRY.test(ap.country))
|
|
19
|
+
return "applicant needs a name (1-200) and an ISO 3166-1 alpha-2 country";
|
|
20
|
+
if (!isObj(a.scope) || Object.keys(sc).length !== 2 || !text(sc.en, 500) || !text(sc.ar, 500))
|
|
21
|
+
return "scope needs en and ar text (1-500)";
|
|
22
|
+
if (typeof a.standard !== "string" || !STANDARD.test(a.standard))
|
|
23
|
+
return "standard is not a standard reference";
|
|
24
|
+
const c = a.controls;
|
|
25
|
+
if (!Array.isArray(c) || c.length < 1 || c.length > 500 || !c.every((x) => typeof x === "string" && CONTROL.test(x)))
|
|
26
|
+
return "controls must be 1-500 clause ids by number";
|
|
27
|
+
if (sortedUnique(c).join("\n") !== c.join("\n"))
|
|
28
|
+
return "controls must be sorted and distinct";
|
|
29
|
+
if (!isIso(a.audited_at) || !isIso(a.valid_until) || !isIso(a.issued_at))
|
|
30
|
+
return "dates must be ISO UTC timestamps";
|
|
31
|
+
if (ms(a.valid_until) <= ms(a.audited_at) || ms(a.issued_at) < ms(a.audited_at))
|
|
32
|
+
return "valid_until must be after audited_at, issued_at not before it";
|
|
33
|
+
if (!AUDIT_OUTCOMES.includes(a.outcome))
|
|
34
|
+
return "unknown outcome";
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/** Sign an audit attestation as the certification body (`signer` holds the body's key). */
|
|
38
|
+
export async function buildAuditAttestation(fields, signer) {
|
|
39
|
+
const unsigned = {
|
|
40
|
+
v: 1,
|
|
41
|
+
type: "certify.audit-attestation",
|
|
42
|
+
body: signerDid(signer),
|
|
43
|
+
holder: fields.holder,
|
|
44
|
+
applicant: fields.applicant,
|
|
45
|
+
scope: fields.scope,
|
|
46
|
+
standard: fields.standard,
|
|
47
|
+
controls: Array.isArray(fields.controls) ? sortedUnique(fields.controls) : fields.controls,
|
|
48
|
+
audited_at: fields.auditedAt,
|
|
49
|
+
valid_until: fields.validUntil,
|
|
50
|
+
outcome: fields.outcome,
|
|
51
|
+
issued_at: fields.issuedAt,
|
|
52
|
+
};
|
|
53
|
+
const err = fieldError(unsigned);
|
|
54
|
+
if (err)
|
|
55
|
+
throw new Error(err);
|
|
56
|
+
return signWith(unsigned, signer);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Verify an audit attestation: signed by its body, well formed, from a body in `trustedBodies`,
|
|
60
|
+
* still valid at `now`, not a major nonconformity, and (with `certificate`) for that certificate's
|
|
61
|
+
* holder and applicant, referenced by its `claim.audit` and dated no later than its issue.
|
|
62
|
+
*/
|
|
63
|
+
export function verifyAuditAttestation(att, opts = {}) {
|
|
64
|
+
const a = isObj(att) ? att : {};
|
|
65
|
+
if (a.v !== 1 || a.type !== "certify.audit-attestation" || !isDid(a.body) || Object.keys(a).length !== 14)
|
|
66
|
+
return { ok: false, reasons: ["not an audit attestation (v1)"] };
|
|
67
|
+
const reasons = [];
|
|
68
|
+
if (!verifySignedObject(a) || a.signer !== a.body)
|
|
69
|
+
reasons.push("bad audit attestation signature");
|
|
70
|
+
const err = fieldError(a);
|
|
71
|
+
if (err)
|
|
72
|
+
reasons.push(`audit attestation is malformed: ${err}`);
|
|
73
|
+
if (!opts.trustedBodies)
|
|
74
|
+
reasons.push("no trusted certification bodies supplied: the audit body was NOT checked");
|
|
75
|
+
else if (!opts.trustedBodies.includes(a.body))
|
|
76
|
+
reasons.push("audit body is not a trusted certification body");
|
|
77
|
+
const now = opts.now ? ms(opts.now) : Date.now();
|
|
78
|
+
if (ms(a.valid_until) < now)
|
|
79
|
+
reasons.push(`audit attestation expired on ${String(a.valid_until)}`);
|
|
80
|
+
if (a.outcome === "major_nonconformities")
|
|
81
|
+
reasons.push("audit outcome is major_nonconformities");
|
|
82
|
+
const cert = opts.certificate;
|
|
83
|
+
if (cert) {
|
|
84
|
+
const claim = isObj(cert.claim) ? cert.claim : {};
|
|
85
|
+
const ca = isObj(claim.applicant) ? claim.applicant : {};
|
|
86
|
+
const ap = isObj(a.applicant) ? a.applicant : {};
|
|
87
|
+
if (a.holder !== cert.holder)
|
|
88
|
+
reasons.push("audit attestation is for a different agent");
|
|
89
|
+
if (ap.name !== ca.name || ap.country !== ca.country)
|
|
90
|
+
reasons.push("audit attestation is for a different applicant");
|
|
91
|
+
if (claim.audit !== jcsHash(a))
|
|
92
|
+
reasons.push("certificate does not reference this audit attestation");
|
|
93
|
+
if (ms(a.audited_at) > ms(cert.issued_at))
|
|
94
|
+
reasons.push("audit is dated after the certificate");
|
|
95
|
+
}
|
|
96
|
+
return { ok: reasons.length === 0, reasons };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Section 3: has the audit attestation of a Level 3 certificate lapsed at `now`? Surveillance then
|
|
100
|
+
* suspends the certificate with reason AUDIT_EXPIRED. False below Level 3; throws when `audit` is
|
|
101
|
+
* not the attestation the certificate references.
|
|
102
|
+
*/
|
|
103
|
+
export function auditLapsed(certificate, audit, now) {
|
|
104
|
+
if (!isIso(now))
|
|
105
|
+
throw new Error("now must be an ISO timestamp");
|
|
106
|
+
const claim = isObj(certificate?.claim) ? certificate.claim : {};
|
|
107
|
+
if (claim.level !== 3)
|
|
108
|
+
return false;
|
|
109
|
+
if (claim.audit !== jcsHash(audit))
|
|
110
|
+
throw new Error("the certificate does not reference this audit attestation");
|
|
111
|
+
return !(ms(audit.valid_until) >= ms(now));
|
|
112
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Credential } from "@zanii/credentials";
|
|
2
|
+
import type { Fingerprint } from "./fingerprint.ts";
|
|
3
|
+
import type { Signer } from "./signer.ts";
|
|
4
|
+
export declare const BEACON_INTERVAL_MS = 300000;
|
|
5
|
+
type FingerprintValue = string | Fingerprint;
|
|
6
|
+
export interface BeaconOptions {
|
|
7
|
+
/** The lab origin, for example `https://lab.zanii.agency`; reports go to `/v1/beacon`. */
|
|
8
|
+
url: string;
|
|
9
|
+
/** The certificate, or its certificate hash. */
|
|
10
|
+
certificate: Credential | string;
|
|
11
|
+
/** Holds the agent's key (the certificate holder). */
|
|
12
|
+
signer: Signer;
|
|
13
|
+
/** The fingerprint the agent runs now, or a function returning it (read at every report). */
|
|
14
|
+
fingerprint: FingerprintValue | (() => FingerprintValue | Promise<FingerprintValue>);
|
|
15
|
+
/** Default 300 000 (five minutes); at least 1 000. */
|
|
16
|
+
intervalMs?: number;
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
/** Called with every result, including failures; the timer never throws. */
|
|
19
|
+
onResult?: (result: BeaconResult) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface BeaconResult {
|
|
22
|
+
/** True when the lab accepted the report (202). */
|
|
23
|
+
ok: boolean;
|
|
24
|
+
/** The HTTP status; 0 when the request did not complete. */
|
|
25
|
+
status: number;
|
|
26
|
+
/** The lab's JSON reply (`{accepted, match, action}` or `{error, code}`), or null. */
|
|
27
|
+
body: unknown;
|
|
28
|
+
/** Set when the request did not complete or the report could not be built. */
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface BeaconHandle {
|
|
32
|
+
/** Send one report now. */
|
|
33
|
+
send: () => Promise<BeaconResult>;
|
|
34
|
+
stop: () => void;
|
|
35
|
+
}
|
|
36
|
+
/** Start reporting: one report now, then one every `intervalMs`. */
|
|
37
|
+
export declare function startBeacon(opts: BeaconOptions): BeaconHandle;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// spec/beacon.md section 4: the client a certified agent runs in production. It signs a beacon
|
|
2
|
+
// report with the agent's key and posts it to the lab (api.md 4.3), now and then every interval.
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { credentialHash } from "@zanii/credentials";
|
|
5
|
+
import { buildBeacon } from "./beacon.js";
|
|
6
|
+
import { isSha } from "./shared.js";
|
|
7
|
+
export const BEACON_INTERVAL_MS = 300_000;
|
|
8
|
+
const MIN_INTERVAL_MS = 1_000;
|
|
9
|
+
const TIMEOUT_MS = 10_000;
|
|
10
|
+
/** Start reporting: one report now, then one every `intervalMs`. */
|
|
11
|
+
export function startBeacon(opts) {
|
|
12
|
+
const endpoint = new URL("/v1/beacon", opts.url);
|
|
13
|
+
if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:")
|
|
14
|
+
throw new Error("url must be http(s)");
|
|
15
|
+
const certificate = typeof opts.certificate === "string" ? opts.certificate : credentialHash(opts.certificate);
|
|
16
|
+
if (!isSha(certificate))
|
|
17
|
+
throw new Error("certificate must be a certificate or its hash");
|
|
18
|
+
const intervalMs = opts.intervalMs ?? BEACON_INTERVAL_MS;
|
|
19
|
+
if (!Number.isInteger(intervalMs) || intervalMs < MIN_INTERVAL_MS)
|
|
20
|
+
throw new Error(`intervalMs must be an integer of at least ${MIN_INTERVAL_MS}`);
|
|
21
|
+
const doFetch = opts.fetch ?? fetch;
|
|
22
|
+
const send = async () => {
|
|
23
|
+
let result;
|
|
24
|
+
try {
|
|
25
|
+
const fp = typeof opts.fingerprint === "function" ? await opts.fingerprint() : opts.fingerprint;
|
|
26
|
+
const report = await buildBeacon({
|
|
27
|
+
certificate,
|
|
28
|
+
fingerprint: typeof fp === "string" ? fp : fp.id,
|
|
29
|
+
...(typeof fp === "string" ? {} : { fingerprintManifest: fp }),
|
|
30
|
+
observedAt: new Date().toISOString(),
|
|
31
|
+
nonce: randomBytes(18).toString("base64url"),
|
|
32
|
+
}, opts.signer);
|
|
33
|
+
const res = await doFetch(endpoint.href, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
36
|
+
body: JSON.stringify(report),
|
|
37
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
38
|
+
});
|
|
39
|
+
const body = await res.json().catch(() => null);
|
|
40
|
+
result = { ok: res.status === 202, status: res.status, body };
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
result = { ok: false, status: 0, body: null, error: e instanceof Error ? e.message : String(e) };
|
|
44
|
+
}
|
|
45
|
+
opts.onResult?.(result);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
void send();
|
|
49
|
+
const timer = setInterval(() => void send(), intervalMs);
|
|
50
|
+
timer.unref();
|
|
51
|
+
return { send, stop: () => clearInterval(timer) };
|
|
52
|
+
}
|
package/dist/beacon.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type QuoteVerifier } from "@zanii/attest";
|
|
2
|
+
import { type Credential } from "@zanii/credentials";
|
|
3
|
+
import { type Signer } from "./signer.ts";
|
|
4
|
+
import { type Fingerprint } from "./fingerprint.ts";
|
|
5
|
+
export declare const NONCE: RegExp;
|
|
6
|
+
/** Section 3: how far a report's observed_at may run ahead of the verifier's clock. */
|
|
7
|
+
export declare const MAX_SKEW_SECONDS = 300;
|
|
8
|
+
export interface BeaconReport {
|
|
9
|
+
v: 1;
|
|
10
|
+
type: "certify.beacon";
|
|
11
|
+
agent: string;
|
|
12
|
+
certificate: string;
|
|
13
|
+
fingerprint: string;
|
|
14
|
+
/** Optional original manifest; its recomputed id must equal fingerprint. */
|
|
15
|
+
fingerprint_manifest?: Fingerprint;
|
|
16
|
+
observed_at: string;
|
|
17
|
+
nonce: string;
|
|
18
|
+
/** Section 5: the platform's attestation of the code that produced this report. */
|
|
19
|
+
attestation?: BeaconAttestation;
|
|
20
|
+
signer: string;
|
|
21
|
+
sig: string;
|
|
22
|
+
}
|
|
23
|
+
/** Section 5: a @zanii/attest Attestation in its wire form (attestationHash's field names). */
|
|
24
|
+
export interface BeaconAttestation {
|
|
25
|
+
v: 1;
|
|
26
|
+
/** `nitro`, `sgx`, `tdx`, `sev-snp`, `tpm`, ... the root of trust. */
|
|
27
|
+
platform: string;
|
|
28
|
+
/** The code measurement (PCR digest, MRENCLAVE, ...). */
|
|
29
|
+
measurement: string;
|
|
30
|
+
/** Hash of the raw quote, which the platform verifier checks. */
|
|
31
|
+
quote_hash: string;
|
|
32
|
+
/** `beaconChallenge` of this report: the report data the quote binds. */
|
|
33
|
+
nonce: string;
|
|
34
|
+
attested_at: string;
|
|
35
|
+
}
|
|
36
|
+
/** Section 5: the challenge an attested report's quote binds: the report's own fields, hashed. */
|
|
37
|
+
export declare const beaconChallenge: (f: {
|
|
38
|
+
certificate: string;
|
|
39
|
+
fingerprint: string;
|
|
40
|
+
observedAt: string;
|
|
41
|
+
nonce: string;
|
|
42
|
+
}) => string;
|
|
43
|
+
/** Sign a beacon report as the agent (`signer` holds the agent's key). */
|
|
44
|
+
export declare function buildBeacon(fields: {
|
|
45
|
+
certificate: string;
|
|
46
|
+
fingerprint: string;
|
|
47
|
+
observedAt: string;
|
|
48
|
+
nonce: string;
|
|
49
|
+
attestation?: BeaconAttestation;
|
|
50
|
+
fingerprintManifest?: Fingerprint;
|
|
51
|
+
}, signer: Signer): Promise<BeaconReport>;
|
|
52
|
+
export interface BeaconCheck {
|
|
53
|
+
/** The report is authentic, well formed, not from the future and (when given) for this certificate. */
|
|
54
|
+
ok: boolean;
|
|
55
|
+
reasons: string[];
|
|
56
|
+
/** Whether the reported fingerprint is the certified one; null without a certificate. */
|
|
57
|
+
match: boolean | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Verify a beacon report. With `certificate`, also check it names that certificate and its holder
|
|
61
|
+
* and say whether the fingerprint matches. `maxAgeSeconds` refuses old reports (replay window).
|
|
62
|
+
*/
|
|
63
|
+
export declare function verifyBeacon(report: unknown, opts?: {
|
|
64
|
+
certificate?: Credential;
|
|
65
|
+
now?: string;
|
|
66
|
+
maxAgeSeconds?: number;
|
|
67
|
+
}): BeaconCheck;
|
|
68
|
+
export interface AttestedBeaconCheck extends BeaconCheck {
|
|
69
|
+
/** true: the platform verifier verified the quote (and the measurement is listed); false: it
|
|
70
|
+
* refused it, or the measurement is not listed; null: no attestation or no verifier, NOT verified. */
|
|
71
|
+
attested: boolean | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Section 5: `verifyBeacon`, then the report's attestation through the injected platform
|
|
75
|
+
* verifier (@zanii/attest `verifyAttestation`). Honest by default: without `verifyQuote` the
|
|
76
|
+
* attestation is not verified (`attested: null`) and the report counts as self-reported.
|
|
77
|
+
*/
|
|
78
|
+
export declare function verifyAttestedBeacon(report: unknown, opts?: {
|
|
79
|
+
certificate?: Credential;
|
|
80
|
+
now?: string;
|
|
81
|
+
maxAgeSeconds?: number;
|
|
82
|
+
verifyQuote?: QuoteVerifier;
|
|
83
|
+
measurements?: readonly string[];
|
|
84
|
+
}): Promise<AttestedBeaconCheck>;
|
package/dist/beacon.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// spec/beacon.md: the production agent reports its current fingerprint, signed by its own DID.
|
|
2
|
+
import { verifyAttestation } from "@zanii/attest";
|
|
3
|
+
import { credentialHash } from "@zanii/credentials";
|
|
4
|
+
import { jcsHash, verifySignedObject } from "@zanii/core";
|
|
5
|
+
import { isDid, isIso, isObj, isSha, ms } from "./shared.js";
|
|
6
|
+
import { signerDid, signWith } from "./signer.js";
|
|
7
|
+
import { checkFingerprint } from "./fingerprint.js";
|
|
8
|
+
export const NONCE = /^[A-Za-z0-9_-]{16,128}$/;
|
|
9
|
+
/** Section 3: how far a report's observed_at may run ahead of the verifier's clock. */
|
|
10
|
+
export const MAX_SKEW_SECONDS = 300;
|
|
11
|
+
const PLATFORM = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
12
|
+
const MEASUREMENT = /^[A-Za-z0-9:._+/=-]{1,256}$/;
|
|
13
|
+
/** Section 5: the challenge an attested report's quote binds: the report's own fields, hashed. */
|
|
14
|
+
export const beaconChallenge = (f) => jcsHash({ v: 1, type: "certify.beacon-challenge", certificate: f.certificate, fingerprint: f.fingerprint, observed_at: f.observedAt, nonce: f.nonce });
|
|
15
|
+
function attestationOk(a, b) {
|
|
16
|
+
if (!isObj(a) || Object.keys(a).length !== 6 || a.v !== 1)
|
|
17
|
+
return false;
|
|
18
|
+
const challenge = beaconChallenge({ certificate: String(b.certificate), fingerprint: String(b.fingerprint), observedAt: String(b.observed_at), nonce: String(b.nonce) });
|
|
19
|
+
return (typeof a.platform === "string" && PLATFORM.test(a.platform) &&
|
|
20
|
+
typeof a.measurement === "string" && MEASUREMENT.test(a.measurement) &&
|
|
21
|
+
isSha(a.quote_hash) && a.nonce === challenge && isIso(a.attested_at) &&
|
|
22
|
+
Math.abs(ms(a.attested_at) - ms(b.observed_at)) <= MAX_SKEW_SECONDS * 1000);
|
|
23
|
+
}
|
|
24
|
+
/** Sign a beacon report as the agent (`signer` holds the agent's key). */
|
|
25
|
+
export async function buildBeacon(fields, signer) {
|
|
26
|
+
if (!isSha(fields.certificate))
|
|
27
|
+
throw new Error("certificate must be a credential hash");
|
|
28
|
+
if (!isSha(fields.fingerprint))
|
|
29
|
+
throw new Error("fingerprint must be a fingerprint id");
|
|
30
|
+
if (!isIso(fields.observedAt))
|
|
31
|
+
throw new Error("observedAt must be an ISO UTC timestamp");
|
|
32
|
+
if (!NONCE.test(fields.nonce))
|
|
33
|
+
throw new Error("nonce must be 16-128 of [A-Za-z0-9_-]");
|
|
34
|
+
if (fields.fingerprintManifest !== undefined && (!checkFingerprint(fields.fingerprintManifest) || fields.fingerprintManifest.id !== fields.fingerprint))
|
|
35
|
+
throw new Error("beacon fingerprint manifest is malformed or does not match the reported fingerprint");
|
|
36
|
+
const unsigned = {
|
|
37
|
+
v: 1,
|
|
38
|
+
type: "certify.beacon",
|
|
39
|
+
agent: signerDid(signer),
|
|
40
|
+
certificate: fields.certificate,
|
|
41
|
+
fingerprint: fields.fingerprint,
|
|
42
|
+
...(fields.fingerprintManifest !== undefined ? { fingerprint_manifest: { ...fields.fingerprintManifest } } : {}),
|
|
43
|
+
observed_at: fields.observedAt,
|
|
44
|
+
nonce: fields.nonce,
|
|
45
|
+
...(fields.attestation !== undefined ? { attestation: fields.attestation } : {}),
|
|
46
|
+
};
|
|
47
|
+
if (fields.attestation !== undefined && !attestationOk(fields.attestation, unsigned))
|
|
48
|
+
throw new Error("attestation is malformed or does not bind this report's beaconChallenge");
|
|
49
|
+
return signWith(unsigned, signer);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Verify a beacon report. With `certificate`, also check it names that certificate and its holder
|
|
53
|
+
* and say whether the fingerprint matches. `maxAgeSeconds` refuses old reports (replay window).
|
|
54
|
+
*/
|
|
55
|
+
export function verifyBeacon(report, opts = {}) {
|
|
56
|
+
const b = isObj(report) ? report : {};
|
|
57
|
+
if (b.v !== 1 || b.type !== "certify.beacon" || !isDid(b.agent) || Object.keys(b).length !== 9 + Number("attestation" in b) + Number("fingerprint_manifest" in b))
|
|
58
|
+
return { ok: false, reasons: ["not a beacon report (v1)"], match: null };
|
|
59
|
+
const reasons = [];
|
|
60
|
+
if (!verifySignedObject(b))
|
|
61
|
+
reasons.push("bad beacon signature");
|
|
62
|
+
if (b.signer !== b.agent)
|
|
63
|
+
reasons.push("beacon is not signed by its agent");
|
|
64
|
+
if (!isSha(b.certificate) || !isSha(b.fingerprint) || !isIso(b.observed_at) || typeof b.nonce !== "string" || !NONCE.test(b.nonce))
|
|
65
|
+
reasons.push("beacon has a malformed field");
|
|
66
|
+
if ("attestation" in b && !attestationOk(b.attestation, b))
|
|
67
|
+
reasons.push("beacon attestation is malformed or does not bind the report");
|
|
68
|
+
if ("fingerprint_manifest" in b && (!checkFingerprint(b.fingerprint_manifest) || b.fingerprint_manifest.id !== b.fingerprint))
|
|
69
|
+
reasons.push("beacon fingerprint manifest is malformed or does not match the reported fingerprint");
|
|
70
|
+
const now = opts.now ? ms(opts.now) : Date.now();
|
|
71
|
+
const at = ms(b.observed_at);
|
|
72
|
+
if (at > now + MAX_SKEW_SECONDS * 1000)
|
|
73
|
+
reasons.push("beacon observed_at is in the future");
|
|
74
|
+
if (opts.maxAgeSeconds !== undefined && at < now - opts.maxAgeSeconds * 1000)
|
|
75
|
+
reasons.push("beacon is older than the accepted window");
|
|
76
|
+
let match = null;
|
|
77
|
+
if (opts.certificate) {
|
|
78
|
+
if (b.certificate !== credentialHash(opts.certificate))
|
|
79
|
+
reasons.push("beacon names a different certificate");
|
|
80
|
+
if (b.agent !== opts.certificate.holder)
|
|
81
|
+
reasons.push("beacon agent is not the certificate holder");
|
|
82
|
+
match = b.fingerprint === opts.certificate.claim?.fingerprint;
|
|
83
|
+
}
|
|
84
|
+
return { ok: reasons.length === 0, reasons, match };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Section 5: `verifyBeacon`, then the report's attestation through the injected platform
|
|
88
|
+
* verifier (@zanii/attest `verifyAttestation`). Honest by default: without `verifyQuote` the
|
|
89
|
+
* attestation is not verified (`attested: null`) and the report counts as self-reported.
|
|
90
|
+
*/
|
|
91
|
+
export async function verifyAttestedBeacon(report, opts = {}) {
|
|
92
|
+
const base = verifyBeacon(report, opts);
|
|
93
|
+
const a = isObj(report) ? report.attestation : undefined;
|
|
94
|
+
if (!base.ok || !isObj(a) || !opts.verifyQuote)
|
|
95
|
+
return { ...base, attested: null };
|
|
96
|
+
const r = await verifyAttestation({ v: 1, platform: String(a.platform), measurement: String(a.measurement), quoteHash: String(a.quote_hash), nonce: String(a.nonce), attestedAt: String(a.attested_at) }, { verifyQuote: opts.verifyQuote });
|
|
97
|
+
const reasons = [...base.reasons];
|
|
98
|
+
let attested = r.quoteVerified === true;
|
|
99
|
+
if (!attested)
|
|
100
|
+
reasons.push("beacon attestation quote verification failed");
|
|
101
|
+
else if (opts.measurements && !opts.measurements.includes(String(a.measurement))) {
|
|
102
|
+
attested = false;
|
|
103
|
+
reasons.push("beacon attestation measurement is not a certified measurement");
|
|
104
|
+
}
|
|
105
|
+
return { ok: reasons.length === 0, reasons, match: base.match, attested };
|
|
106
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type Credential } from "@zanii/credentials";
|
|
2
|
+
import { type VerifiableCredential } from "@zanii/vc";
|
|
3
|
+
import { type Signer } from "./signer.ts";
|
|
4
|
+
import { type CertificateVcCheck } from "./vc.ts";
|
|
5
|
+
export declare const BITSTRING_STATUS_SLOTS = 131072;
|
|
6
|
+
export type BitstringPurpose = "revocation" | "suspension";
|
|
7
|
+
export interface BitstringStatusEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
type: "BitstringStatusListEntry";
|
|
10
|
+
statusPurpose: BitstringPurpose;
|
|
11
|
+
statusListIndex: string;
|
|
12
|
+
statusListCredential: string;
|
|
13
|
+
}
|
|
14
|
+
export interface BitstringStatusListInput {
|
|
15
|
+
id: string;
|
|
16
|
+
purpose: BitstringPurpose;
|
|
17
|
+
entries: readonly {
|
|
18
|
+
index: number;
|
|
19
|
+
value: boolean;
|
|
20
|
+
}[];
|
|
21
|
+
validFrom: string;
|
|
22
|
+
validUntil: string;
|
|
23
|
+
created: string;
|
|
24
|
+
previous?: VerifiableCredential;
|
|
25
|
+
}
|
|
26
|
+
export interface BitstringStatusTrust {
|
|
27
|
+
issuers: readonly string[];
|
|
28
|
+
now: string;
|
|
29
|
+
}
|
|
30
|
+
declare const entryShape: (s: unknown) => s is BitstringStatusEntry;
|
|
31
|
+
export { entryShape as isBitstringStatusEntry };
|
|
32
|
+
export declare function bitstringStatusEntry(input: {
|
|
33
|
+
index: number;
|
|
34
|
+
list: string;
|
|
35
|
+
purpose: BitstringPurpose;
|
|
36
|
+
}): BitstringStatusEntry;
|
|
37
|
+
export declare function buildBitstringStatusList(input: BitstringStatusListInput, signer: Signer): Promise<VerifiableCredential>;
|
|
38
|
+
export declare function verifyBitstringStatusList(doc: unknown, opts: BitstringStatusTrust): {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
reasons: string[];
|
|
41
|
+
bits: Uint8Array | null;
|
|
42
|
+
};
|
|
43
|
+
export declare function verifyBitstringStatusEntry(entry: unknown, doc: unknown, opts: BitstringStatusTrust): {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
reasons: string[];
|
|
46
|
+
value: boolean | null;
|
|
47
|
+
};
|
|
48
|
+
export declare function certificateToBitstringVc(cert: Credential, signer: Signer, opts: {
|
|
49
|
+
created: string;
|
|
50
|
+
index: number;
|
|
51
|
+
revocationList: string;
|
|
52
|
+
suspensionList: string;
|
|
53
|
+
}): Promise<VerifiableCredential>;
|
|
54
|
+
export declare function verifyCertificateBitstringVc(vc: unknown, opts?: {
|
|
55
|
+
at?: string;
|
|
56
|
+
}): CertificateVcCheck;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// W3C Bitstring Status List v1.0, fixed 131072-slot publishing profile. Offline status remains a snapshot.
|
|
2
|
+
import { gzipSync, gunzipSync } from "node:zlib";
|
|
3
|
+
import { publicKeyFromDid } from "@zanii/core";
|
|
4
|
+
import { credentialHash } from "@zanii/credentials";
|
|
5
|
+
import { VC_CONTEXT, verifyVC } from "@zanii/vc";
|
|
6
|
+
import { isInt, isIso, isObj, ms } from "./shared.js";
|
|
7
|
+
import { signerDid } from "./signer.js";
|
|
8
|
+
import { signVcWith } from "./vc.js";
|
|
9
|
+
export const BITSTRING_STATUS_SLOTS = 131_072;
|
|
10
|
+
const BYTES = BITSTRING_STATUS_SLOTS / 8;
|
|
11
|
+
const purpose = (p) => p === "revocation" || p === "suspension";
|
|
12
|
+
const web = (s) => {
|
|
13
|
+
if (typeof s !== "string" || s.length > 2048 || !/^[\x21-\x7e]+$/.test(s))
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
const u = new URL(s);
|
|
17
|
+
return u.protocol === "https:" && !u.username && !u.password && !s.includes("#") && s === u.href;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const decode = (s) => {
|
|
24
|
+
if (typeof s !== "string" || s.length > 32768 || !/^u[A-Za-z0-9_-]+$/.test(s) || s.trim() !== s)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const data = Buffer.from(s.slice(1), "base64url");
|
|
28
|
+
if (`u${data.toString("base64url")}` !== s)
|
|
29
|
+
return null;
|
|
30
|
+
const bits = gunzipSync(data, { maxOutputLength: BYTES });
|
|
31
|
+
return bits.length === BYTES ? bits : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const encode = (bits) => {
|
|
38
|
+
// shortcut: stored GZIP blocks fix bytes across zlib versions; use a canonical compressor if shard bandwidth requires it.
|
|
39
|
+
const gzip = gzipSync(bits, { level: 0 });
|
|
40
|
+
gzip[9] = 255; // Stable RFC1952 OS field across Node/Python; payload is ordinary GZIP.
|
|
41
|
+
return `u${gzip.toString("base64url")}`;
|
|
42
|
+
};
|
|
43
|
+
const entryShape = (s) => isObj(s) && Object.keys(s).length === 5 && s.type === "BitstringStatusListEntry" && purpose(s.statusPurpose)
|
|
44
|
+
&& web(s.statusListCredential) && typeof s.statusListIndex === "string" && /^(?:0|[1-9][0-9]{0,5})$/.test(s.statusListIndex) && s.statusListIndex.trim() === s.statusListIndex
|
|
45
|
+
&& Number(s.statusListIndex) < BITSTRING_STATUS_SLOTS && s.id === `${s.statusListCredential}#${s.statusListIndex}`;
|
|
46
|
+
export { entryShape as isBitstringStatusEntry };
|
|
47
|
+
const proofShape = (proof) => isObj(proof) && Object.keys(proof).length === 6 && proof.type === "DataIntegrityProof"
|
|
48
|
+
&& proof.cryptosuite === "eddsa-jcs-2022" && proof.proofPurpose === "assertionMethod" && isIso(proof.created)
|
|
49
|
+
&& typeof proof.verificationMethod === "string" && proof.verificationMethod.length <= 140
|
|
50
|
+
&& typeof proof.proofValue === "string" && proof.proofValue.length <= 100;
|
|
51
|
+
export function bitstringStatusEntry(input) {
|
|
52
|
+
if (!isInt(input.index, 0, BITSTRING_STATUS_SLOTS - 1) || !web(input.list) || !purpose(input.purpose))
|
|
53
|
+
throw new Error("invalid bitstring status entry");
|
|
54
|
+
return { id: `${input.list}#${input.index}`, type: "BitstringStatusListEntry", statusPurpose: input.purpose, statusListIndex: String(input.index), statusListCredential: input.list };
|
|
55
|
+
}
|
|
56
|
+
export async function buildBitstringStatusList(input, signer) {
|
|
57
|
+
if (!web(input.id) || !purpose(input.purpose) || !isIso(input.validFrom) || !isIso(input.validUntil) || !isIso(input.created)
|
|
58
|
+
|| ms(input.validUntil) <= ms(input.validFrom) || ms(input.validUntil) - ms(input.validFrom) > 86_400_000
|
|
59
|
+
|| !Array.isArray(input.entries) || input.entries.length > BITSTRING_STATUS_SLOTS)
|
|
60
|
+
throw new Error("invalid bitstring status list input");
|
|
61
|
+
const bits = new Uint8Array(BYTES), seen = new Set();
|
|
62
|
+
for (const e of input.entries) {
|
|
63
|
+
if (!isObj(e) || Object.keys(e).length !== 2 || !isInt(e.index, 0, BITSTRING_STATUS_SLOTS - 1) || typeof e.value !== "boolean" || seen.has(e.index))
|
|
64
|
+
throw new Error("invalid or duplicate bitstring status index");
|
|
65
|
+
seen.add(e.index);
|
|
66
|
+
if (e.value)
|
|
67
|
+
bits[Math.floor(e.index / 8)] |= 0x80 >> (e.index % 8);
|
|
68
|
+
}
|
|
69
|
+
if (input.previous !== undefined) {
|
|
70
|
+
const prior = input.previous, subject = isObj(prior?.credentialSubject) ? prior.credentialSubject : {};
|
|
71
|
+
const old = verifyBitstringStatusList(prior, { issuers: [signerDid(signer)], now: String(prior?.validFrom) }).bits;
|
|
72
|
+
if (prior?.id !== input.id || subject.statusPurpose !== input.purpose || !old
|
|
73
|
+
|| !isIso(prior.validFrom) || ms(prior.validFrom) > ms(input.validFrom))
|
|
74
|
+
throw new Error("invalid previous bitstring status list");
|
|
75
|
+
if (input.purpose === "revocation" && old.some((byte, i) => (byte & bits[i]) !== byte))
|
|
76
|
+
throw new Error("revocation bits cannot be cleared");
|
|
77
|
+
}
|
|
78
|
+
return signVcWith({ "@context": [VC_CONTEXT], id: input.id, type: ["VerifiableCredential", "BitstringStatusListCredential"], issuer: signerDid(signer),
|
|
79
|
+
validFrom: input.validFrom, validUntil: input.validUntil,
|
|
80
|
+
credentialSubject: { id: `${input.id}#list`, type: "BitstringStatusList", statusPurpose: input.purpose, encodedList: encode(bits) } }, signer, input.created);
|
|
81
|
+
}
|
|
82
|
+
export function verifyBitstringStatusList(doc, opts) {
|
|
83
|
+
const denied = (reason) => ({ ok: false, reasons: [reason], bits: null });
|
|
84
|
+
if (!isObj(opts) || !Array.isArray(opts.issuers) || opts.issuers.length < 1 || opts.issuers.length > 64 || !Array.from(opts.issuers).every((d) => typeof d === "string" && d.length <= 69 && publicKeyFromDid(d)) || !isIso(opts.now))
|
|
85
|
+
return denied("explicit trusted bitstring issuers and reference time are required");
|
|
86
|
+
if (!isObj(doc) || Object.keys(doc).length !== 8 || !web(doc.id) || !opts.issuers.includes(String(doc.issuer)) || !isIso(doc.validFrom) || !isIso(doc.validUntil)
|
|
87
|
+
|| ms(doc.validFrom) > ms(opts.now) || ms(doc.validUntil) <= ms(opts.now) || ms(doc.validUntil) - ms(doc.validFrom) > 86_400_000
|
|
88
|
+
|| !Array.isArray(doc.type) || doc.type.length !== 2 || doc.type[0] !== "VerifiableCredential" || doc.type[1] !== "BitstringStatusListCredential"
|
|
89
|
+
|| !Array.isArray(doc["@context"]) || doc["@context"].length !== 1 || doc["@context"][0] !== VC_CONTEXT || !proofShape(doc.proof))
|
|
90
|
+
return denied("invalid, untrusted or stale bitstring status list");
|
|
91
|
+
const subject = doc.credentialSubject;
|
|
92
|
+
if (!isObj(subject) || Object.keys(subject).length !== 4 || subject.id !== `${doc.id}#list` || subject.type !== "BitstringStatusList" || !purpose(subject.statusPurpose))
|
|
93
|
+
return denied("invalid bitstring status subject");
|
|
94
|
+
const bits = decode(subject.encodedList);
|
|
95
|
+
if (!bits)
|
|
96
|
+
return denied("invalid or oversized bitstring encoding");
|
|
97
|
+
const check = verifyVC(doc, { at: opts.now });
|
|
98
|
+
return check.ok ? { ok: true, reasons: [], bits } : { ok: false, reasons: check.reasons, bits: null };
|
|
99
|
+
}
|
|
100
|
+
export function verifyBitstringStatusEntry(entry, doc, opts) {
|
|
101
|
+
if (!entryShape(entry))
|
|
102
|
+
return { ok: false, reasons: ["invalid bitstring status entry"], value: null };
|
|
103
|
+
const checked = verifyBitstringStatusList(doc, opts);
|
|
104
|
+
if (!checked.ok || !checked.bits)
|
|
105
|
+
return { ok: false, reasons: checked.reasons, value: null };
|
|
106
|
+
const list = doc;
|
|
107
|
+
if (list.id !== entry.statusListCredential || list.credentialSubject.statusPurpose !== entry.statusPurpose)
|
|
108
|
+
return { ok: false, reasons: ["bitstring list URL or purpose does not match the entry"], value: null };
|
|
109
|
+
const index = Number(entry.statusListIndex);
|
|
110
|
+
return { ok: true, reasons: [], value: !!(checked.bits[Math.floor(index / 8)] & (0x80 >> (index % 8))) };
|
|
111
|
+
}
|
|
112
|
+
export async function certificateToBitstringVc(cert, signer, opts) {
|
|
113
|
+
if (cert?.v !== 1 || cert.type !== "credential" || cert.issuer !== signerDid(signer) || !isIso(opts.created) || opts.revocationList === opts.suspensionList)
|
|
114
|
+
throw new Error("invalid certificate or bitstring VC signer/options");
|
|
115
|
+
return signVcWith({ "@context": [VC_CONTEXT], type: ["VerifiableCredential", "ZaniiCertifyCertificateV2"], issuer: cert.issuer,
|
|
116
|
+
validFrom: cert.issued_at, validUntil: cert.exp,
|
|
117
|
+
credentialSubject: { id: cert.holder, scope: cert.scope, claim: cert.claim, issuerDomain: cert.issuer_domain, certificate: credentialHash(cert), sig: cert.sig },
|
|
118
|
+
credentialStatus: [bitstringStatusEntry({ index: opts.index, list: opts.revocationList, purpose: "revocation" }), bitstringStatusEntry({ index: opts.index, list: opts.suspensionList, purpose: "suspension" })] }, signer, opts.created);
|
|
119
|
+
}
|
|
120
|
+
export function verifyCertificateBitstringVc(vc, opts = {}) {
|
|
121
|
+
if (!isObj(vc) || !proofShape(vc.proof))
|
|
122
|
+
return { ok: false, reasons: ["not a certificate bitstring VC v2 proof"], certificate: null };
|
|
123
|
+
const checked = verifyVC(vc, opts.at === undefined ? {} : { at: opts.at });
|
|
124
|
+
if (!checked.ok)
|
|
125
|
+
return { ok: false, reasons: checked.reasons, certificate: null };
|
|
126
|
+
const doc = vc, s = doc.credentialSubject;
|
|
127
|
+
if (!doc.type.includes("ZaniiCertifyCertificateV2") || !isObj(s) || !["id", "scope", "claim", "issuerDomain", "sig", "certificate"].every((k) => Object.hasOwn(s, k)))
|
|
128
|
+
return { ok: false, reasons: ["not a complete certificate bitstring VC v2"], certificate: null };
|
|
129
|
+
const cert = { v: 1, type: "credential", issuer: doc.issuer, issuer_domain: s.issuerDomain, holder: s.id, scope: s.scope, claim: s.claim, issued_at: doc.validFrom, exp: doc.validUntil, sig: s.sig };
|
|
130
|
+
const statuses = doc.credentialStatus;
|
|
131
|
+
if (credentialHash(cert) !== s.certificate || !Array.isArray(statuses) || statuses.length !== 2 || !Array.from(statuses).every(entryShape)
|
|
132
|
+
|| statuses[0].statusPurpose !== "revocation" || statuses[1].statusPurpose !== "suspension" || statuses[0].statusListIndex !== statuses[1].statusListIndex || statuses[0].statusListCredential === statuses[1].statusListCredential)
|
|
133
|
+
return { ok: false, reasons: ["bitstring VC certificate or status bindings are invalid"], certificate: null };
|
|
134
|
+
return { ok: true, reasons: [], certificate: cert };
|
|
135
|
+
}
|