@colophon-claims/verify 0.0.0 → 0.2.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/README.md +39 -0
- package/dist/admission/application.d.ts +18 -0
- package/dist/admission/application.js +53 -0
- package/dist/admission/contracts.d.ts +506 -0
- package/dist/admission/contracts.js +459 -0
- package/dist/admission/index.d.ts +5 -0
- package/dist/admission/index.js +5 -0
- package/dist/admission/intake.d.ts +75 -0
- package/dist/admission/intake.js +70 -0
- package/dist/admission/result-evaluation.d.ts +18 -0
- package/dist/admission/result-evaluation.js +71 -0
- package/dist/admission/screening-sample.d.ts +56 -0
- package/dist/admission/screening-sample.js +126 -0
- package/dist/admission/verification.d.ts +102 -0
- package/dist/admission/verification.js +844 -0
- package/dist/anchor/check.d.ts +95 -0
- package/dist/anchor/check.js +146 -0
- package/dist/anchor/ports.d.ts +6 -0
- package/dist/anchor/ports.js +354 -0
- package/dist/assets.d.ts +33 -0
- package/dist/assets.js +885 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +20 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +205 -0
- package/dist/comparison.d.ts +64 -0
- package/dist/comparison.js +235 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/manifest.d.ts +54 -0
- package/dist/manifest.js +240 -0
- package/dist/materialize.d.ts +4 -0
- package/dist/materialize.js +13 -0
- package/dist/profile/admission-receipts.d.ts +11 -0
- package/dist/profile/admission-receipts.js +24 -0
- package/dist/profile/anchor-claims.d.ts +157 -0
- package/dist/profile/anchor-claims.js +330 -0
- package/dist/profile/artifacts.d.ts +261 -0
- package/dist/profile/artifacts.js +139 -0
- package/dist/profile/assembly-ports.d.ts +19 -0
- package/dist/profile/assembly-ports.js +42 -0
- package/dist/profile/binary-judge-manifest.d.ts +121 -0
- package/dist/profile/binary-judge-manifest.js +106 -0
- package/dist/profile/binary-qualification.d.ts +9 -0
- package/dist/profile/binary-qualification.js +58 -0
- package/dist/profile/branding-assets.d.ts +5 -0
- package/dist/profile/branding-assets.js +5 -0
- package/dist/profile/branding.d.ts +24 -0
- package/dist/profile/branding.js +14 -0
- package/dist/profile/claim-consistency.d.ts +26 -0
- package/dist/profile/claim-consistency.js +91 -0
- package/dist/profile/claim.d.ts +327 -0
- package/dist/profile/claim.js +742 -0
- package/dist/profile/errors.d.ts +49 -0
- package/dist/profile/errors.js +78 -0
- package/dist/profile/inspect-assurance.d.ts +16 -0
- package/dist/profile/inspect-assurance.js +30 -0
- package/dist/profile/inspect-disclosure.d.ts +28 -0
- package/dist/profile/inspect-disclosure.js +54 -0
- package/dist/profile/inspect-manifest.d.ts +706 -0
- package/dist/profile/inspect-manifest.js +397 -0
- package/dist/profile/isolation.d.ts +21 -0
- package/dist/profile/isolation.js +31 -0
- package/dist/profile/pinning-evidence.d.ts +41 -0
- package/dist/profile/pinning-evidence.js +43 -0
- package/dist/profile/ports.d.ts +3 -0
- package/dist/profile/ports.js +11 -0
- package/dist/profile/preview-log.d.ts +4 -0
- package/dist/profile/preview-log.js +3 -0
- package/dist/profile/run-results.d.ts +17 -0
- package/dist/profile/run-results.js +23 -0
- package/dist/profile/signing.d.ts +10 -0
- package/dist/profile/signing.js +46 -0
- package/dist/profile/trust.d.ts +13 -0
- package/dist/profile/trust.js +35 -0
- package/dist/profile/venue.d.ts +2 -0
- package/dist/profile/venue.js +2 -0
- package/dist/profile/verdict.d.ts +28 -0
- package/dist/profile/verdict.js +51 -0
- package/dist/reader-instructions.d.ts +41 -0
- package/dist/reader-instructions.js +52 -0
- package/dist/schema.d.ts +380 -0
- package/dist/schema.js +498 -0
- package/dist/verify.d.ts +72 -0
- package/dist/verify.js +1573 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +63 -3
- package/schemas/assembly-row.schema.json +194 -0
- package/schemas/bundle-manifest.schema.json +23 -0
- package/schemas/claim-package.schema.json +279 -0
- package/schemas/dsse-envelope.schema.json +25 -0
- package/schemas/evidence-catalog.schema.json +43 -0
- package/schemas/public-trust.schema.json +44 -0
- package/schemas/verdict-catalog.schema.json +27 -0
- package/scripts/external-verify.py +238 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `integrity-anchors` check (anchor-evidence design §8): the one shared implementation both
|
|
3
|
+
* `bundle verify` and the workspace-side `run.verify` report through.
|
|
4
|
+
*
|
|
5
|
+
* It never throws. Every carried anchor gets an outcome, because the check's whole purpose is to
|
|
6
|
+
* report on all of them — one unreadable proof must not take the report down with it, and a caller
|
|
7
|
+
* that wants to refuse decides that from the returned statuses rather than from an exception that
|
|
8
|
+
* escaped mid-walk. The callers refuse `record-integrity` on any `invalid`, which is the design's
|
|
9
|
+
* "fails the whole verification loudly".
|
|
10
|
+
*
|
|
11
|
+
* Four rules do the work, and each is a different attack:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Exact-bytes parse, strict schema.** Unknown keys fail closed — the public-bundle closure
|
|
14
|
+
* discipline, deliberately stricter than the protocol layer's unknown-field tolerance.
|
|
15
|
+
* 2. **The subject is recomputed, never read.** The digest comes from the authenticated snapshot's
|
|
16
|
+
* own `run.json` / `matrix.json` bytes; a stored assertion is never the comparison source.
|
|
17
|
+
* Selectors are digest-keyed, so a `subject.kind` label can never route an anchor onto a claim
|
|
18
|
+
* its digest does not back — the kind is then *checked* for equality, and a mismatch is
|
|
19
|
+
* `invalid`.
|
|
20
|
+
* 3. **Trust material is verifier-side.** None ships. Absent roots or headers yield `present`, not
|
|
21
|
+
* `invalid`: an operator's incomplete root set is not an accusation against the proof, and
|
|
22
|
+
* authority acceptability is consumer policy. Bundle-carried certificate chains are archival and
|
|
23
|
+
* are never used to validate.
|
|
24
|
+
* 4. **The splice-catch.** An `authority-time` lock anchor must satisfy `genTime <= run.closeAt`.
|
|
25
|
+
* It reads `facts.genTime`, which both `verified` and `present` carry — routing it through the
|
|
26
|
+
* evaluated `time` instead would silently disable it in the default no-roots configuration,
|
|
27
|
+
* which is the configuration nearly every reader is in. It catches an anchor spliced in after
|
|
28
|
+
* the run's own pre-registered close instant, and nothing more; it is not ordering evidence and
|
|
29
|
+
* is never described as such. Chain-time proofs carry no time, so the rule does not reach them.
|
|
30
|
+
*
|
|
31
|
+
* Absence is an outcome, not a silence: a subject with no carried anchor reports `absent`, and one
|
|
32
|
+
* whose sealed Run declared anchoring intent that the bundle does not satisfy reports
|
|
33
|
+
* `declared-but-absent`. Both pass. A stripped anchor cannot masquerade as never-attempted.
|
|
34
|
+
*/
|
|
35
|
+
import type { AnchorProofResult, AnchorTimeBasis, OpenTimestampsTrustMaterial, Rfc3161AnchorTrustMaterial } from "@jinn-network/trust-core";
|
|
36
|
+
import type { CarriedAnchorRecord, ClaimAnchorSubject } from "../profile/anchor-claims.js";
|
|
37
|
+
/**
|
|
38
|
+
* Verifier-side trust material, keyed by provider profile. Strictly the verifier operator's own
|
|
39
|
+
* configuration: this package ships none, and an empty set can never yield `verified` (§8 step 3).
|
|
40
|
+
*/
|
|
41
|
+
export interface PublicBundleAnchorTrustMaterial {
|
|
42
|
+
readonly rfc3161?: Rfc3161AnchorTrustMaterial;
|
|
43
|
+
readonly opentimestamps?: OpenTimestampsTrustMaterial;
|
|
44
|
+
}
|
|
45
|
+
/** §4.3's four proof outcomes. `absent` and `declared-but-absent` are per-subject context outcomes
|
|
46
|
+
* this check owns; a proof verifier never reports them. */
|
|
47
|
+
export type AnchorProofStatus = AnchorProofResult["status"];
|
|
48
|
+
/** §7.3's absence semantics, plus the positive case. */
|
|
49
|
+
export type AnchorSubjectOutcome = "anchored" | "absent" | "declared-but-absent";
|
|
50
|
+
export interface AnchorVerificationEntry {
|
|
51
|
+
readonly recordSha256: string;
|
|
52
|
+
readonly status: AnchorProofStatus;
|
|
53
|
+
/** The provider profile the record names. Absent only when the record did not parse at all. */
|
|
54
|
+
readonly provider?: string;
|
|
55
|
+
/** The subject the record's digest resolves to. Absent on a dangling anchor by definition. */
|
|
56
|
+
readonly subject?: ClaimAnchorSubject;
|
|
57
|
+
readonly timeBasis?: AnchorTimeBasis;
|
|
58
|
+
/** The evaluated instant, present only on `verified` — the one status where a time basis was
|
|
59
|
+
* actually evaluated against supplied material. */
|
|
60
|
+
readonly time?: string;
|
|
61
|
+
/** The profile's extracted byte-facts, reported for `verified` and `present`. */
|
|
62
|
+
readonly facts?: unknown;
|
|
63
|
+
/** Why the proof is `pending` or `invalid`. */
|
|
64
|
+
readonly reason?: string;
|
|
65
|
+
/** The verifier's own evaluation disclosure: whether this reader had material for this profile at
|
|
66
|
+
* all. It is never a fact about the bundle. */
|
|
67
|
+
readonly trustMaterial: "supplied" | "none";
|
|
68
|
+
}
|
|
69
|
+
export interface AnchorSubjectReport {
|
|
70
|
+
readonly subject: ClaimAnchorSubject;
|
|
71
|
+
readonly outcome: AnchorSubjectOutcome;
|
|
72
|
+
/** The declared provider profiles this bundle carries no matching anchor for. Present only on
|
|
73
|
+
* `declared-but-absent`. */
|
|
74
|
+
readonly declaredProfiles?: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
export interface IntegrityAnchorsReport {
|
|
77
|
+
/** One entry per carried record, in record-digest order. */
|
|
78
|
+
readonly anchors: readonly AnchorVerificationEntry[];
|
|
79
|
+
readonly subjects: readonly AnchorSubjectReport[];
|
|
80
|
+
/** The subset the caller must refuse on. Empty means the check passes. */
|
|
81
|
+
readonly invalid: readonly AnchorVerificationEntry[];
|
|
82
|
+
}
|
|
83
|
+
export interface EvaluateIntegrityAnchorsInput {
|
|
84
|
+
readonly records: readonly CarriedAnchorRecord[];
|
|
85
|
+
/** sha256 hex of the authenticated `run.json` bytes. */
|
|
86
|
+
readonly runSha256: string;
|
|
87
|
+
/** sha256 hex of the authenticated `matrix.json` bytes. */
|
|
88
|
+
readonly matrixSha256: string;
|
|
89
|
+
/** The sealed Run's own `closeAt`, for the §8 step-4 splice-catch. */
|
|
90
|
+
readonly closeAt: string;
|
|
91
|
+
/** Provider profiles the sealed Run's `anchor-intent/v1` extension declares (§7.3). */
|
|
92
|
+
readonly declaredProfiles?: readonly string[];
|
|
93
|
+
readonly trust?: PublicBundleAnchorTrustMaterial;
|
|
94
|
+
}
|
|
95
|
+
export declare function evaluateIntegrityAnchors(input: EvaluateIntegrityAnchorsInput): IntegrityAnchorsReport;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `integrity-anchors` check (anchor-evidence design §8): the one shared implementation both
|
|
3
|
+
* `bundle verify` and the workspace-side `run.verify` report through.
|
|
4
|
+
*
|
|
5
|
+
* It never throws. Every carried anchor gets an outcome, because the check's whole purpose is to
|
|
6
|
+
* report on all of them — one unreadable proof must not take the report down with it, and a caller
|
|
7
|
+
* that wants to refuse decides that from the returned statuses rather than from an exception that
|
|
8
|
+
* escaped mid-walk. The callers refuse `record-integrity` on any `invalid`, which is the design's
|
|
9
|
+
* "fails the whole verification loudly".
|
|
10
|
+
*
|
|
11
|
+
* Four rules do the work, and each is a different attack:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Exact-bytes parse, strict schema.** Unknown keys fail closed — the public-bundle closure
|
|
14
|
+
* discipline, deliberately stricter than the protocol layer's unknown-field tolerance.
|
|
15
|
+
* 2. **The subject is recomputed, never read.** The digest comes from the authenticated snapshot's
|
|
16
|
+
* own `run.json` / `matrix.json` bytes; a stored assertion is never the comparison source.
|
|
17
|
+
* Selectors are digest-keyed, so a `subject.kind` label can never route an anchor onto a claim
|
|
18
|
+
* its digest does not back — the kind is then *checked* for equality, and a mismatch is
|
|
19
|
+
* `invalid`.
|
|
20
|
+
* 3. **Trust material is verifier-side.** None ships. Absent roots or headers yield `present`, not
|
|
21
|
+
* `invalid`: an operator's incomplete root set is not an accusation against the proof, and
|
|
22
|
+
* authority acceptability is consumer policy. Bundle-carried certificate chains are archival and
|
|
23
|
+
* are never used to validate.
|
|
24
|
+
* 4. **The splice-catch.** An `authority-time` lock anchor must satisfy `genTime <= run.closeAt`.
|
|
25
|
+
* It reads `facts.genTime`, which both `verified` and `present` carry — routing it through the
|
|
26
|
+
* evaluated `time` instead would silently disable it in the default no-roots configuration,
|
|
27
|
+
* which is the configuration nearly every reader is in. It catches an anchor spliced in after
|
|
28
|
+
* the run's own pre-registered close instant, and nothing more; it is not ordering evidence and
|
|
29
|
+
* is never described as such. Chain-time proofs carry no time, so the rule does not reach them.
|
|
30
|
+
*
|
|
31
|
+
* Absence is an outcome, not a silence: a subject with no carried anchor reports `absent`, and one
|
|
32
|
+
* whose sealed Run declared anchoring intent that the bundle does not satisfy reports
|
|
33
|
+
* `declared-but-absent`. Both pass. A stripped anchor cannot masquerade as never-attempted.
|
|
34
|
+
*/
|
|
35
|
+
import { MATRIX_RECORD_KIND, RUN_RECORD_KIND, compareCalendarStrictRfc3339Instants, } from "@jinn-network/benchmarking-records";
|
|
36
|
+
import { OPENTIMESTAMPS_ANCHOR_PROFILE, RFC3161_TSA_ANCHOR_PROFILE, createOpenTimestampsProofVerifier, createRfc3161AnchorProofVerifier, decodeAnchorProofContent, parseExactAnchorEvidence, } from "@jinn-network/trust-core";
|
|
37
|
+
import { nodeCryptoAnchorPorts } from "./ports.js";
|
|
38
|
+
const SUBJECT_KINDS = new Map([
|
|
39
|
+
["lock", RUN_RECORD_KIND],
|
|
40
|
+
["matrix", MATRIX_RECORD_KIND],
|
|
41
|
+
]);
|
|
42
|
+
function compareCodeUnits(left, right) {
|
|
43
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
function invalidEntry(recordSha256, reason, extra = {}) {
|
|
46
|
+
return { recordSha256, status: "invalid", reason, trustMaterial: "none", ...extra };
|
|
47
|
+
}
|
|
48
|
+
export function evaluateIntegrityAnchors(input) {
|
|
49
|
+
const subjectDigests = [
|
|
50
|
+
["lock", input.runSha256],
|
|
51
|
+
["matrix", input.matrixSha256],
|
|
52
|
+
];
|
|
53
|
+
const declared = input.declaredProfiles ?? [];
|
|
54
|
+
const anchors = [...input.records]
|
|
55
|
+
.sort((left, right) => compareCodeUnits(left.recordSha256, right.recordSha256))
|
|
56
|
+
.map((carried) => {
|
|
57
|
+
// 1. Exact-bytes parse under the strict schema, and the sanctioned proof decoder.
|
|
58
|
+
let record;
|
|
59
|
+
let proofBytes;
|
|
60
|
+
try {
|
|
61
|
+
record = parseExactAnchorEvidence(carried.bytes);
|
|
62
|
+
proofBytes = decodeAnchorProofContent(record.proof.content);
|
|
63
|
+
}
|
|
64
|
+
catch (cause) {
|
|
65
|
+
return invalidEntry(carried.recordSha256, `not the exact sealed encoding of a conforming AnchorEvidence record: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
66
|
+
}
|
|
67
|
+
// 2. Recompute the subject from the authenticated snapshot, then check the kind label.
|
|
68
|
+
const subjectDigest = record.subject.digest.sha256;
|
|
69
|
+
const resolved = subjectDigests.find(([, digest]) => digest === subjectDigest);
|
|
70
|
+
if (resolved === undefined) {
|
|
71
|
+
return invalidEntry(carried.recordSha256, `the anchored digest ${subjectDigest} is neither this bundle's sealed Run nor its sealed Matrix`, { provider: record.provider });
|
|
72
|
+
}
|
|
73
|
+
const [subject] = resolved;
|
|
74
|
+
const resolvedKind = SUBJECT_KINDS.get(subject);
|
|
75
|
+
if (record.subject.kind !== resolvedKind) {
|
|
76
|
+
return invalidEntry(carried.recordSha256, `subject.kind is ${record.subject.kind}, but the record its digest resolves to is ${resolvedKind}`, { provider: record.provider, subject });
|
|
77
|
+
}
|
|
78
|
+
// 3. Verify through the provider profile, with whatever material this reader configured.
|
|
79
|
+
const rfc3161 = record.provider === RFC3161_TSA_ANCHOR_PROFILE;
|
|
80
|
+
const openTimestamps = record.provider === OPENTIMESTAMPS_ANCHOR_PROFILE;
|
|
81
|
+
if (!rfc3161 && !openTimestamps) {
|
|
82
|
+
return invalidEntry(carried.recordSha256, `no proof verifier implements the anchor profile ${record.provider}`, { provider: record.provider, subject });
|
|
83
|
+
}
|
|
84
|
+
const material = rfc3161 ? input.trust?.rfc3161 : input.trust?.opentimestamps;
|
|
85
|
+
const result = rfc3161
|
|
86
|
+
? createRfc3161AnchorProofVerifier(nodeCryptoAnchorPorts).verifyProof({
|
|
87
|
+
subjectSha256: subjectDigest,
|
|
88
|
+
proofBytes,
|
|
89
|
+
...(input.trust?.rfc3161 === undefined ? {} : { trust: input.trust.rfc3161 }),
|
|
90
|
+
})
|
|
91
|
+
: createOpenTimestampsProofVerifier().verifyProof({
|
|
92
|
+
subjectSha256: subjectDigest,
|
|
93
|
+
proofBytes,
|
|
94
|
+
...(input.trust?.opentimestamps === undefined ? {} : { trust: input.trust.opentimestamps }),
|
|
95
|
+
});
|
|
96
|
+
const trustMaterial = material === undefined ? "none" : "supplied";
|
|
97
|
+
const base = {
|
|
98
|
+
recordSha256: carried.recordSha256,
|
|
99
|
+
provider: record.provider,
|
|
100
|
+
subject,
|
|
101
|
+
trustMaterial,
|
|
102
|
+
};
|
|
103
|
+
if (result.status === "invalid") {
|
|
104
|
+
return { ...base, status: "invalid", reason: result.reason };
|
|
105
|
+
}
|
|
106
|
+
if (result.status === "pending") {
|
|
107
|
+
return { ...base, status: "pending", timeBasis: result.timeBasis, reason: result.reason };
|
|
108
|
+
}
|
|
109
|
+
// Exhaustive over the four proof statuses: `verified` and `present` are what remains, and the
|
|
110
|
+
// rest of this walk reads the `facts` only they carry. A fifth member of ANCHOR_PROOF_STATUSES
|
|
111
|
+
// fails here rather than arriving at the splice-catch as an unhandled shape.
|
|
112
|
+
result.status;
|
|
113
|
+
// 4. The splice-catch, on `facts.genTime` so it survives the default no-roots configuration.
|
|
114
|
+
if (subject === "lock" && result.timeBasis === "authority-time") {
|
|
115
|
+
const genTime = result.facts.genTime;
|
|
116
|
+
const compared = typeof genTime === "string"
|
|
117
|
+
? compareCalendarStrictRfc3339Instants(genTime, input.closeAt)
|
|
118
|
+
: undefined;
|
|
119
|
+
if (compared === undefined || compared > 0) {
|
|
120
|
+
return invalidEntry(carried.recordSha256, typeof genTime === "string"
|
|
121
|
+
? `the token's genTime ${genTime} is after this run's own pre-registered close instant ${input.closeAt}`
|
|
122
|
+
: "an authority-time lock anchor carries no comparable genTime", { provider: record.provider, subject });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
status: result.status,
|
|
128
|
+
timeBasis: result.timeBasis,
|
|
129
|
+
...(result.status === "verified" ? { time: result.time } : {}),
|
|
130
|
+
facts: result.facts,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
const subjects = subjectDigests.map(([subject]) => {
|
|
134
|
+
const carried = anchors.filter((entry) => entry.subject === subject && entry.status !== "invalid");
|
|
135
|
+
// §7.3: declared intent changes absence semantics. It is a fact the sealed Run states about
|
|
136
|
+
// itself, so it is read against the lock subject; a matrix anchor is never declared in advance.
|
|
137
|
+
const missing = subject === "lock"
|
|
138
|
+
? declared.filter((profile) => !carried.some((entry) => entry.provider === profile))
|
|
139
|
+
: [];
|
|
140
|
+
if (missing.length > 0) {
|
|
141
|
+
return { subject, outcome: "declared-but-absent", declaredProfiles: [...missing].sort(compareCodeUnits) };
|
|
142
|
+
}
|
|
143
|
+
return { subject, outcome: carried.length > 0 ? "anchored" : "absent" };
|
|
144
|
+
});
|
|
145
|
+
return { anchors, subjects, invalid: anchors.filter((entry) => entry.status === "invalid") };
|
|
146
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnchorCertificateReader, AnchorChainVerifier, AnchorSignatureVerifier, Rfc3161AnchorProofVerifierPorts } from "@jinn-network/trust-core";
|
|
2
|
+
export declare const anchorSignatureVerifier: AnchorSignatureVerifier;
|
|
3
|
+
export declare const anchorCertificateReader: AnchorCertificateReader;
|
|
4
|
+
export declare const anchorChainVerifier: AnchorChainVerifier;
|
|
5
|
+
/** The three ports, ready to inject into `createRfc3161AnchorProofVerifier`. */
|
|
6
|
+
export declare const nodeCryptoAnchorPorts: Rfc3161AnchorProofVerifierPorts;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The three `node:crypto` ports the RFC 3161 anchor rule engine injects
|
|
4
|
+
* (anchor-evidence design §6.1 "Placement").
|
|
5
|
+
*
|
|
6
|
+
* `trust-core` owns the rules and produces the exact bytes a signature must
|
|
7
|
+
* cover; it may not import a platform module, and hand-writing an X.509 parser
|
|
8
|
+
* to make a security decision is the invention §3 exists to prevent. So exactly
|
|
9
|
+
* three primitives cross the boundary here, in the standalone verifier package
|
|
10
|
+
* where `node:crypto` is legal and where a reader who wants to check a claim
|
|
11
|
+
* with no Jinn code at all can see what the verifier actually does.
|
|
12
|
+
*
|
|
13
|
+
* Three implementation facts are load-bearing and easy to get wrong:
|
|
14
|
+
*
|
|
15
|
+
* - **The verification hash never comes from a platform default.** Real
|
|
16
|
+
* authorities emit bare `rsaEncryption` as the SignerInfo `signatureAlgorithm`
|
|
17
|
+
* and leave the hash to the SignerInfo `digestAlgorithm` (the committed
|
|
18
|
+
* RSA production capture does exactly this). `crypto.verify` falls back to SHA-256
|
|
19
|
+
* when the algorithm pins no digest, so a conformant SHA-384 token would be
|
|
20
|
+
* verified under the wrong hash -- and the design's SHA-256-family floor would
|
|
21
|
+
* bind by accident of that default rather than by rule. The hash is therefore
|
|
22
|
+
* read from `algorithmOid` when the algorithm names one, and from
|
|
23
|
+
* `digestAlgorithmOid` when it does not.
|
|
24
|
+
* - **Node's `X509Certificate` does not expose raw issuer, serial, subject, or
|
|
25
|
+
* extension bytes**, and §6.1 rules 7 and 10 compare *bytes* -- distinguished
|
|
26
|
+
* names are deliberately kept out of string comparison because DN rendering is
|
|
27
|
+
* not canonical across implementations. Those four fields are therefore
|
|
28
|
+
* extracted structurally from the certificate DER through trust-core's reader.
|
|
29
|
+
* In particular `X509Certificate.serialNumber` is a hex string with the DER
|
|
30
|
+
* sign octet dropped, which would fail a byte comparison against any
|
|
31
|
+
* `SignerInfo.sid` naming a serial whose leading octet has the high bit set.
|
|
32
|
+
* Everything Node *does* expose exactly -- the SubjectPublicKeyInfo export,
|
|
33
|
+
* the validity dates, the extended key usage list -- comes from Node.
|
|
34
|
+
* - **Extension criticality is not checkable here.** `X509Certificate` surfaces
|
|
35
|
+
* no criticality flags, so §6.1 rule 9's "sole usage" half is enforced and its
|
|
36
|
+
* "critical" half is not. The gap is recorded (design §16, issue #2761) rather
|
|
37
|
+
* than papered over with a hand-rolled extension parser.
|
|
38
|
+
*
|
|
39
|
+
* Every port follows the package's port convention: one object argument, plain
|
|
40
|
+
* data in and out, and a `try`/`catch` that answers `false` rather than
|
|
41
|
+
* throwing -- a platform refusal is an answer, not an exception the rule engine
|
|
42
|
+
* has to interpret.
|
|
43
|
+
*/
|
|
44
|
+
import { X509Certificate, constants, createPublicKey, verify } from "node:crypto";
|
|
45
|
+
import { DER_TAG, OID_ECDSA_WITH_SHA256, OID_ECDSA_WITH_SHA384, OID_ECDSA_WITH_SHA512, OID_RSASSA_PSS, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA384, OID_SHA384_WITH_RSA_ENCRYPTION, OID_SHA512, OID_SHA512_WITH_RSA_ENCRYPTION, compareCalendarStrictRfc3339Instants, decodeDer, decodeDerChildren, encodeDerElement, isCalendarStrictRfc3339, readDerOid, } from "@jinn-network/trust-core";
|
|
46
|
+
const OID_EXT_SUBJECT_KEY_IDENTIFIER = "2.5.29.14";
|
|
47
|
+
const OID_EXT_SUBJECT_ALT_NAME = "2.5.29.17";
|
|
48
|
+
const OID_MGF1 = "1.2.840.113549.1.1.8";
|
|
49
|
+
const CONTEXT_CONSTRUCTED_0 = 0xa0;
|
|
50
|
+
const CONTEXT_CONSTRUCTED_1 = 0xa1;
|
|
51
|
+
const CONTEXT_CONSTRUCTED_2 = 0xa2;
|
|
52
|
+
const CONTEXT_CONSTRUCTED_3 = 0xa3;
|
|
53
|
+
/** `GeneralName ::= CHOICE { ... directoryName [4] Name ... }` -- explicit,
|
|
54
|
+
* because `Name` is itself a CHOICE. */
|
|
55
|
+
const GENERAL_NAME_DIRECTORY = 0xa4;
|
|
56
|
+
/** Hashes named by the signature algorithm itself. */
|
|
57
|
+
const ALGORITHM_HASHES = new Map([
|
|
58
|
+
[OID_ECDSA_WITH_SHA256, "sha256"],
|
|
59
|
+
[OID_ECDSA_WITH_SHA384, "sha384"],
|
|
60
|
+
[OID_ECDSA_WITH_SHA512, "sha512"],
|
|
61
|
+
[OID_SHA256_WITH_RSA_ENCRYPTION, "sha256"],
|
|
62
|
+
[OID_SHA384_WITH_RSA_ENCRYPTION, "sha384"],
|
|
63
|
+
[OID_SHA512_WITH_RSA_ENCRYPTION, "sha512"],
|
|
64
|
+
]);
|
|
65
|
+
/** Hashes named by a digest AlgorithmIdentifier. */
|
|
66
|
+
const DIGEST_HASHES = new Map([
|
|
67
|
+
[OID_SHA256, "sha256"],
|
|
68
|
+
[OID_SHA384, "sha384"],
|
|
69
|
+
[OID_SHA512, "sha512"],
|
|
70
|
+
]);
|
|
71
|
+
/** Which key type each admitted signature algorithm requires. A token naming an
|
|
72
|
+
* RSA algorithm over an EC key is a confusion this refuses outright. */
|
|
73
|
+
const ALGORITHM_KEY_TYPES = new Map([
|
|
74
|
+
[OID_ECDSA_WITH_SHA256, ["ec"]],
|
|
75
|
+
[OID_ECDSA_WITH_SHA384, ["ec"]],
|
|
76
|
+
[OID_ECDSA_WITH_SHA512, ["ec"]],
|
|
77
|
+
[OID_SHA256_WITH_RSA_ENCRYPTION, ["rsa"]],
|
|
78
|
+
[OID_SHA384_WITH_RSA_ENCRYPTION, ["rsa"]],
|
|
79
|
+
[OID_SHA512_WITH_RSA_ENCRYPTION, ["rsa"]],
|
|
80
|
+
[OID_RSASSA_PSS, ["rsa", "rsa-pss"]],
|
|
81
|
+
]);
|
|
82
|
+
/** Bare `rsaEncryption` names no hash at all: the SignerInfo digest algorithm
|
|
83
|
+
* is the only statement of it anywhere in the token. */
|
|
84
|
+
const RSA_KEY_TYPES = ["rsa", "rsa-pss"];
|
|
85
|
+
function children(element) {
|
|
86
|
+
return decodeDerChildren(element);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `RSASSA-PSS-params ::= SEQUENCE { hashAlgorithm [0] DEFAULT sha1,
|
|
90
|
+
* maskGenAlgorithm [1] DEFAULT mgf1SHA1, saltLength [2] DEFAULT 20,
|
|
91
|
+
* trailerField [3] DEFAULT 1 }`, all tags explicit.
|
|
92
|
+
*
|
|
93
|
+
* The rule engine has already floored `hashAlgorithm` against the SHA-256
|
|
94
|
+
* family; this reads the same bytes for the salt length Node needs. The MGF1
|
|
95
|
+
* hash must match the signature hash, because `crypto.verify` uses one hash for
|
|
96
|
+
* both -- a token that used different ones would otherwise be verified under
|
|
97
|
+
* parameters it did not declare. `trailerField` has exactly one admitted value.
|
|
98
|
+
*/
|
|
99
|
+
function readPssParameters(parameters) {
|
|
100
|
+
if (parameters === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
const parts = children(decodeDer(parameters));
|
|
103
|
+
const tagged = (identifier) => parts.find((part) => part.identifier === identifier);
|
|
104
|
+
const hashAlgorithm = tagged(CONTEXT_CONSTRUCTED_0);
|
|
105
|
+
if (hashAlgorithm === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
const hashName = DIGEST_HASHES.get(readDerOid(children(children(hashAlgorithm)[0])[0]));
|
|
108
|
+
if (hashName === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
const maskGenAlgorithm = tagged(CONTEXT_CONSTRUCTED_1);
|
|
111
|
+
if (maskGenAlgorithm !== undefined) {
|
|
112
|
+
const mgf = children(children(maskGenAlgorithm)[0]);
|
|
113
|
+
if (readDerOid(mgf[0]) !== OID_MGF1)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (DIGEST_HASHES.get(readDerOid(children(mgf[1])[0])) !== hashName)
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// An absent maskGenAlgorithm defaults to MGF1-SHA-1, which cannot pair with
|
|
120
|
+
// a SHA-256-family signature hash.
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const saltLengthElement = tagged(CONTEXT_CONSTRUCTED_2);
|
|
124
|
+
const saltLength = saltLengthElement === undefined
|
|
125
|
+
? 20
|
|
126
|
+
: readSmallInteger(children(saltLengthElement)[0]);
|
|
127
|
+
const trailerFieldElement = tagged(CONTEXT_CONSTRUCTED_3);
|
|
128
|
+
if (trailerFieldElement !== undefined && readSmallInteger(children(trailerFieldElement)[0]) !== 1) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return { hashName, saltLength };
|
|
132
|
+
}
|
|
133
|
+
function readSmallInteger(element) {
|
|
134
|
+
if (element.identifier !== DER_TAG.INTEGER || element.content.length > 4) {
|
|
135
|
+
throw new Error("Expected a small DER INTEGER.");
|
|
136
|
+
}
|
|
137
|
+
let value = 0;
|
|
138
|
+
for (const octet of element.content)
|
|
139
|
+
value = value * 256 + octet;
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
export const anchorSignatureVerifier = {
|
|
143
|
+
verifySignature(input) {
|
|
144
|
+
try {
|
|
145
|
+
const key = createPublicKey({
|
|
146
|
+
key: Buffer.from(input.spkiDer),
|
|
147
|
+
format: "der",
|
|
148
|
+
type: "spki",
|
|
149
|
+
});
|
|
150
|
+
const message = Buffer.from(input.message);
|
|
151
|
+
const signature = Buffer.from(input.signature);
|
|
152
|
+
if (input.algorithmOid === OID_RSASSA_PSS) {
|
|
153
|
+
const parameters = readPssParameters(input.parameters);
|
|
154
|
+
if (parameters === undefined || !isKeyType(key, RSA_KEY_TYPES))
|
|
155
|
+
return false;
|
|
156
|
+
return verify(parameters.hashName, message, { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: parameters.saltLength }, signature);
|
|
157
|
+
}
|
|
158
|
+
// The algorithm's own digest when it names one; otherwise the SignerInfo
|
|
159
|
+
// digest algorithm. Never a platform default.
|
|
160
|
+
const named = ALGORITHM_HASHES.get(input.algorithmOid);
|
|
161
|
+
const hashName = named ?? DIGEST_HASHES.get(input.digestAlgorithmOid);
|
|
162
|
+
if (hashName === undefined)
|
|
163
|
+
return false;
|
|
164
|
+
const keyTypes = ALGORITHM_KEY_TYPES.get(input.algorithmOid) ?? RSA_KEY_TYPES;
|
|
165
|
+
if (!isKeyType(key, keyTypes))
|
|
166
|
+
return false;
|
|
167
|
+
return verify(hashName, message, key, signature);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
function isKeyType(key, admitted) {
|
|
175
|
+
return key.asymmetricKeyType !== undefined && admitted.includes(key.asymmetricKeyType);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The four fields Node does not expose as bytes, read straight out of the
|
|
179
|
+
* certificate DER: `TBSCertificate ::= SEQUENCE { version [0] EXPLICIT DEFAULT
|
|
180
|
+
* v1, serialNumber, signature, issuer, validity, subject, subjectPublicKeyInfo,
|
|
181
|
+
* ..., extensions [3] EXPLICIT OPTIONAL }`.
|
|
182
|
+
*/
|
|
183
|
+
function readStructuralFields(certificateDer) {
|
|
184
|
+
const tbs = children(children(decodeDer(certificateDer))[0]);
|
|
185
|
+
const offset = tbs[0].identifier === CONTEXT_CONSTRUCTED_0 ? 1 : 0;
|
|
186
|
+
const extensionsWrapper = tbs.slice(offset + 6).find((element) => element.identifier === CONTEXT_CONSTRUCTED_3);
|
|
187
|
+
return {
|
|
188
|
+
serialNumber: tbs[offset].content,
|
|
189
|
+
issuerDer: tbs[offset + 2].bytes,
|
|
190
|
+
subjectDer: tbs[offset + 4].bytes,
|
|
191
|
+
extensions: extensionsWrapper === undefined ? [] : children(children(extensionsWrapper)[0]),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** `Extension ::= SEQUENCE { extnID, critical BOOLEAN DEFAULT FALSE,
|
|
195
|
+
* extnValue OCTET STRING }` -- the DER inside `extnValue`. */
|
|
196
|
+
function extensionValue(extensions, oid) {
|
|
197
|
+
for (const extension of extensions) {
|
|
198
|
+
const parts = children(extension);
|
|
199
|
+
if (readDerOid(parts[0]) === oid)
|
|
200
|
+
return parts.at(-1).content;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
/** Calendar-strict RFC 3339 UTC at second precision -- the spelling every other
|
|
205
|
+
* trust instant uses, so one comparator judges them all. `toISOString` would add
|
|
206
|
+
* a `.000` fraction that says nothing the seconds do not. */
|
|
207
|
+
function toRfc3339(date) {
|
|
208
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
209
|
+
return `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`
|
|
210
|
+
+ `T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}Z`;
|
|
211
|
+
}
|
|
212
|
+
export const anchorCertificateReader = {
|
|
213
|
+
readCertificate(certificateDer) {
|
|
214
|
+
const certificate = new X509Certificate(Buffer.from(certificateDer));
|
|
215
|
+
const structural = readStructuralFields(certificateDer);
|
|
216
|
+
const subjectKeyIdentifier = extensionValue(structural.extensions, OID_EXT_SUBJECT_KEY_IDENTIFIER);
|
|
217
|
+
const subjectAltName = extensionValue(structural.extensions, OID_EXT_SUBJECT_ALT_NAME);
|
|
218
|
+
const sid = [
|
|
219
|
+
{
|
|
220
|
+
kind: "issuerAndSerialNumber",
|
|
221
|
+
issuerDer: structural.issuerDer,
|
|
222
|
+
serialNumber: structural.serialNumber,
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
if (subjectKeyIdentifier !== undefined) {
|
|
226
|
+
sid.push({
|
|
227
|
+
kind: "subjectKeyIdentifier",
|
|
228
|
+
keyIdentifier: decodeDer(subjectKeyIdentifier).content,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
subjectPublicKeyInfoDer: new Uint8Array(certificate.publicKey.export({ format: "der", type: "spki" })),
|
|
233
|
+
notBefore: toRfc3339(certificate.validFromDate),
|
|
234
|
+
notAfter: toRfc3339(certificate.validToDate),
|
|
235
|
+
// Node names the extended-key-usage OID array `keyUsage`; the key-usage
|
|
236
|
+
// bit string is `X509Certificate.keyUsage`'s neighbour and is not this.
|
|
237
|
+
extendedKeyUsageOids: certificate.keyUsage ?? [],
|
|
238
|
+
subjectNames: [
|
|
239
|
+
encodeDerElement(GENERAL_NAME_DIRECTORY, structural.subjectDer),
|
|
240
|
+
...(subjectAltName === undefined
|
|
241
|
+
? []
|
|
242
|
+
: children(decodeDer(subjectAltName)).map((element) => element.bytes)),
|
|
243
|
+
],
|
|
244
|
+
sid,
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Chain validation
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
/** No legitimate timestamp chain is deeper than this; the bound stops a cyclic
|
|
252
|
+
* or adversarial certificate set from looping. */
|
|
253
|
+
const MAX_CHAIN_DEPTH = 8;
|
|
254
|
+
/**
|
|
255
|
+
* Validity at the instant the caller named, judged by the same calendar-strict
|
|
256
|
+
* RFC 3339 comparator every other trust instant goes through.
|
|
257
|
+
*
|
|
258
|
+
* `Date.parse` is deliberately not used. This port is a public export, its
|
|
259
|
+
* `atTime` can arrive from anywhere, and `Date.parse` accepts bare dates
|
|
260
|
+
* (`"2026-08-17"`), reads some spellings as local time, and falls back to
|
|
261
|
+
* implementation-defined parsing for the rest -- three ways for a chain to be
|
|
262
|
+
* validated at an instant nobody named. A string the comparator cannot judge
|
|
263
|
+
* yields `undefined`, which reads here as "cannot validate", never as "valid".
|
|
264
|
+
*/
|
|
265
|
+
function validAt(certificate, atTime) {
|
|
266
|
+
const notBefore = compareCalendarStrictRfc3339Instants(atTime, toRfc3339(certificate.validFromDate));
|
|
267
|
+
const notAfter = compareCalendarStrictRfc3339Instants(atTime, toRfc3339(certificate.validToDate));
|
|
268
|
+
return notBefore !== undefined && notAfter !== undefined && notBefore >= 0 && notAfter <= 0;
|
|
269
|
+
}
|
|
270
|
+
export const anchorChainVerifier = {
|
|
271
|
+
verifyCertificateChain(input) {
|
|
272
|
+
try {
|
|
273
|
+
// §8 step 3: the verifier ships with no roots, and a chain validated
|
|
274
|
+
// solely against bundle-supplied roots would re-import the self-run
|
|
275
|
+
// problem with extra ceremony. An empty set can never verify.
|
|
276
|
+
if (input.trustAnchorsDer.length === 0)
|
|
277
|
+
return false;
|
|
278
|
+
if (!isCalendarStrictRfc3339(input.atTime))
|
|
279
|
+
return false;
|
|
280
|
+
const anchors = input.trustAnchorsDer.map((der) => new X509Certificate(Buffer.from(der)));
|
|
281
|
+
const anchorFingerprints = new Set(anchors.map((anchor) => anchor.fingerprint256));
|
|
282
|
+
const chain = input.certificateChainDer.map((der) => new X509Certificate(Buffer.from(der)));
|
|
283
|
+
let current = chain[0];
|
|
284
|
+
if (current === undefined)
|
|
285
|
+
return false;
|
|
286
|
+
const seen = new Set();
|
|
287
|
+
for (let depth = 0; depth < MAX_CHAIN_DEPTH; depth += 1) {
|
|
288
|
+
if (seen.has(current.fingerprint256))
|
|
289
|
+
return false;
|
|
290
|
+
seen.add(current.fingerprint256);
|
|
291
|
+
// Validity is judged at the token's own genTime, not at the wall clock:
|
|
292
|
+
// a historical token with an expired-but-then-valid chain must not fail
|
|
293
|
+
// for the wrong reason.
|
|
294
|
+
if (!validAt(current, input.atTime))
|
|
295
|
+
return false;
|
|
296
|
+
// A certificate that *is* one of the roots the operator supplied needs
|
|
297
|
+
// no issuance check: the operator trusted those exact bytes, which is
|
|
298
|
+
// what a trust anchor means (RFC 5280 admits a zero-length path). This
|
|
299
|
+
// is also the only way a directly-trusted self-signed timestamping
|
|
300
|
+
// certificate can ever anchor -- such a certificate asserts
|
|
301
|
+
// `digitalSignature` and not `keyCertSign`, so an issuance check against
|
|
302
|
+
// itself refuses it for a reason that does not apply.
|
|
303
|
+
if (anchorFingerprints.has(current.fingerprint256))
|
|
304
|
+
return true;
|
|
305
|
+
const leaf = current;
|
|
306
|
+
if (anchors.some((anchor) => issued(leaf, anchor) && validAt(anchor, input.atTime)))
|
|
307
|
+
return true;
|
|
308
|
+
const issuer = chain.find((candidate) => candidate.fingerprint256 !== leaf.fingerprint256 && issued(leaf, candidate));
|
|
309
|
+
if (issuer === undefined)
|
|
310
|
+
return false;
|
|
311
|
+
current = issuer;
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* Whether `issuer` may issue `certificate`, and did.
|
|
322
|
+
*
|
|
323
|
+
* `checkIssued` covers name linkage, the authority/subject key identifiers when
|
|
324
|
+
* present, and the issuer's `keyCertSign` key usage -- but **not** basic
|
|
325
|
+
* constraints, so a `CA:FALSE` end-entity certificate that happens to assert
|
|
326
|
+
* `keyCertSign` passes it. That is a measured bypass, not a theoretical one: it
|
|
327
|
+
* would let a leaf be spliced in as an intermediate. `issuer.ca` is therefore
|
|
328
|
+
* required before the name check, and the signature is verified after it,
|
|
329
|
+
* because `checkIssued` proves relationship and not authorship.
|
|
330
|
+
*
|
|
331
|
+
* **Disclosed gaps**, in the style §16 uses for the extension-criticality gap
|
|
332
|
+
* (issue #2761's family): this walk checks issuance, key usage, basic
|
|
333
|
+
* constraints, and validity at the caller's instant. It does **not** check
|
|
334
|
+
* revocation (no CRL or OCSP is fetched -- acquisition never runs at
|
|
335
|
+
* verification time, §4.3), `pathLenConstraint`, or name constraints. A verifier
|
|
336
|
+
* that needs those runs its own path validation over the same carried chain;
|
|
337
|
+
* naming the gap is the honest form, and papering over it with a partial
|
|
338
|
+
* implementation would be worse than either.
|
|
339
|
+
*/
|
|
340
|
+
function issued(certificate, issuer) {
|
|
341
|
+
try {
|
|
342
|
+
return issuer.ca && certificate.checkIssued(issuer) && certificate.verify(issuer.publicKey);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
/** The three ports, ready to inject into `createRfc3161AnchorProofVerifier`. */
|
|
350
|
+
export const nodeCryptoAnchorPorts = {
|
|
351
|
+
signatureVerifier: anchorSignatureVerifier,
|
|
352
|
+
certificateReader: anchorCertificateReader,
|
|
353
|
+
chainVerifier: anchorChainVerifier,
|
|
354
|
+
};
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MatrixRecord, ReportRecord } from "@jinn-network/benchmarking-records";
|
|
2
|
+
import type { ClaimPackage } from "./profile/claim.js";
|
|
3
|
+
import type { PublicComparisonView } from "./comparison.js";
|
|
4
|
+
export interface PublicAssetInput {
|
|
5
|
+
readonly claim: ClaimPackage;
|
|
6
|
+
readonly matrix: MatrixRecord;
|
|
7
|
+
readonly report: ReportRecord;
|
|
8
|
+
readonly reportSha256: string;
|
|
9
|
+
readonly matrixSha256: string;
|
|
10
|
+
/** Canonically sorted identities for every authenticated `records/<sha>.bin` closure member. */
|
|
11
|
+
readonly recordSha256s: readonly string[];
|
|
12
|
+
readonly dissentCellKeys: readonly string[];
|
|
13
|
+
/** Verifier-derived, authenticated human projection. Absent only for legacy bundle assets. */
|
|
14
|
+
readonly comparison?: PublicComparisonView;
|
|
15
|
+
/** Producer-verified binary admission/instrument facts. Required for claim-package/2 only. */
|
|
16
|
+
readonly binaryQualification?: {
|
|
17
|
+
readonly publicationGrade: boolean;
|
|
18
|
+
readonly truthAdmission: "two-human-unanimous" | "operator-only" | "screened-operator-sampled";
|
|
19
|
+
readonly sourceManifestSha256: string;
|
|
20
|
+
readonly admissionManifestSha256: string;
|
|
21
|
+
readonly exclusions: readonly unknown[];
|
|
22
|
+
readonly instruments: readonly {
|
|
23
|
+
armId: string;
|
|
24
|
+
instrumentSha256: string;
|
|
25
|
+
promptTemplateSha256: string;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function binaryAdmissionHtml(input: PublicAssetInput): string;
|
|
30
|
+
export declare function binaryAdmissionMarkdown(input: PublicAssetInput): string;
|
|
31
|
+
/** Fixed, deterministic public-bundle/2 presentation bytes. The builder only projects already
|
|
32
|
+
* verified stored facts; it never computes a statistic, selects a winner, or reconciles records. */
|
|
33
|
+
export declare function buildPublicAssets(input: PublicAssetInput): Readonly<Record<string, Uint8Array>>;
|