@freedomofpress/sigstore-browser 0.1.15 → 0.1.17
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/bundle.d.ts +2 -3
- package/dist/bundle.js.map +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/dsse.d.ts +0 -1
- package/dist/dsse.js +4 -9
- package/dist/dsse.js.map +1 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -10
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +1 -2
- package/dist/interfaces.js +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/oid.d.ts +0 -1
- package/dist/oid.js.map +1 -1
- package/dist/policy.d.ts +0 -1
- package/dist/policy.js +1 -1
- package/dist/policy.js.map +1 -1
- package/dist/rfc3161/error.d.ts +0 -1
- package/dist/rfc3161/error.js.map +1 -1
- package/dist/rfc3161/index.d.ts +0 -1
- package/dist/rfc3161/index.js.map +1 -1
- package/dist/rfc3161/timestamp.d.ts +0 -1
- package/dist/rfc3161/timestamp.js +3 -3
- package/dist/rfc3161/timestamp.js.map +1 -1
- package/dist/rfc3161/tstinfo.d.ts +0 -1
- package/dist/rfc3161/tstinfo.js +2 -3
- package/dist/rfc3161/tstinfo.js.map +1 -1
- package/dist/sigstore.d.ts +2 -3
- package/dist/sigstore.js +11 -14
- package/dist/sigstore.js.map +1 -1
- package/dist/timestamp/tsa.d.ts +1 -2
- package/dist/timestamp/tsa.js +1 -1
- package/dist/timestamp/tsa.js.map +1 -1
- package/dist/tlog/body.d.ts +0 -1
- package/dist/tlog/body.js +1 -1
- package/dist/tlog/body.js.map +1 -1
- package/dist/tlog/checkpoint.d.ts +0 -1
- package/dist/tlog/checkpoint.js.map +1 -1
- package/dist/tlog/dsse.d.ts +0 -1
- package/dist/tlog/dsse.js +6 -4
- package/dist/tlog/dsse.js.map +1 -1
- package/dist/tlog/hashedrekord.d.ts +0 -1
- package/dist/tlog/hashedrekord.js.map +1 -1
- package/dist/tlog/intoto.d.ts +0 -1
- package/dist/tlog/intoto.js +2 -3
- package/dist/tlog/intoto.js.map +1 -1
- package/dist/tlog/merkle.d.ts +0 -1
- package/dist/tlog/merkle.js +8 -102
- package/dist/tlog/merkle.js.map +1 -1
- package/dist/trust/tuf-root.d.ts +0 -1
- package/dist/trust/tuf-root.js.map +1 -1
- package/dist/trust/tuf.d.ts +0 -1
- package/dist/trust/tuf.js +2 -5
- package/dist/trust/tuf.js.map +1 -1
- package/dist/x509/cert.d.ts +0 -1
- package/dist/x509/cert.js +2 -2
- package/dist/x509/cert.js.map +1 -1
- package/dist/x509/chain.d.ts +0 -1
- package/dist/x509/chain.js +2 -2
- package/dist/x509/chain.js.map +1 -1
- package/dist/x509/ext.d.ts +0 -1
- package/dist/x509/ext.js.map +1 -1
- package/dist/x509/index.d.ts +1 -2
- package/dist/x509/index.js +1 -1
- package/dist/x509/index.js.map +1 -1
- package/dist/x509/sct.d.ts +0 -1
- package/dist/x509/sct.js.map +1 -1
- package/package.json +16 -9
- package/.github/workflows/conformance.yml +0 -29
- package/.github/workflows/publish.yml +0 -47
- package/dist/bundle.d.ts.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/dsse.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/interfaces.d.ts.map +0 -1
- package/dist/oid.d.ts.map +0 -1
- package/dist/policy.d.ts.map +0 -1
- package/dist/rfc3161/error.d.ts.map +0 -1
- package/dist/rfc3161/index.d.ts.map +0 -1
- package/dist/rfc3161/timestamp.d.ts.map +0 -1
- package/dist/rfc3161/tstinfo.d.ts.map +0 -1
- package/dist/sigstore.d.ts.map +0 -1
- package/dist/timestamp/tsa.d.ts.map +0 -1
- package/dist/tlog/body.d.ts.map +0 -1
- package/dist/tlog/checkpoint.d.ts.map +0 -1
- package/dist/tlog/dsse.d.ts.map +0 -1
- package/dist/tlog/hashedrekord.d.ts.map +0 -1
- package/dist/tlog/intoto.d.ts.map +0 -1
- package/dist/tlog/merkle.d.ts.map +0 -1
- package/dist/trust/tuf-root.d.ts.map +0 -1
- package/dist/trust/tuf.d.ts.map +0 -1
- package/dist/x509/cert.d.ts.map +0 -1
- package/dist/x509/chain.d.ts.map +0 -1
- package/dist/x509/ext.d.ts.map +0 -1
- package/dist/x509/index.d.ts.map +0 -1
- package/dist/x509/sct.d.ts.map +0 -1
- package/eslint.config.mjs +0 -31
- package/src/bundle.ts +0 -186
- package/src/cli.ts +0 -290
- package/src/dsse.ts +0 -49
- package/src/ed25519.browser.test.ts +0 -76
- package/src/errors.ts +0 -82
- package/src/index.browser.test.ts +0 -301
- package/src/index.ts +0 -93
- package/src/interfaces.ts +0 -150
- package/src/oid.ts +0 -34
- package/src/policy.ts +0 -476
- package/src/rfc3161/error.ts +0 -16
- package/src/rfc3161/index.ts +0 -17
- package/src/rfc3161/timestamp.ts +0 -268
- package/src/rfc3161/tstinfo.ts +0 -78
- package/src/security.browser.test.ts +0 -249
- package/src/sigstore.ts +0 -605
- package/src/timestamp/tsa.ts +0 -212
- package/src/tlog/body.ts +0 -84
- package/src/tlog/checkpoint.ts +0 -154
- package/src/tlog/dsse.ts +0 -153
- package/src/tlog/hashedrekord.ts +0 -136
- package/src/tlog/intoto.ts +0 -109
- package/src/tlog/merkle.ts +0 -152
- package/src/trust/README.md +0 -38
- package/src/trust/tuf-root.ts +0 -3
- package/src/trust/tuf.ts +0 -233
- package/src/verify.browser.test.ts +0 -281
- package/src/x509/cert.ts +0 -569
- package/src/x509/chain.ts +0 -212
- package/src/x509/ext.ts +0 -356
- package/src/x509/index.ts +0 -19
- package/src/x509/sct.ts +0 -156
- package/test/fixtures/rekor2.ts +0 -14
- package/test/fixtures/sigstore.ts +0 -13
- package/tsconfig.json +0 -18
- package/vite.config.playwright.ts +0 -14
- package/vite.config.ts +0 -18
package/src/rfc3161/timestamp.ts
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright 2023 The Sigstore Authors.
|
|
3
|
-
|
|
4
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
you may not use this file except in compliance with the License.
|
|
6
|
-
You may obtain a copy of the License at
|
|
7
|
-
|
|
8
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
|
|
10
|
-
Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
See the License for the specific language governing permissions and
|
|
14
|
-
limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { ASN1Obj, uint8ArrayEqual, verifySignature } from "@freedomofpress/crypto-browser";
|
|
17
|
-
import { HashAlgorithms } from "../interfaces.js";
|
|
18
|
-
import { ECDSA_SIGNATURE_ALGOS, RSA_SIGNATURE_ALGOS, SHA2_HASH_ALGOS } from "../oid.js";
|
|
19
|
-
import { RFC3161TimestampVerificationError } from "./error.js";
|
|
20
|
-
import { TSTInfo } from "./tstinfo.js";
|
|
21
|
-
|
|
22
|
-
const OID_PKCS9_CONTENT_TYPE_SIGNED_DATA = "1.2.840.113549.1.7.2";
|
|
23
|
-
const OID_PKCS9_CONTENT_TYPE_TSTINFO = "1.2.840.113549.1.9.16.1.4";
|
|
24
|
-
const OID_PKCS9_MESSAGE_DIGEST_KEY = "1.2.840.113549.1.9.4";
|
|
25
|
-
|
|
26
|
-
export class RFC3161Timestamp {
|
|
27
|
-
public root: ASN1Obj;
|
|
28
|
-
|
|
29
|
-
constructor(asn1: ASN1Obj) {
|
|
30
|
-
this.root = asn1;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public static parse(der: Uint8Array): RFC3161Timestamp {
|
|
34
|
-
const asn1 = ASN1Obj.parseBuffer(der);
|
|
35
|
-
return new RFC3161Timestamp(asn1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
get status(): bigint {
|
|
39
|
-
return this.pkiStatusInfoObj.subs[0].toInteger();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
get contentType(): string {
|
|
43
|
-
return this.contentTypeObj.toOID();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get eContentType(): string {
|
|
47
|
-
return this.eContentTypeObj.toOID();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
get signingTime(): Date {
|
|
51
|
-
return this.tstInfo.genTime;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
get signerIssuer(): Uint8Array {
|
|
55
|
-
return this.signerSidObj.subs[0].value;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
get signerSerialNumber(): Uint8Array {
|
|
59
|
-
return this.signerSidObj.subs[1].value;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
get signerDigestAlgorithm(): string {
|
|
63
|
-
const oid = this.signerDigestAlgorithmObj.subs[0].toOID();
|
|
64
|
-
const algo = SHA2_HASH_ALGOS[oid];
|
|
65
|
-
if (!algo) {
|
|
66
|
-
throw new Error(`Unknown digest algorithm OID: ${oid}`);
|
|
67
|
-
}
|
|
68
|
-
return algo;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
get signatureAlgorithm(): string {
|
|
72
|
-
const oid = this.signatureAlgorithmObj.subs[0].toOID();
|
|
73
|
-
const algo = ECDSA_SIGNATURE_ALGOS[oid] || RSA_SIGNATURE_ALGOS[oid];
|
|
74
|
-
return algo;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
get signatureValue(): Uint8Array {
|
|
78
|
-
return this.signatureValueObj.value;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
get tstInfo(): TSTInfo {
|
|
82
|
-
// Need to unpack tstInfo from an OCTET STRING
|
|
83
|
-
return new TSTInfo(this.eContentObj.subs[0].subs[0]);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public async verify(data: Uint8Array, publicKey: CryptoKey): Promise<void> {
|
|
87
|
-
if (!this.timeStampTokenObj) {
|
|
88
|
-
throw new RFC3161TimestampVerificationError("timeStampToken is missing");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Check for expected ContentInfo content type
|
|
92
|
-
if (this.contentType !== OID_PKCS9_CONTENT_TYPE_SIGNED_DATA) {
|
|
93
|
-
throw new RFC3161TimestampVerificationError(
|
|
94
|
-
`incorrect content type: ${this.contentType}`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Check for expected encapsulated content type
|
|
99
|
-
if (this.eContentType !== OID_PKCS9_CONTENT_TYPE_TSTINFO) {
|
|
100
|
-
throw new RFC3161TimestampVerificationError(
|
|
101
|
-
`incorrect encapsulated content type: ${this.eContentType}`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check that the tstInfo references the correct artifact
|
|
106
|
-
await this.tstInfo.verify(data);
|
|
107
|
-
// Check that the signed message digest matches the tstInfo
|
|
108
|
-
await this.verifyMessageDigest();
|
|
109
|
-
// Check that the signature is valid for the signed attributes
|
|
110
|
-
await this.verifySignature(publicKey);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private async verifyMessageDigest(): Promise<void> {
|
|
114
|
-
// Check that the tstInfo matches the signed data
|
|
115
|
-
const hashAlg = this.signerDigestAlgorithm;
|
|
116
|
-
|
|
117
|
-
// Convert hash algorithm name to the format expected by WebCrypto
|
|
118
|
-
const hashAlgName = hashAlg === "sha256" ? HashAlgorithms.SHA256 :
|
|
119
|
-
hashAlg === "sha384" ? HashAlgorithms.SHA384 :
|
|
120
|
-
hashAlg === "sha512" ? HashAlgorithms.SHA512 : hashAlg;
|
|
121
|
-
|
|
122
|
-
const tstInfoDigest = await crypto.subtle.digest(
|
|
123
|
-
hashAlgName,
|
|
124
|
-
this.tstInfo.raw as BufferSource,
|
|
125
|
-
);
|
|
126
|
-
const expectedDigest = this.messageDigestAttributeObj.subs[1].subs[0].value;
|
|
127
|
-
|
|
128
|
-
if (!uint8ArrayEqual(new Uint8Array(tstInfoDigest), expectedDigest)) {
|
|
129
|
-
throw new RFC3161TimestampVerificationError(
|
|
130
|
-
"signed data does not match tstInfo",
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private async verifySignature(key: CryptoKey): Promise<void> {
|
|
136
|
-
// Encode the signed attributes for verification
|
|
137
|
-
const signedAttrs = this.signedAttrsObj.toDER();
|
|
138
|
-
signedAttrs[0] = 0x31; // Change context-specific tag to SET
|
|
139
|
-
|
|
140
|
-
const oid = this.signatureAlgorithmObj.subs[0].toOID();
|
|
141
|
-
// Try both ECDSA and RSA signature algorithms
|
|
142
|
-
const algo = ECDSA_SIGNATURE_ALGOS[oid] || RSA_SIGNATURE_ALGOS[oid];
|
|
143
|
-
|
|
144
|
-
if (!algo) {
|
|
145
|
-
throw new RFC3161TimestampVerificationError(
|
|
146
|
-
`Unsupported signature algorithm OID: ${oid}`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Check that the signature is valid for the signed attributes
|
|
151
|
-
const verified = await verifySignature(
|
|
152
|
-
key,
|
|
153
|
-
signedAttrs,
|
|
154
|
-
this.signatureValue,
|
|
155
|
-
algo,
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
if (!verified) {
|
|
159
|
-
throw new RFC3161TimestampVerificationError(
|
|
160
|
-
"signature verification failed",
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
|
|
166
|
-
private get pkiStatusInfoObj(): ASN1Obj {
|
|
167
|
-
// pkiStatusInfo is the first element of the timestamp response sequence
|
|
168
|
-
return this.root.subs[0];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
|
|
172
|
-
private get timeStampTokenObj(): ASN1Obj {
|
|
173
|
-
// timeStampToken is the first element of the timestamp response sequence
|
|
174
|
-
return this.root.subs[1];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-3
|
|
178
|
-
private get contentTypeObj(): ASN1Obj {
|
|
179
|
-
return this.timeStampTokenObj.subs[0];
|
|
180
|
-
}
|
|
181
|
-
// https://www.rfc-editor.org/rfc/rfc5652#section-3
|
|
182
|
-
private get signedDataObj(): ASN1Obj {
|
|
183
|
-
const obj = this.timeStampTokenObj.subs.find((sub) =>
|
|
184
|
-
sub.tag.isContextSpecific(0x00),
|
|
185
|
-
);
|
|
186
|
-
if (!obj) {
|
|
187
|
-
throw new RFC3161TimestampVerificationError(
|
|
188
|
-
"Missing timeStampTokenObj sub.",
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
return obj.subs[0];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.1
|
|
195
|
-
private get encapContentInfoObj(): ASN1Obj {
|
|
196
|
-
return this.signedDataObj.subs[2];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.1
|
|
200
|
-
private get signerInfosObj(): ASN1Obj {
|
|
201
|
-
// SignerInfos is the last element of the signed data sequence
|
|
202
|
-
const sd = this.signedDataObj;
|
|
203
|
-
return sd.subs[sd.subs.length - 1];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// https://www.rfc-editor.org/rfc/rfc5652#section-5.1
|
|
207
|
-
private get signerInfoObj(): ASN1Obj {
|
|
208
|
-
// Only supporting one signer
|
|
209
|
-
return this.signerInfosObj.subs[0];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.2
|
|
213
|
-
private get eContentTypeObj(): ASN1Obj {
|
|
214
|
-
return this.encapContentInfoObj.subs[0];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.2
|
|
218
|
-
private get eContentObj(): ASN1Obj {
|
|
219
|
-
return this.encapContentInfoObj.subs[1];
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.3
|
|
223
|
-
private get signedAttrsObj(): ASN1Obj {
|
|
224
|
-
const signedAttrs = this.signerInfoObj.subs.find((sub) =>
|
|
225
|
-
sub.tag.isContextSpecific(0x00),
|
|
226
|
-
);
|
|
227
|
-
if (!signedAttrs) {
|
|
228
|
-
throw new RFC3161TimestampVerificationError("Missing signedAttrsObj.");
|
|
229
|
-
}
|
|
230
|
-
return signedAttrs;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.3
|
|
234
|
-
private get messageDigestAttributeObj(): ASN1Obj {
|
|
235
|
-
const messageDigest = this.signedAttrsObj.subs.find(
|
|
236
|
-
(sub) =>
|
|
237
|
-
sub.subs[0].tag.isOID() &&
|
|
238
|
-
sub.subs[0].toOID() === OID_PKCS9_MESSAGE_DIGEST_KEY,
|
|
239
|
-
);
|
|
240
|
-
if (!messageDigest) {
|
|
241
|
-
throw new RFC3161TimestampVerificationError("Missing messageDigest.");
|
|
242
|
-
}
|
|
243
|
-
return messageDigest;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.3
|
|
247
|
-
private get signerSidObj(): ASN1Obj {
|
|
248
|
-
return this.signerInfoObj.subs[1];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.3
|
|
252
|
-
private get signerDigestAlgorithmObj(): ASN1Obj {
|
|
253
|
-
// Signature is the 2nd element of the signerInfoObj object
|
|
254
|
-
return this.signerInfoObj.subs[2];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.3
|
|
258
|
-
private get signatureAlgorithmObj(): ASN1Obj {
|
|
259
|
-
// Signature is the 4th element of the signerInfoObj object
|
|
260
|
-
return this.signerInfoObj.subs[4];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// https://datatracker.ietf.org/doc/html/rfc5652#section-5.3
|
|
264
|
-
private get signatureValueObj(): ASN1Obj {
|
|
265
|
-
// Signature is the 6th element of the signerInfoObj object
|
|
266
|
-
return this.signerInfoObj.subs[5];
|
|
267
|
-
}
|
|
268
|
-
}
|
package/src/rfc3161/tstinfo.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright 2023 The Sigstore Authors.
|
|
3
|
-
|
|
4
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
you may not use this file except in compliance with the License.
|
|
6
|
-
You may obtain a copy of the License at
|
|
7
|
-
|
|
8
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
|
|
10
|
-
Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
See the License for the specific language governing permissions and
|
|
14
|
-
limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { ASN1Obj, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
17
|
-
import { HashAlgorithms } from "../interfaces.js";
|
|
18
|
-
import { SHA2_HASH_ALGOS } from "../oid.js";
|
|
19
|
-
import { RFC3161TimestampVerificationError } from "./error.js";
|
|
20
|
-
|
|
21
|
-
export class TSTInfo {
|
|
22
|
-
public root: ASN1Obj;
|
|
23
|
-
|
|
24
|
-
constructor(asn1: ASN1Obj) {
|
|
25
|
-
this.root = asn1;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
get version(): bigint {
|
|
29
|
-
return this.root.subs[0].toInteger();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
get genTime(): Date {
|
|
33
|
-
return this.root.subs[4].toDate();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get messageImprintHashAlgorithm(): string {
|
|
37
|
-
const oid = this.messageImprintObj.subs[0].subs[0].toOID();
|
|
38
|
-
const algo = SHA2_HASH_ALGOS[oid];
|
|
39
|
-
if (!algo) {
|
|
40
|
-
throw new Error(`Unknown message imprint hash algorithm OID: ${oid}`);
|
|
41
|
-
}
|
|
42
|
-
return algo;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
get messageImprintHashedMessage(): Uint8Array {
|
|
46
|
-
return this.messageImprintObj.subs[1].value;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
get raw(): Uint8Array {
|
|
50
|
-
return this.root.toDER();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public async verify(data: Uint8Array): Promise<void> {
|
|
54
|
-
const hashAlg = this.messageImprintHashAlgorithm;
|
|
55
|
-
|
|
56
|
-
// Convert hash algorithm name to the format expected by WebCrypto
|
|
57
|
-
const hashAlgName = hashAlg === "sha256" ? HashAlgorithms.SHA256 :
|
|
58
|
-
hashAlg === "sha384" ? HashAlgorithms.SHA384 :
|
|
59
|
-
hashAlg === "sha512" ? HashAlgorithms.SHA512 : hashAlg;
|
|
60
|
-
|
|
61
|
-
const digest = await crypto.subtle.digest(
|
|
62
|
-
hashAlgName,
|
|
63
|
-
data as BufferSource,
|
|
64
|
-
);
|
|
65
|
-
if (
|
|
66
|
-
!uint8ArrayEqual(new Uint8Array(digest), this.messageImprintHashedMessage)
|
|
67
|
-
) {
|
|
68
|
-
throw new RFC3161TimestampVerificationError(
|
|
69
|
-
"message imprint does not match artifact",
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
|
|
75
|
-
private get messageImprintObj(): ASN1Obj {
|
|
76
|
-
return this.root.subs[2];
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import { base64ToUint8Array, canonicalize, hexToUint8Array, Uint8ArrayToBase64, Uint8ArrayToHex } from "@freedomofpress/crypto-browser";
|
|
2
|
-
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
3
|
-
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { bundle, digestHex, identity, issuer, trustRoot } from "../test/fixtures/sigstore.js";
|
|
6
|
-
import { assertBundle, type TLogEntry } from "./bundle.js";
|
|
7
|
-
import type { RawLog } from "./interfaces.js";
|
|
8
|
-
import { AllOf, AnyOf } from "./policy.js";
|
|
9
|
-
import { SigstoreVerifier, type SigstoreVerifierOptions } from "./sigstore.js";
|
|
10
|
-
import { verifyBundleTimestamp } from "./timestamp/tsa.js";
|
|
11
|
-
import { X509Certificate } from "./x509/cert.js";
|
|
12
|
-
|
|
13
|
-
// Keep real timestamp verification by default; individual tests supply already-verified
|
|
14
|
-
// timestamps to isolate threshold accounting from the separately tested cryptography.
|
|
15
|
-
vi.mock("./timestamp/tsa.js", async (importOriginal) => {
|
|
16
|
-
const actual = await importOriginal<typeof import("./timestamp/tsa.js")>();
|
|
17
|
-
return { ...actual, verifyBundleTimestamp: vi.fn(actual.verifyBundleTimestamp) };
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
vi.mocked(verifyBundleTimestamp).mockReset();
|
|
22
|
-
vi.restoreAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const digest = hexToUint8Array(digestHex);
|
|
26
|
-
const signingTime = new Date(Number(bundle().verificationMaterial.tlogEntries[0].integratedTime) * 1000);
|
|
27
|
-
const verify = (v: SigstoreVerifier, b = bundle(), data = digest) =>
|
|
28
|
-
v.verifyArtifact(identity, issuer, b, data, true);
|
|
29
|
-
|
|
30
|
-
async function verifier(options: SigstoreVerifierOptions = {}) {
|
|
31
|
-
const v = new SigstoreVerifier(options);
|
|
32
|
-
await v.loadSigstoreRoot(trustRoot());
|
|
33
|
-
return v;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Real SET signatures over the fixture's body, with independent test log keys.
|
|
37
|
-
async function signedLog(): Promise<{ log: RawLog; entry: TLogEntry }> {
|
|
38
|
-
const secret = ed25519.utils.randomSecretKey();
|
|
39
|
-
const spki = new Uint8Array([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, ...ed25519.getPublicKey(secret)]);
|
|
40
|
-
const logID = new Uint8Array(await crypto.subtle.digest("SHA-256", spki));
|
|
41
|
-
const log: RawLog = {
|
|
42
|
-
baseUrl: "https://log.test",
|
|
43
|
-
hashAlgorithm: "SHA2_256",
|
|
44
|
-
publicKey: { rawBytes: Uint8ArrayToBase64(spki), keyDetails: "PKIX_ED25519", validFor: { start: "2025-01-01T00:00:00Z" } },
|
|
45
|
-
logId: { keyId: Uint8ArrayToBase64(logID) },
|
|
46
|
-
};
|
|
47
|
-
const entry = bundle().verificationMaterial.tlogEntries[0];
|
|
48
|
-
entry.logId = log.logId;
|
|
49
|
-
delete entry.inclusionProof;
|
|
50
|
-
const signed = new TextEncoder().encode(canonicalize({
|
|
51
|
-
body: entry.canonicalizedBody,
|
|
52
|
-
integratedTime: Number(entry.integratedTime),
|
|
53
|
-
logIndex: Number(entry.logIndex),
|
|
54
|
-
logID: Uint8ArrayToHex(logID),
|
|
55
|
-
}));
|
|
56
|
-
entry.inclusionPromise = { signedEntryTimestamp: Uint8ArrayToBase64(ed25519.sign(signed, secret)) };
|
|
57
|
-
return { log, entry };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const operatorCases = [
|
|
61
|
-
{ name: "anonymous authorities retain threshold-one support", operators: ["", ""], threshold: 1, passes: true },
|
|
62
|
-
{ name: "anonymous authorities cannot establish independence", operators: ["", ""], threshold: 2, passes: false },
|
|
63
|
-
{ name: "anonymous plus named does not count twice", operators: ["", "a.test"], threshold: 2, passes: false },
|
|
64
|
-
{ name: "named plus anonymous does not count twice", operators: ["a.test", ""], threshold: 2, passes: false },
|
|
65
|
-
{ name: "multiple keys from one named operator count once", operators: ["a.test", "a.test"], threshold: 2, passes: false },
|
|
66
|
-
{ name: "distinct named operators satisfy the threshold", operators: ["a.test", "b.test"], threshold: 2, passes: true },
|
|
67
|
-
{ name: "an anonymous authority cannot add a third operator", operators: ["a.test", "", "b.test"], threshold: 3, passes: false },
|
|
68
|
-
{ name: "an anonymous authority does not invalidate two named operators", operators: ["a.test", "", "b.test"], threshold: 2, passes: true },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
describe("Operator thresholds", () => {
|
|
72
|
-
let signedLogs: Awaited<ReturnType<typeof signedLog>>[];
|
|
73
|
-
beforeAll(async () => { signedLogs = await Promise.all([signedLog(), signedLog(), signedLog()]); });
|
|
74
|
-
|
|
75
|
-
it.each(operatorCases)("Rekor: $name", async ({ operators, threshold, passes }) => {
|
|
76
|
-
const root = trustRoot();
|
|
77
|
-
root.tlogs = operators.map((operator, i) => ({ ...signedLogs[i].log, operator: operator || undefined }));
|
|
78
|
-
const b = bundle();
|
|
79
|
-
b.mediaType = "application/vnd.dev.sigstore.bundle+json;version=0.1";
|
|
80
|
-
b.verificationMaterial.tlogEntries = operators.map((_, i) => signedLogs[i].entry);
|
|
81
|
-
const v = new SigstoreVerifier({ tlogThreshold: threshold });
|
|
82
|
-
await v.loadSigstoreRoot(root);
|
|
83
|
-
const result = verify(v, b);
|
|
84
|
-
if (passes) await expect(result).resolves.toBe(true);
|
|
85
|
-
else await expect(result).rejects.toThrow("Not enough verified transparency logs");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it.each(operatorCases)("CT: $name", async ({ operators, threshold, passes }) => {
|
|
89
|
-
const v = await verifier({ ctlogThreshold: threshold });
|
|
90
|
-
// SCT verification itself is covered by the real fixture tests.
|
|
91
|
-
vi.spyOn(v, "verifySCT").mockResolvedValue(new Set(operators));
|
|
92
|
-
const result = verify(v);
|
|
93
|
-
if (passes) await expect(result).resolves.toBe(true);
|
|
94
|
-
else await expect(result).rejects.toThrow("Not enough verified CT log operators");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it.each(operatorCases)("TSA: $name", async ({ operators, threshold, passes }) => {
|
|
98
|
-
vi.mocked(verifyBundleTimestamp).mockResolvedValue(operators.map((operator) => ({ signingTime, operator })));
|
|
99
|
-
const result = verify(await verifier({ tsaThreshold: threshold }));
|
|
100
|
-
if (passes) await expect(result).resolves.toBe(true);
|
|
101
|
-
else await expect(result).rejects.toThrow("Not enough verified TSA operators");
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("Small input hardening", () => {
|
|
106
|
-
it("awaits asynchronous custom policies and policy combinators", async () => {
|
|
107
|
-
const v = await verifier();
|
|
108
|
-
const rejection = {
|
|
109
|
-
verify: async () => { throw new Error("asynchronous policy rejected"); },
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
await expect(v.verifyArtifactPolicy(rejection, bundle(), digest, true))
|
|
113
|
-
.rejects.toThrow("asynchronous policy rejected");
|
|
114
|
-
await expect(v.verifyArtifactPolicy(new AllOf([rejection]), bundle(), digest, true))
|
|
115
|
-
.rejects.toThrow("asynchronous policy rejected");
|
|
116
|
-
await expect(v.verifyArtifactPolicy(new AnyOf([rejection]), bundle(), digest, true))
|
|
117
|
-
.rejects.toThrow("0 of 1 policies succeeded");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("rejects malformed authority validity periods while loading the root", async () => {
|
|
121
|
-
for (let i = 0; i < 4; i++) {
|
|
122
|
-
const root = trustRoot();
|
|
123
|
-
const periods = [
|
|
124
|
-
root.tlogs[0].publicKey.validFor,
|
|
125
|
-
root.ctlogs[0].publicKey.validFor,
|
|
126
|
-
root.certificateAuthorities[0].validFor,
|
|
127
|
-
root.timestampAuthorities[0].validFor,
|
|
128
|
-
];
|
|
129
|
-
periods[i].start = "not-a-date";
|
|
130
|
-
await expect(new SigstoreVerifier().loadSigstoreRoot(root))
|
|
131
|
-
.rejects.toThrow("Invalid authority validity period");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const root = trustRoot();
|
|
135
|
-
root.tlogs[0].publicKey.validFor = {
|
|
136
|
-
start: "2026-01-02T00:00:00Z",
|
|
137
|
-
end: "2026-01-01T00:00:00Z",
|
|
138
|
-
};
|
|
139
|
-
await expect(new SigstoreVerifier().loadSigstoreRoot(root))
|
|
140
|
-
.rejects.toThrow("Invalid authority validity period");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("requires non-negative safe-integer thresholds", () => {
|
|
144
|
-
for (const key of ["tlogThreshold", "ctlogThreshold", "tsaThreshold"] as const) {
|
|
145
|
-
for (const value of [NaN, Infinity, -Infinity, -1, 0.5, Number.MAX_SAFE_INTEGER + 1, "2"]) {
|
|
146
|
-
expect(() => new SigstoreVerifier({ [key]: value } as SigstoreVerifierOptions)).toThrow(`${key} must be a non-negative safe integer`);
|
|
147
|
-
}
|
|
148
|
-
for (const value of [0, 1, 2]) expect(() => new SigstoreVerifier({ [key]: value })).not.toThrow();
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("rejects digest-only inputs that are not 32 bytes", async () => {
|
|
153
|
-
const v = await verifier();
|
|
154
|
-
for (const length of [0, 31, 33, 64]) {
|
|
155
|
-
await expect(verify(v, bundle(), new Uint8Array(length))).rejects.toThrow("SHA-256 digest must be exactly 32 bytes");
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("rejects oversized or unrepresentable log numbers before cryptography", () => {
|
|
160
|
-
for (const field of ["logIndex", "integratedTime"] as const) {
|
|
161
|
-
for (const value of ["9".repeat(10000), "9223372036854775808", "-1", "1.5"]) {
|
|
162
|
-
const b = bundle();
|
|
163
|
-
b.verificationMaterial.tlogEntries[0][field] = value;
|
|
164
|
-
expect(() => assertBundle(b)).toThrow("Invalid bundle");
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const unsafe = bundle();
|
|
168
|
-
unsafe.verificationMaterial.tlogEntries[0].logIndex = "9007199254740993";
|
|
169
|
-
expect(() => assertBundle(unsafe)).toThrow("inclusion promise");
|
|
170
|
-
const invalidDate = bundle();
|
|
171
|
-
invalidDate.verificationMaterial.tlogEntries[0].integratedTime = "8640000000001";
|
|
172
|
-
expect(() => assertBundle(invalidDate)).toThrow("integratedTime");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("bounds proof integers and sibling counts while preserving full int64 precision", () => {
|
|
176
|
-
for (const field of ["logIndex", "treeSize"] as const) {
|
|
177
|
-
const b = bundle();
|
|
178
|
-
const proof = b.verificationMaterial.tlogEntries[0].inclusionProof;
|
|
179
|
-
if (!proof) throw new Error("Fixture must contain an inclusion proof");
|
|
180
|
-
proof[field] = "9223372036854775808";
|
|
181
|
-
expect(() => assertBundle(b)).toThrow("inclusion proof");
|
|
182
|
-
}
|
|
183
|
-
const b = bundle();
|
|
184
|
-
const entry = b.verificationMaterial.tlogEntries[0];
|
|
185
|
-
const proof = entry.inclusionProof;
|
|
186
|
-
if (!proof) throw new Error("Fixture must contain an inclusion proof");
|
|
187
|
-
delete entry.inclusionPromise;
|
|
188
|
-
entry.logIndex = "9223372036854775806";
|
|
189
|
-
proof.logIndex = entry.logIndex;
|
|
190
|
-
proof.treeSize = "9223372036854775807";
|
|
191
|
-
proof.hashes = Array(63).fill(Uint8ArrayToBase64(new Uint8Array(32)));
|
|
192
|
-
expect(() => assertBundle(b)).not.toThrow();
|
|
193
|
-
proof.hashes.push(proof.hashes[0]);
|
|
194
|
-
expect(() => assertBundle(b)).toThrow("inclusion proof");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("enforces the timestamp cap through the exported helper too", async () => {
|
|
198
|
-
await expect(verifyBundleTimestamp({ rfc3161Timestamps: Array(33).fill({ signedTimestamp: "AA==" }) }, digest, []))
|
|
199
|
-
.rejects.toThrow("Invalid bundle: rfc3161Timestamps");
|
|
200
|
-
await expect(verifyBundleTimestamp(undefined, digest, [])).resolves.toEqual([]);
|
|
201
|
-
await expect(verifyBundleTimestamp({}, digest, [])).resolves.toEqual([]);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("bounds encoded bundle data and certificate-chain length", async () => {
|
|
205
|
-
const oversized = "A".repeat(16 * 1024 * 1024 + 1);
|
|
206
|
-
const b = bundle();
|
|
207
|
-
if (!b.dsseEnvelope) throw new Error("Fixture must contain a DSSE envelope");
|
|
208
|
-
b.dsseEnvelope.payload = oversized;
|
|
209
|
-
expect(() => assertBundle(b)).toThrow("Invalid bundle");
|
|
210
|
-
await expect(verifyBundleTimestamp(
|
|
211
|
-
{ rfc3161Timestamps: [{ signedTimestamp: oversized }] },
|
|
212
|
-
digest,
|
|
213
|
-
[],
|
|
214
|
-
)).rejects.toThrow("Invalid bundle");
|
|
215
|
-
|
|
216
|
-
const chained = bundle();
|
|
217
|
-
const cert = chained.verificationMaterial.certificate;
|
|
218
|
-
if (!cert) throw new Error("Fixture must contain a certificate");
|
|
219
|
-
delete chained.verificationMaterial.certificate;
|
|
220
|
-
chained.verificationMaterial.x509CertificateChain = {
|
|
221
|
-
certificates: Array(33).fill(cert),
|
|
222
|
-
};
|
|
223
|
-
expect(() => assertBundle(chained)).toThrow("Invalid bundle: certificate");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("rejects malformed or excessive subjects and digest maps", async () => {
|
|
227
|
-
const v = await verifier();
|
|
228
|
-
const b = bundle();
|
|
229
|
-
const cert = b.verificationMaterial.certificate;
|
|
230
|
-
const env = b.dsseEnvelope;
|
|
231
|
-
if (!cert || !env) throw new Error("Fixture must contain a certificate and DSSE envelope");
|
|
232
|
-
const signingCert = X509Certificate.parse(base64ToUint8Array(cert.rawBytes));
|
|
233
|
-
const signature = base64ToUint8Array(env.signatures[0].sig);
|
|
234
|
-
// Isolate parsing of authenticated payloads; the real bundle tests exercise all trust checks.
|
|
235
|
-
vi.spyOn(v as unknown as { verifyBundle: () => Promise<{ signingCert: X509Certificate; signature: Uint8Array }> }, "verifyBundle")
|
|
236
|
-
.mockResolvedValue({ signingCert, signature });
|
|
237
|
-
const subject = { digest: { sha256: digestHex } };
|
|
238
|
-
for (const payload of [null, {}, { subject: [] }, { subject: Array(1025).fill(subject) }]) {
|
|
239
|
-
env.payload = Uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify(payload)));
|
|
240
|
-
await expect(verify(v, b)).rejects.toThrow("DSSE payload must have between 1 and 1024 subjects");
|
|
241
|
-
}
|
|
242
|
-
for (const invalid of [null, { digest: [] }, { digest: Object.fromEntries(Array.from({ length: 33 }, (_, i) => [`hash${i}`, "00"])) }]) {
|
|
243
|
-
env.payload = Uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify({ subject: [invalid, subject] })));
|
|
244
|
-
await expect(verify(v, b)).rejects.toThrow("Invalid DSSE subject digest map");
|
|
245
|
-
}
|
|
246
|
-
env.payload = Uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify({ subject: [{ digest: { sha256: 123 } }] })));
|
|
247
|
-
await expect(verify(v, b)).rejects.toThrow("does not match any subject");
|
|
248
|
-
});
|
|
249
|
-
});
|