@primust/verifier 1.0.1 → 1.1.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/dist/{chunk-ZADQUKKN.js → chunk-NOADQWB6.js} +59 -10
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/tsa-chain-7KSQ5LAH.js +235 -0
- package/package.json +1 -1
|
@@ -2308,14 +2308,32 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
|
|
|
2308
2308
|
}
|
|
2309
2309
|
const tsAnchor = artifact.timestamp_anchor;
|
|
2310
2310
|
if (tsAnchor && tsAnchor.type === "rfc3161" && typeof tsAnchor.value === "string") {
|
|
2311
|
-
|
|
2311
|
+
const imprintOk = verifyTimestampImprint(
|
|
2312
2312
|
tsAnchor.value,
|
|
2313
2313
|
timestampBody(artifact)
|
|
2314
2314
|
);
|
|
2315
|
-
if (
|
|
2315
|
+
if (imprintOk === false) {
|
|
2316
|
+
result.timestamp_anchor_valid = false;
|
|
2316
2317
|
result.warnings.push("rfc3161_imprint_mismatch");
|
|
2317
|
-
} else if (
|
|
2318
|
-
|
|
2318
|
+
} else if (imprintOk === true) {
|
|
2319
|
+
let chain = null;
|
|
2320
|
+
try {
|
|
2321
|
+
const { verifyTsaChain } = await import("./tsa-chain-7KSQ5LAH.js");
|
|
2322
|
+
chain = verifyTsaChain(tsAnchor.value);
|
|
2323
|
+
} catch {
|
|
2324
|
+
chain = null;
|
|
2325
|
+
}
|
|
2326
|
+
if (chain === null) {
|
|
2327
|
+
result.timestamp_anchor_valid = true;
|
|
2328
|
+
result.warnings.push("rfc3161_tsa_chain_verifier_unavailable");
|
|
2329
|
+
} else if (chain.ok) {
|
|
2330
|
+
result.timestamp_anchor_valid = true;
|
|
2331
|
+
} else {
|
|
2332
|
+
result.timestamp_anchor_valid = false;
|
|
2333
|
+
result.warnings.push(`rfc3161_tsa_chain_invalid:${chain.reason}`);
|
|
2334
|
+
}
|
|
2335
|
+
} else {
|
|
2336
|
+
result.timestamp_anchor_valid = null;
|
|
2319
2337
|
}
|
|
2320
2338
|
} else {
|
|
2321
2339
|
result.timestamp_anchor_valid = null;
|
|
@@ -2829,23 +2847,54 @@ function findBuffer(haystack, needle) {
|
|
|
2829
2847
|
return -1;
|
|
2830
2848
|
}
|
|
2831
2849
|
var REKOR_API = "https://rekor.sigstore.dev/api/v1";
|
|
2850
|
+
var DEFAULT_REVOKED_KEYS_URLS = [
|
|
2851
|
+
"https://keys.primust.com/.well-known/primust-pubkeys/revoked.json",
|
|
2852
|
+
"https://keys.eu.primust.com/.well-known/primust-pubkeys/revoked.json"
|
|
2853
|
+
];
|
|
2854
|
+
function revokedKeysUrls() {
|
|
2855
|
+
const env = globalThis.process?.env?.PRIMUST_REVOKED_KEYS_URLS;
|
|
2856
|
+
if (!env || !env.trim()) return DEFAULT_REVOKED_KEYS_URLS;
|
|
2857
|
+
return env.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2858
|
+
}
|
|
2859
|
+
async function isKeyRevoked(fingerprintHex) {
|
|
2860
|
+
for (const url of revokedKeysUrls()) {
|
|
2861
|
+
try {
|
|
2862
|
+
const resp = await fetch(url, {
|
|
2863
|
+
headers: { Accept: "application/json" },
|
|
2864
|
+
signal: AbortSignal.timeout(3e3)
|
|
2865
|
+
});
|
|
2866
|
+
if (!resp.ok) continue;
|
|
2867
|
+
const body = await resp.json();
|
|
2868
|
+
const list = Array.isArray(body) ? body : Array.isArray(body.revoked) ? body.revoked : [];
|
|
2869
|
+
const needle = fingerprintHex.toLowerCase();
|
|
2870
|
+
for (const entry of list) {
|
|
2871
|
+
if (typeof entry === "string" && entry.toLowerCase().replace(/^sha256:/, "") === needle) {
|
|
2872
|
+
return true;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
return false;
|
|
2876
|
+
} catch {
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
return null;
|
|
2881
|
+
}
|
|
2832
2882
|
async function checkRekor(publicKeyB64Url, kid) {
|
|
2883
|
+
void kid;
|
|
2833
2884
|
try {
|
|
2834
2885
|
const keyBytes = fromBase64Url(publicKeyB64Url);
|
|
2835
2886
|
const fingerprint = createHash("sha256").update(Buffer.from(keyBytes)).digest("hex");
|
|
2887
|
+
const revoked = await isKeyRevoked(fingerprint);
|
|
2888
|
+
if (revoked === true) return "revoked";
|
|
2836
2889
|
const resp = await fetch(`${REKOR_API}/index/retrieve`, {
|
|
2837
2890
|
method: "POST",
|
|
2838
2891
|
headers: { "Content-Type": "application/json" },
|
|
2839
2892
|
body: JSON.stringify({ hash: `sha256:${fingerprint}` }),
|
|
2840
2893
|
signal: AbortSignal.timeout(5e3)
|
|
2841
2894
|
});
|
|
2842
|
-
if (!resp.ok)
|
|
2843
|
-
return "unavailable";
|
|
2844
|
-
}
|
|
2895
|
+
if (!resp.ok) return "unavailable";
|
|
2845
2896
|
const entries = await resp.json();
|
|
2846
|
-
if (!entries || entries.length === 0)
|
|
2847
|
-
return "not_found";
|
|
2848
|
-
}
|
|
2897
|
+
if (!entries || entries.length === 0) return "not_found";
|
|
2849
2898
|
return "active";
|
|
2850
2899
|
} catch {
|
|
2851
2900
|
return "unavailable";
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// src/tsa-chain.ts
|
|
2
|
+
import { X509Certificate, createHash } from "crypto";
|
|
3
|
+
var DIGICERT_TRUSTED_ROOT_G4_FPR_HEX = "552f7bdcf1a7af9e6ce672017f4f12abf77240c78e761ac203d1d9d20ac89988";
|
|
4
|
+
var TIME_STAMPING_EKU_OID = "1.3.6.1.5.5.7.3.8";
|
|
5
|
+
var DIGICERT_TRUSTED_ROOT_G4_PEM = `-----BEGIN CERTIFICATE-----
|
|
6
|
+
MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
|
|
7
|
+
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
|
8
|
+
d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
|
|
9
|
+
RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
|
|
10
|
+
UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
|
|
11
|
+
Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
|
|
12
|
+
SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
|
|
13
|
+
ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
|
|
14
|
+
xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
|
|
15
|
+
ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
|
|
16
|
+
DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
|
|
17
|
+
jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
|
|
18
|
+
CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
|
|
19
|
+
EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
|
|
20
|
+
fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
|
|
21
|
+
uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
|
|
22
|
+
chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
|
|
23
|
+
9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
|
|
24
|
+
hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
|
|
25
|
+
ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
|
|
26
|
+
SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
|
|
27
|
+
+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
|
|
28
|
+
fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
|
|
29
|
+
sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
|
|
30
|
+
cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
|
|
31
|
+
0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
|
|
32
|
+
4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
|
|
33
|
+
r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
|
|
34
|
+
/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
|
|
35
|
+
gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
|
|
36
|
+
-----END CERTIFICATE-----`;
|
|
37
|
+
function loadBundledRoots() {
|
|
38
|
+
const out = [];
|
|
39
|
+
try {
|
|
40
|
+
out.push(new X509Certificate(DIGICERT_TRUSTED_ROOT_G4_PEM));
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
var BUNDLED_ROOTS = loadBundledRoots();
|
|
46
|
+
function trustedRootFprs() {
|
|
47
|
+
const env = globalThis.process?.env?.PRIMUST_TSA_ROOT_FPR;
|
|
48
|
+
if (!env || !env.trim()) {
|
|
49
|
+
return /* @__PURE__ */ new Set([DIGICERT_TRUSTED_ROOT_G4_FPR_HEX]);
|
|
50
|
+
}
|
|
51
|
+
return new Set(
|
|
52
|
+
env.split(",").map((f) => f.trim().toLowerCase().replace(/:/g, "")).filter(Boolean)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function verifyTsaChain(tsTokenB64) {
|
|
56
|
+
let tokenDer;
|
|
57
|
+
try {
|
|
58
|
+
tokenDer = Buffer.from(tsTokenB64, "base64");
|
|
59
|
+
} catch {
|
|
60
|
+
return { ok: false, reason: "parse_error" };
|
|
61
|
+
}
|
|
62
|
+
let certs;
|
|
63
|
+
try {
|
|
64
|
+
certs = extractCertificates(tokenDer);
|
|
65
|
+
} catch {
|
|
66
|
+
return { ok: false, reason: "parse_error" };
|
|
67
|
+
}
|
|
68
|
+
if (certs.length === 0) {
|
|
69
|
+
return { ok: false, reason: "no_certs" };
|
|
70
|
+
}
|
|
71
|
+
const trusted = trustedRootFprs();
|
|
72
|
+
let leaf = null;
|
|
73
|
+
for (const c of certs) {
|
|
74
|
+
if (hasTimeStampingEku(c)) {
|
|
75
|
+
leaf = c;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!leaf) {
|
|
80
|
+
const subjects = new Set(certs.map((c) => c.subject));
|
|
81
|
+
const issuers = new Set(certs.map((c) => c.issuer));
|
|
82
|
+
for (const c of certs) {
|
|
83
|
+
if (subjects.has(c.subject) && !issuers.has(c.subject)) {
|
|
84
|
+
leaf = c;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!leaf) {
|
|
90
|
+
return { ok: false, reason: "leaf_not_found" };
|
|
91
|
+
}
|
|
92
|
+
if (!hasTimeStampingEku(leaf)) {
|
|
93
|
+
return { ok: false, reason: "missing_eku" };
|
|
94
|
+
}
|
|
95
|
+
if (!isWithinValidity(leaf)) {
|
|
96
|
+
return { ok: false, reason: "expired" };
|
|
97
|
+
}
|
|
98
|
+
const candidateRoots = BUNDLED_ROOTS.filter(
|
|
99
|
+
(r) => trusted.has(certFingerprint(r))
|
|
100
|
+
);
|
|
101
|
+
const opRootPem = globalThis.process?.env?.PRIMUST_TSA_ROOT_PEM;
|
|
102
|
+
if (opRootPem) {
|
|
103
|
+
try {
|
|
104
|
+
const opRoot = new X509Certificate(opRootPem);
|
|
105
|
+
if (trusted.has(certFingerprint(opRoot))) {
|
|
106
|
+
candidateRoots.push(opRoot);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
return { ok: false, reason: "parse_error" };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (candidateRoots.length === 0) {
|
|
113
|
+
return { ok: false, reason: "root_not_trusted" };
|
|
114
|
+
}
|
|
115
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const c of certs) bySubject.set(c.subject, c);
|
|
117
|
+
const byRootSubject = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const r of candidateRoots) byRootSubject.set(r.subject, r);
|
|
119
|
+
let current = leaf;
|
|
120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
121
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
122
|
+
if (seen.has(current.subject)) {
|
|
123
|
+
return { ok: false, reason: "chain_incomplete" };
|
|
124
|
+
}
|
|
125
|
+
seen.add(current.subject);
|
|
126
|
+
const rootCert = byRootSubject.get(current.issuer);
|
|
127
|
+
if (rootCert) {
|
|
128
|
+
if (!current.verify(rootCert.publicKey)) {
|
|
129
|
+
return { ok: false, reason: "signature_invalid" };
|
|
130
|
+
}
|
|
131
|
+
if (!isWithinValidity(rootCert)) {
|
|
132
|
+
return { ok: false, reason: "expired" };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, reason: "ok" };
|
|
135
|
+
}
|
|
136
|
+
const issuerCert = bySubject.get(current.issuer);
|
|
137
|
+
if (issuerCert && issuerCert !== current) {
|
|
138
|
+
if (!current.verify(issuerCert.publicKey)) {
|
|
139
|
+
return { ok: false, reason: "signature_invalid" };
|
|
140
|
+
}
|
|
141
|
+
if (!isWithinValidity(issuerCert)) {
|
|
142
|
+
return { ok: false, reason: "expired" };
|
|
143
|
+
}
|
|
144
|
+
if (issuerCert.subject === issuerCert.issuer) {
|
|
145
|
+
if (trusted.has(certFingerprint(issuerCert))) {
|
|
146
|
+
return { ok: true, reason: "ok" };
|
|
147
|
+
}
|
|
148
|
+
return { ok: false, reason: "root_not_trusted" };
|
|
149
|
+
}
|
|
150
|
+
current = issuerCert;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
return { ok: false, reason: "chain_incomplete" };
|
|
154
|
+
}
|
|
155
|
+
return { ok: false, reason: "chain_incomplete" };
|
|
156
|
+
}
|
|
157
|
+
function certFingerprint(cert) {
|
|
158
|
+
return createHash("sha256").update(cert.raw).digest("hex");
|
|
159
|
+
}
|
|
160
|
+
function hasTimeStampingEku(cert) {
|
|
161
|
+
const info = cert.toString();
|
|
162
|
+
return info.includes(TIME_STAMPING_EKU_OID) || info.includes("Time Stamping");
|
|
163
|
+
}
|
|
164
|
+
function isWithinValidity(cert) {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const nb = Date.parse(cert.validFrom);
|
|
167
|
+
const na = Date.parse(cert.validTo);
|
|
168
|
+
if (Number.isNaN(nb) || Number.isNaN(na)) return false;
|
|
169
|
+
return nb <= now && now <= na;
|
|
170
|
+
}
|
|
171
|
+
function extractCertificates(tokenDer) {
|
|
172
|
+
const signedDataOid = Buffer.from([
|
|
173
|
+
6,
|
|
174
|
+
9,
|
|
175
|
+
42,
|
|
176
|
+
134,
|
|
177
|
+
72,
|
|
178
|
+
134,
|
|
179
|
+
247,
|
|
180
|
+
13,
|
|
181
|
+
1,
|
|
182
|
+
7,
|
|
183
|
+
2
|
|
184
|
+
]);
|
|
185
|
+
const oidIdx = tokenDer.indexOf(signedDataOid);
|
|
186
|
+
if (oidIdx === -1) return [];
|
|
187
|
+
const scanStart = oidIdx + signedDataOid.length;
|
|
188
|
+
const limit = tokenDer.length;
|
|
189
|
+
for (let i = scanStart; i < limit - 2; i++) {
|
|
190
|
+
if (tokenDer[i] === 160) {
|
|
191
|
+
const lenInfo = parseDerLength(tokenDer, i + 1);
|
|
192
|
+
if (!lenInfo) continue;
|
|
193
|
+
const { length, headerLen } = lenInfo;
|
|
194
|
+
const setStart = i + 1 + headerLen;
|
|
195
|
+
const setEnd = setStart + length;
|
|
196
|
+
if (setEnd > limit) continue;
|
|
197
|
+
const certs = [];
|
|
198
|
+
let p = setStart;
|
|
199
|
+
while (p < setEnd) {
|
|
200
|
+
if (tokenDer[p] !== 48) break;
|
|
201
|
+
const li = parseDerLength(tokenDer, p + 1);
|
|
202
|
+
if (!li) break;
|
|
203
|
+
const certLen = li.length;
|
|
204
|
+
const certHdr = li.headerLen;
|
|
205
|
+
const certEnd = p + 1 + certHdr + certLen;
|
|
206
|
+
if (certEnd > setEnd) break;
|
|
207
|
+
const certDer = tokenDer.subarray(p, certEnd);
|
|
208
|
+
try {
|
|
209
|
+
certs.push(new X509Certificate(certDer));
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
p = certEnd;
|
|
213
|
+
}
|
|
214
|
+
if (certs.length > 0) return certs;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
function parseDerLength(buf, offset) {
|
|
220
|
+
if (offset >= buf.length) return null;
|
|
221
|
+
const first = buf[offset];
|
|
222
|
+
if ((first & 128) === 0) {
|
|
223
|
+
return { length: first, headerLen: 1 };
|
|
224
|
+
}
|
|
225
|
+
const n = first & 127;
|
|
226
|
+
if (n === 0 || n > 4 || offset + n >= buf.length) return null;
|
|
227
|
+
let val = 0;
|
|
228
|
+
for (let i = 0; i < n; i++) {
|
|
229
|
+
val = val << 8 | buf[offset + 1 + i];
|
|
230
|
+
}
|
|
231
|
+
return { length: val, headerLen: 1 + n };
|
|
232
|
+
}
|
|
233
|
+
export {
|
|
234
|
+
verifyTsaChain
|
|
235
|
+
};
|