@emilia-protocol/gate 0.19.0 → 0.20.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/CHANGELOG.md +29 -0
- package/README.md +22 -0
- package/action-refusal-statement.js +1 -0
- package/coverage-reconciliation-attestation.js +1 -0
- package/dist/action-refusal-statement.d.ts +109 -0
- package/dist/action-refusal-statement.d.ts.map +1 -0
- package/dist/action-refusal-statement.js +333 -0
- package/dist/action-refusal-statement.js.map +1 -0
- package/dist/coverage-reconciliation-attestation.d.ts +29 -0
- package/dist/coverage-reconciliation-attestation.d.ts.map +1 -0
- package/dist/coverage-reconciliation-attestation.js +111 -0
- package/dist/coverage-reconciliation-attestation.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/loss-allocation-schedule.d.ts +53 -0
- package/dist/loss-allocation-schedule.d.ts.map +1 -0
- package/dist/loss-allocation-schedule.js +351 -0
- package/dist/loss-allocation-schedule.js.map +1 -0
- package/dist/loss-experience-feed.d.ts +82 -0
- package/dist/loss-experience-feed.d.ts.map +1 -0
- package/dist/loss-experience-feed.js +314 -0
- package/dist/loss-experience-feed.js.map +1 -0
- package/dist/open-exposure-ledger-postgres.d.ts +40 -0
- package/dist/open-exposure-ledger-postgres.d.ts.map +1 -0
- package/dist/open-exposure-ledger-postgres.js +577 -0
- package/dist/open-exposure-ledger-postgres.js.map +1 -0
- package/dist/open-exposure-ledger.d.ts +243 -0
- package/dist/open-exposure-ledger.d.ts.map +1 -0
- package/dist/open-exposure-ledger.js +794 -0
- package/dist/open-exposure-ledger.js.map +1 -0
- package/dist/receipt-census.d.ts +21 -0
- package/dist/receipt-census.d.ts.map +1 -0
- package/dist/receipt-census.js +162 -0
- package/dist/receipt-census.js.map +1 -0
- package/dist/reliance-risk-crypto.d.ts +28 -0
- package/dist/reliance-risk-crypto.d.ts.map +1 -0
- package/dist/reliance-risk-crypto.js +110 -0
- package/dist/reliance-risk-crypto.js.map +1 -0
- package/loss-allocation-schedule.js +1 -0
- package/loss-experience-feed.js +1 -0
- package/open-exposure-ledger-postgres.js +1 -0
- package/open-exposure-ledger.js +1 -0
- package/package.json +36 -1
- package/receipt-census.js +1 -0
- package/src/action-refusal-statement.ts +396 -0
- package/src/coverage-reconciliation-attestation.ts +107 -0
- package/src/index.ts +7 -0
- package/src/loss-allocation-schedule.ts +427 -0
- package/src/loss-experience-feed.ts +398 -0
- package/src/open-exposure-ledger-postgres.ts +701 -0
- package/src/open-exposure-ledger.ts +1213 -0
- package/src/receipt-census.ts +163 -0
- package/src/reliance-risk-crypto.ts +114 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/** Governed-taxonomy aggregate receipt census with coarse primary suppression. */
|
|
3
|
+
import { RISK_DIGEST, riskDigest, riskExact, riskIdentifier, riskInstant, riskRecord, riskFreeze, type RiskRecord } from './reliance-risk-crypto.js';
|
|
4
|
+
|
|
5
|
+
export const RECEIPT_CENSUS_VERSION = 'EP-RECEIPT-CENSUS-v1';
|
|
6
|
+
export const RECEIPT_CENSUS_CLAIM_BOUNDARY = 'aggregate_observation_with_primary_suppression_not_differential_privacy_or_identifier_detection_not_causation_coverage_legal_liability_adjudication_solvency_payment_population_completeness_or_authorization';
|
|
7
|
+
const PERIOD_KEYS = ['start', 'end'] as const;
|
|
8
|
+
const PROGRAM_KEYS = ['program_id', 'version', 'source_digest', 'program_digest'] as const;
|
|
9
|
+
const BUCKET_KEYS = ['action_class', 'program_version', 'outcome', 'count', 'open_exposure_amount_minor', 'reported_loss_amount_minor', 'currency'] as const;
|
|
10
|
+
const BODY_KEYS = ['@version', 'census_id', 'relying_party_id', 'period', 'program', 'taxonomy_digest', 'minimum_bucket_count', 'buckets', 'suppressed', 'source_inventory_digest', 'generated_at', 'claim_boundary'] as const;
|
|
11
|
+
|
|
12
|
+
export interface ReceiptCensusTaxonomy {
|
|
13
|
+
taxonomy_id: string;
|
|
14
|
+
allowed_action_classes: readonly string[];
|
|
15
|
+
allowed_outcomes: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const utf8 = new TextEncoder();
|
|
19
|
+
|
|
20
|
+
function utf8ByteCompare(left: string, right: string): number {
|
|
21
|
+
const a = utf8.encode(left);
|
|
22
|
+
const b = utf8.encode(right);
|
|
23
|
+
const length = Math.min(a.length, b.length);
|
|
24
|
+
for (let index = 0; index < length; index += 1) {
|
|
25
|
+
if (a[index] !== b[index]) return a[index] - b[index];
|
|
26
|
+
}
|
|
27
|
+
return a.length - b.length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeTaxonomy(value: unknown): RiskRecord {
|
|
31
|
+
if (!riskExact(value, ['taxonomy_id', 'allowed_action_classes', 'allowed_outcomes'])
|
|
32
|
+
|| !riskIdentifier(value.taxonomy_id)
|
|
33
|
+
|| !Array.isArray(value.allowed_action_classes) || value.allowed_action_classes.length < 1
|
|
34
|
+
|| value.allowed_action_classes.length > 10_000
|
|
35
|
+
|| !value.allowed_action_classes.every(riskIdentifier)
|
|
36
|
+
|| new Set(value.allowed_action_classes).size !== value.allowed_action_classes.length
|
|
37
|
+
|| !Array.isArray(value.allowed_outcomes) || value.allowed_outcomes.length < 1
|
|
38
|
+
|| value.allowed_outcomes.length > 1_000
|
|
39
|
+
|| !value.allowed_outcomes.every(riskIdentifier)
|
|
40
|
+
|| new Set(value.allowed_outcomes).size !== value.allowed_outcomes.length) {
|
|
41
|
+
throw new TypeError('receipt census taxonomy is invalid');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
taxonomy_id: value.taxonomy_id,
|
|
45
|
+
allowed_action_classes: [...value.allowed_action_classes].sort(utf8ByteCompare),
|
|
46
|
+
allowed_outcomes: [...value.allowed_outcomes].sort(utf8ByteCompare),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function receiptCensusTaxonomyDigest(taxonomy: ReceiptCensusTaxonomy): string {
|
|
51
|
+
return riskDigest(normalizeTaxonomy(taxonomy));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function money(value: unknown): value is string { return typeof value === 'string' && /^(?:0|[1-9][0-9]{0,39})$/.test(value); }
|
|
55
|
+
function validProgram(value: unknown): value is RiskRecord {
|
|
56
|
+
return riskExact(value, PROGRAM_KEYS) && riskIdentifier(value.program_id)
|
|
57
|
+
&& Number.isSafeInteger(value.version) && value.version >= 1
|
|
58
|
+
&& RISK_DIGEST.test(value.source_digest) && RISK_DIGEST.test(value.program_digest);
|
|
59
|
+
}
|
|
60
|
+
function validBucket(value: unknown): value is RiskRecord {
|
|
61
|
+
return riskExact(value, BUCKET_KEYS) && riskIdentifier(value.action_class)
|
|
62
|
+
&& Number.isSafeInteger(value.program_version) && value.program_version >= 1
|
|
63
|
+
&& riskIdentifier(value.outcome) && Number.isSafeInteger(value.count) && value.count >= 0
|
|
64
|
+
&& money(value.open_exposure_amount_minor) && money(value.reported_loss_amount_minor)
|
|
65
|
+
&& typeof value.currency === 'string' && /^[A-Z]{3}$/.test(value.currency);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function bucketKey(bucket: RiskRecord): string {
|
|
69
|
+
return JSON.stringify([
|
|
70
|
+
bucket.action_class,
|
|
71
|
+
bucket.program_version,
|
|
72
|
+
bucket.outcome,
|
|
73
|
+
bucket.currency,
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function uniqueSortedBuckets(buckets: RiskRecord[]): boolean {
|
|
78
|
+
let prior: string | null = null;
|
|
79
|
+
for (const bucket of buckets) {
|
|
80
|
+
const current = bucketKey(bucket);
|
|
81
|
+
if (prior !== null && utf8ByteCompare(prior, current) >= 0) return false;
|
|
82
|
+
prior = current;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createReceiptCensus(input: RiskRecord, taxonomy: ReceiptCensusTaxonomy): RiskRecord {
|
|
88
|
+
const normalizedTaxonomy = normalizeTaxonomy(taxonomy);
|
|
89
|
+
const allowedActionClasses = new Set(normalizedTaxonomy.allowed_action_classes as string[]);
|
|
90
|
+
const allowedOutcomes = new Set(normalizedTaxonomy.allowed_outcomes as string[]);
|
|
91
|
+
const allowedInput = ['census_id', 'relying_party_id', 'period', 'program', 'minimum_bucket_count', 'buckets', 'source_inventory_digest', 'generated_at'];
|
|
92
|
+
if (!riskExact(input, allowedInput) || !riskIdentifier(input.census_id) || !riskIdentifier(input.relying_party_id)
|
|
93
|
+
|| !riskExact(input.period, PERIOD_KEYS) || !validProgram(input.program)
|
|
94
|
+
|| !Number.isSafeInteger(input.minimum_bucket_count) || input.minimum_bucket_count < 2
|
|
95
|
+
|| input.minimum_bucket_count > 1000 || !Array.isArray(input.buckets) || input.buckets.length > 10_000
|
|
96
|
+
|| !input.buckets.every(validBucket) || !RISK_DIGEST.test(input.source_inventory_digest)
|
|
97
|
+
|| !Number.isFinite(riskInstant(input.generated_at))) throw new TypeError('receipt census input or bucket fields are invalid');
|
|
98
|
+
if (!input.buckets.every((bucket: RiskRecord) => allowedActionClasses.has(bucket.action_class)
|
|
99
|
+
&& allowedOutcomes.has(bucket.outcome) && bucket.program_version === input.program.version)) {
|
|
100
|
+
throw new TypeError('receipt census bucket is outside the relying-party taxonomy');
|
|
101
|
+
}
|
|
102
|
+
const start = riskInstant(input.period.start); const end = riskInstant(input.period.end);
|
|
103
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start || riskInstant(input.generated_at) < end) {
|
|
104
|
+
throw new TypeError('receipt census period is invalid');
|
|
105
|
+
}
|
|
106
|
+
const keys = input.buckets.map((bucket: RiskRecord) => bucketKey(bucket));
|
|
107
|
+
if (new Set(keys).size !== keys.length) throw new TypeError('receipt census contains duplicate bucket identities');
|
|
108
|
+
const visible = input.buckets
|
|
109
|
+
.filter((bucket: RiskRecord) => bucket.count >= input.minimum_bucket_count)
|
|
110
|
+
.map((bucket: RiskRecord) => ({ ...bucket }))
|
|
111
|
+
.sort((left: RiskRecord, right: RiskRecord) => utf8ByteCompare(bucketKey(left), bucketKey(right)));
|
|
112
|
+
const hidden = input.buckets.filter((bucket: RiskRecord) => bucket.count < input.minimum_bucket_count);
|
|
113
|
+
const body = {
|
|
114
|
+
'@version': RECEIPT_CENSUS_VERSION,
|
|
115
|
+
census_id: input.census_id,
|
|
116
|
+
relying_party_id: input.relying_party_id,
|
|
117
|
+
period: input.period,
|
|
118
|
+
program: input.program,
|
|
119
|
+
taxonomy_digest: riskDigest(normalizedTaxonomy),
|
|
120
|
+
minimum_bucket_count: input.minimum_bucket_count,
|
|
121
|
+
buckets: visible,
|
|
122
|
+
suppressed: { bucket_count: hidden.length, record_count: hidden.reduce((sum: number, bucket: RiskRecord) => sum + bucket.count, 0) },
|
|
123
|
+
source_inventory_digest: input.source_inventory_digest,
|
|
124
|
+
generated_at: input.generated_at,
|
|
125
|
+
claim_boundary: RECEIPT_CENSUS_CLAIM_BOUNDARY,
|
|
126
|
+
};
|
|
127
|
+
return riskFreeze({ ...body, census_digest: riskDigest(body) });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validateReceiptCensus(value: unknown, taxonomy?: ReceiptCensusTaxonomy) {
|
|
131
|
+
try {
|
|
132
|
+
if (taxonomy === undefined) return { valid: false, reason: 'census_taxonomy_required' };
|
|
133
|
+
const normalizedTaxonomy = normalizeTaxonomy(taxonomy);
|
|
134
|
+
const taxonomyDigest = riskDigest(normalizedTaxonomy);
|
|
135
|
+
const allowedActionClasses = new Set(normalizedTaxonomy.allowed_action_classes as string[]);
|
|
136
|
+
const allowedOutcomes = new Set(normalizedTaxonomy.allowed_outcomes as string[]);
|
|
137
|
+
if (!riskRecord(value) || !Object.hasOwn(value, 'census_digest')) return { valid: false, reason: 'census_shape_invalid' };
|
|
138
|
+
const { census_digest: supplied, ...body } = value;
|
|
139
|
+
if (!riskExact(body, BODY_KEYS) || body['@version'] !== RECEIPT_CENSUS_VERSION
|
|
140
|
+
|| !riskIdentifier(body.census_id) || !riskIdentifier(body.relying_party_id)
|
|
141
|
+
|| !riskExact(body.period, PERIOD_KEYS) || !validProgram(body.program)
|
|
142
|
+
|| body.taxonomy_digest !== taxonomyDigest
|
|
143
|
+
|| !Number.isSafeInteger(body.minimum_bucket_count) || body.minimum_bucket_count < 2
|
|
144
|
+
|| body.minimum_bucket_count > 1000
|
|
145
|
+
|| !Array.isArray(body.buckets) || body.buckets.length > 10_000
|
|
146
|
+
|| !body.buckets.every(validBucket)
|
|
147
|
+
|| !body.buckets.every((bucket: RiskRecord) => allowedActionClasses.has(bucket.action_class)
|
|
148
|
+
&& allowedOutcomes.has(bucket.outcome) && bucket.program_version === body.program.version)
|
|
149
|
+
|| !body.buckets.every((bucket: RiskRecord) => bucket.count >= body.minimum_bucket_count)
|
|
150
|
+
|| !uniqueSortedBuckets(body.buckets)
|
|
151
|
+
|| !riskExact(body.suppressed, ['bucket_count', 'record_count'])
|
|
152
|
+
|| !Number.isSafeInteger(body.suppressed.bucket_count) || body.suppressed.bucket_count < 0
|
|
153
|
+
|| !Number.isSafeInteger(body.suppressed.record_count) || body.suppressed.record_count < 0
|
|
154
|
+
|| !RISK_DIGEST.test(body.source_inventory_digest)
|
|
155
|
+
|| !Number.isFinite(riskInstant(body.generated_at))
|
|
156
|
+
|| body.claim_boundary !== RECEIPT_CENSUS_CLAIM_BOUNDARY
|
|
157
|
+
|| !RISK_DIGEST.test(supplied) || riskDigest(body) !== supplied) return { valid: false, reason: 'census_integrity_invalid' };
|
|
158
|
+
const start = riskInstant(body.period.start); const end = riskInstant(body.period.end);
|
|
159
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start
|
|
160
|
+
|| riskInstant(body.generated_at) < end) return { valid: false, reason: 'census_period_invalid' };
|
|
161
|
+
return { valid: true, reason: null, census_digest: supplied };
|
|
162
|
+
} catch { return { valid: false, reason: 'census_invalid' }; }
|
|
163
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { canonicalize, hashCanonical } from './execution-binding.js';
|
|
4
|
+
|
|
5
|
+
export type RiskRecord = Record<string, any>;
|
|
6
|
+
export type TrustedRiskKeys = Record<string, { issuer_id: string; public_key: string }>;
|
|
7
|
+
|
|
8
|
+
export const RISK_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
9
|
+
export const RISK_CAID = /^caid:1:[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
|
|
10
|
+
export const RISK_ID = /^[A-Za-z0-9][A-Za-z0-9:_.@/+\-]{0,511}$/;
|
|
11
|
+
const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/;
|
|
12
|
+
|
|
13
|
+
export function riskRecord(value: unknown): value is RiskRecord {
|
|
14
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
15
|
+
const prototype = Object.getPrototypeOf(value);
|
|
16
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
17
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
18
|
+
if (typeof key !== 'string') return false;
|
|
19
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
20
|
+
return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function riskExact(value: unknown, keys: readonly string[]): value is RiskRecord {
|
|
25
|
+
return riskRecord(value) && Reflect.ownKeys(value).length === keys.length
|
|
26
|
+
&& keys.every((key) => Object.hasOwn(value, key));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function riskIdentifier(value: unknown): value is string {
|
|
30
|
+
return typeof value === 'string' && RISK_ID.test(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function riskInstant(value: unknown): number {
|
|
34
|
+
if (typeof value !== 'string') return NaN;
|
|
35
|
+
const match = value.match(RFC3339);
|
|
36
|
+
if (!match) return NaN;
|
|
37
|
+
const parsed = Date.parse(value);
|
|
38
|
+
if (!Number.isFinite(parsed)) return NaN;
|
|
39
|
+
return new Date(parsed).toISOString().slice(0, 19) === value.slice(0, 19) ? parsed : NaN;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function riskDigest(value: unknown): string {
|
|
43
|
+
return `sha256:${hashCanonical(value)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function riskClone<T>(value: T): T {
|
|
47
|
+
return JSON.parse(canonicalize(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function riskFreeze<T>(value: T): T {
|
|
51
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
52
|
+
Object.freeze(value);
|
|
53
|
+
for (const child of Object.values(value as RiskRecord)) riskFreeze(child);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function signingBytes(version: string, body: RiskRecord): Buffer {
|
|
58
|
+
return Buffer.from(`${version}\0${canonicalize(body)}`, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function signRiskBody(
|
|
62
|
+
version: string,
|
|
63
|
+
bodyInput: RiskRecord,
|
|
64
|
+
signer: { issuer_id: string; key_id: string; private_key: crypto.KeyLike },
|
|
65
|
+
): RiskRecord {
|
|
66
|
+
const key = signer.private_key instanceof crypto.KeyObject
|
|
67
|
+
? signer.private_key : crypto.createPrivateKey(signer.private_key);
|
|
68
|
+
if (key.asymmetricKeyType !== 'ed25519') throw new TypeError('risk artifact signing key must be Ed25519');
|
|
69
|
+
if (!riskIdentifier(signer.issuer_id) || !riskIdentifier(signer.key_id)) throw new TypeError('risk artifact issuer is invalid');
|
|
70
|
+
const body = riskClone({ ...bodyInput, issuer: { id: signer.issuer_id, key_id: signer.key_id } });
|
|
71
|
+
const bodyDigest = riskDigest(body);
|
|
72
|
+
const proof = {
|
|
73
|
+
algorithm: 'Ed25519',
|
|
74
|
+
key_id: signer.key_id,
|
|
75
|
+
body_digest: bodyDigest,
|
|
76
|
+
signature_b64u: crypto.sign(null, signingBytes(version, body), key).toString('base64url'),
|
|
77
|
+
};
|
|
78
|
+
return riskFreeze({ ...body, proof });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function verifyRiskBody(
|
|
82
|
+
artifact: unknown,
|
|
83
|
+
version: string,
|
|
84
|
+
trustedKeys: TrustedRiskKeys | undefined,
|
|
85
|
+
): { valid: boolean; reason: string | null; body: RiskRecord | null; artifact_digest: string | null } {
|
|
86
|
+
const refuse = (reason: string) => ({ valid: false, reason, body: null, artifact_digest: null });
|
|
87
|
+
try {
|
|
88
|
+
if (!riskRecord(artifact) || !riskRecord(artifact.proof) || !riskRecord(artifact.issuer)) return refuse('artifact_shape_invalid');
|
|
89
|
+
const proof = artifact.proof;
|
|
90
|
+
const { proof: _proof, ...body } = artifact;
|
|
91
|
+
if (artifact['@version'] !== version || !riskExact(artifact.issuer, ['id', 'key_id'])
|
|
92
|
+
|| !riskIdentifier(artifact.issuer.id) || !riskIdentifier(artifact.issuer.key_id)
|
|
93
|
+
|| !riskExact(proof, ['algorithm', 'key_id', 'body_digest', 'signature_b64u'])
|
|
94
|
+
|| proof.algorithm !== 'Ed25519' || proof.key_id !== artifact.issuer.key_id
|
|
95
|
+
|| typeof proof.body_digest !== 'string' || !RISK_DIGEST.test(proof.body_digest)
|
|
96
|
+
|| typeof proof.signature_b64u !== 'string' || !/^[A-Za-z0-9_-]+$/.test(proof.signature_b64u)) {
|
|
97
|
+
return refuse('artifact_signature_envelope_invalid');
|
|
98
|
+
}
|
|
99
|
+
if (riskDigest(body) !== proof.body_digest) return refuse('digest_mismatch');
|
|
100
|
+
const pin = trustedKeys?.[artifact.issuer.key_id];
|
|
101
|
+
if (!pin || pin.issuer_id !== artifact.issuer.id) return refuse('issuer_untrusted');
|
|
102
|
+
const keyBytes = Buffer.from(pin.public_key, 'base64url');
|
|
103
|
+
if (keyBytes.toString('base64url') !== pin.public_key) return refuse('pinned_key_invalid');
|
|
104
|
+
const key = crypto.createPublicKey({ key: keyBytes, type: 'spki', format: 'der' });
|
|
105
|
+
if (key.asymmetricKeyType !== 'ed25519') return refuse('pinned_key_invalid');
|
|
106
|
+
const signature = Buffer.from(proof.signature_b64u, 'base64url');
|
|
107
|
+
if (signature.length !== 64 || signature.toString('base64url') !== proof.signature_b64u
|
|
108
|
+
|| !crypto.verify(null, signingBytes(version, body), key, signature)) return refuse('signature_invalid');
|
|
109
|
+
return { valid: true, reason: null, body, artifact_digest: riskDigest(artifact) };
|
|
110
|
+
} catch {
|
|
111
|
+
return refuse('artifact_invalid');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|