@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/timestamp/tsa.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* TSA (Time-Stamping Authority) timestamp verification
|
|
3
|
-
*
|
|
4
|
-
* Based on sigstore-js:
|
|
5
|
-
* https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/timestamp/tsa.ts
|
|
6
|
-
*
|
|
7
|
-
* Key differences from sigstore-js:
|
|
8
|
-
* - Browser-compatible: uses Uint8Array instead of Buffer for binary data
|
|
9
|
-
* - Uses Web Crypto API for all cryptographic operations
|
|
10
|
-
* - Integrates with existing RFC3161 timestamp implementation
|
|
11
|
-
* - Uses CertificateChainVerifier for full X.509 path validation
|
|
12
|
-
* - Implements filterCertAuthorities inline (from sigstore-js/packages/verify/src/trust/filter.ts)
|
|
13
|
-
* - Adds verifyBundleTimestamp for Sigstore bundle integration (new functionality)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { base64ToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
17
|
-
import { assertTimestampVerificationData } from "../bundle.js";
|
|
18
|
-
import { CertificateChainVerifier, X509Certificate } from "../x509/index.js";
|
|
19
|
-
import { parseValidityPeriod, RawTimestampAuthority } from "../interfaces.js";
|
|
20
|
-
import { RFC3161Timestamp } from "../rfc3161/index.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Verifies an RFC3161 timestamp against a set of timestamp authorities
|
|
24
|
-
*
|
|
25
|
-
* @param timestamp - The RFC3161 timestamp to verify
|
|
26
|
-
* @param data - The data that was timestamped
|
|
27
|
-
* @param timestampAuthorities - List of trusted timestamp authorities
|
|
28
|
-
* @returns The verified signing time
|
|
29
|
-
* @throws Error if timestamp cannot be verified
|
|
30
|
-
*/
|
|
31
|
-
export async function verifyRFC3161Timestamp(
|
|
32
|
-
timestamp: RFC3161Timestamp,
|
|
33
|
-
data: Uint8Array,
|
|
34
|
-
timestampAuthorities: RawTimestampAuthority[]
|
|
35
|
-
): Promise<Date> {
|
|
36
|
-
return (await verifyTimestamp(timestamp, data, timestampAuthorities)).signingTime;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface VerifiedTimestamp {
|
|
40
|
-
signingTime: Date;
|
|
41
|
-
// Empty means unknown, which cannot add independence to named operators.
|
|
42
|
-
operator: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function verifyTimestamp(
|
|
46
|
-
timestamp: RFC3161Timestamp,
|
|
47
|
-
data: Uint8Array,
|
|
48
|
-
timestampAuthorities: RawTimestampAuthority[]
|
|
49
|
-
): Promise<VerifiedTimestamp> {
|
|
50
|
-
const signingTime = timestamp.signingTime;
|
|
51
|
-
|
|
52
|
-
// Filter for CAs which were valid at the time of signing
|
|
53
|
-
let validAuthorities = filterCertAuthorities(
|
|
54
|
-
timestampAuthorities,
|
|
55
|
-
signingTime
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// Filter for CAs which match serial and issuer embedded in the timestamp
|
|
59
|
-
validAuthorities = filterCAsBySerialAndIssuer(validAuthorities, {
|
|
60
|
-
serialNumber: timestamp.signerSerialNumber,
|
|
61
|
-
issuer: timestamp.signerIssuer,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Check that we can verify the timestamp with AT LEAST ONE of the remaining CAs
|
|
65
|
-
const verificationResults = await Promise.allSettled(
|
|
66
|
-
validAuthorities.map(ca => verifyTimestampForCA(timestamp, data, ca))
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const verified = verificationResults.findIndex(
|
|
70
|
-
result => result.status === "fulfilled"
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (verified < 0) {
|
|
74
|
-
const errors = verificationResults
|
|
75
|
-
.filter(r => r.status === "rejected")
|
|
76
|
-
.map(r => (r as PromiseRejectedResult).reason?.message || 'Unknown error');
|
|
77
|
-
throw new Error(`Timestamp could not be verified against any trusted authority. Errors: ${errors.join(', ')}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const ca = validAuthorities[verified];
|
|
81
|
-
return { signingTime, operator: ca.operator || "" };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Filters certificate authorities to those valid at a specific time
|
|
86
|
-
*
|
|
87
|
-
* Adapted from sigstore-js/packages/verify/src/trust/filter.ts
|
|
88
|
-
*/
|
|
89
|
-
function filterCertAuthorities(
|
|
90
|
-
authorities: RawTimestampAuthority[],
|
|
91
|
-
validAt: Date
|
|
92
|
-
): RawTimestampAuthority[] {
|
|
93
|
-
return authorities.filter(ca => {
|
|
94
|
-
const { start, end } = parseValidityPeriod(ca.validFor);
|
|
95
|
-
return start <= validAt && validAt <= end;
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Filters certificate authorities by serial number and issuer
|
|
101
|
-
*
|
|
102
|
-
* Only checks the LEAF certificate (TSA signing cert), not the entire chain.
|
|
103
|
-
* This matches the sigstore-js reference implementation.
|
|
104
|
-
*/
|
|
105
|
-
function filterCAsBySerialAndIssuer(
|
|
106
|
-
timestampAuthorities: RawTimestampAuthority[],
|
|
107
|
-
criteria: { serialNumber: Uint8Array; issuer: Uint8Array }
|
|
108
|
-
): RawTimestampAuthority[] {
|
|
109
|
-
return timestampAuthorities.filter(ca => {
|
|
110
|
-
if (!ca.certChain || ca.certChain.certificates.length === 0) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const leafCert = X509Certificate.parse(
|
|
115
|
-
base64ToUint8Array(ca.certChain.certificates[0].rawBytes)
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
return uint8ArrayEqual(leafCert.serialNumber, criteria.serialNumber) &&
|
|
119
|
-
uint8ArrayEqual(leafCert.issuer, criteria.issuer);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Verifies a timestamp against a specific certificate authority
|
|
125
|
-
*/
|
|
126
|
-
async function verifyTimestampForCA(
|
|
127
|
-
timestamp: RFC3161Timestamp,
|
|
128
|
-
data: Uint8Array,
|
|
129
|
-
ca: RawTimestampAuthority
|
|
130
|
-
): Promise<void> {
|
|
131
|
-
if (!ca.certChain || ca.certChain.certificates.length === 0) {
|
|
132
|
-
throw new Error("Certificate authority missing certificate chain");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const leafCert = X509Certificate.parse(
|
|
136
|
-
base64ToUint8Array(ca.certChain.certificates[0].rawBytes)
|
|
137
|
-
);
|
|
138
|
-
const signingTime = timestamp.signingTime;
|
|
139
|
-
|
|
140
|
-
const trustedCerts = ca.certChain.certificates
|
|
141
|
-
.slice(1)
|
|
142
|
-
.map(cert => X509Certificate.parse(base64ToUint8Array(cert.rawBytes)));
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const verifier = new CertificateChainVerifier({
|
|
146
|
-
untrustedCert: leafCert,
|
|
147
|
-
trustedCerts: trustedCerts,
|
|
148
|
-
timestamp: signingTime,
|
|
149
|
-
});
|
|
150
|
-
await verifier.verify();
|
|
151
|
-
} catch (e) {
|
|
152
|
-
throw new Error(`TSA certificate chain verification failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const publicKey = await leafCert.publicKeyObj;
|
|
156
|
-
|
|
157
|
-
await timestamp.verify(data, publicKey);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Verifies all timestamps in a bundle's timestamp verification data
|
|
162
|
-
*
|
|
163
|
-
* Matches sigstore-js behavior: all timestamps must successfully verify.
|
|
164
|
-
* Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L64-L108
|
|
165
|
-
*
|
|
166
|
-
* @param timestampData - The timestamp verification data from the bundle
|
|
167
|
-
* @param signature - The signature being verified
|
|
168
|
-
* @param timestampAuthorities - List of trusted timestamp authorities
|
|
169
|
-
* @returns The verified signing times and their operators (empty if no timestamps present)
|
|
170
|
-
* @throws Error if any timestamp fails verification or duplicates are found
|
|
171
|
-
*/
|
|
172
|
-
export async function verifyBundleTimestamp(
|
|
173
|
-
timestampData: any,
|
|
174
|
-
signature: Uint8Array,
|
|
175
|
-
timestampAuthorities: RawTimestampAuthority[]
|
|
176
|
-
): Promise<VerifiedTimestamp[]> {
|
|
177
|
-
assertTimestampVerificationData(timestampData);
|
|
178
|
-
if (!timestampData?.rfc3161Timestamps?.length) {
|
|
179
|
-
return [];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Verify ALL timestamps - each must succeed (matches sigstore-js behavior)
|
|
183
|
-
// Collect verification results for duplicate checking after verification
|
|
184
|
-
// Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L86-L92
|
|
185
|
-
const verifiedResults: Array<VerifiedTimestamp & { signerSerialNumber: string }> = [];
|
|
186
|
-
|
|
187
|
-
for (const tsData of timestampData.rfc3161Timestamps) {
|
|
188
|
-
// Decode the base64-encoded timestamp
|
|
189
|
-
const timestampBytes = base64ToUint8Array(tsData.signedTimestamp);
|
|
190
|
-
const timestamp = RFC3161Timestamp.parse(timestampBytes);
|
|
191
|
-
|
|
192
|
-
verifiedResults.push({
|
|
193
|
-
...(await verifyTimestamp(timestamp, signature, timestampAuthorities)),
|
|
194
|
-
signerSerialNumber: Array.from(timestamp.signerSerialNumber).join(','),
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Check for duplicate timestamps using deep equality on parsed values
|
|
199
|
-
// sigstore-js uses isDeepStrictEqual on {type, logID, timestamp} objects
|
|
200
|
-
for (let i = 0; i < verifiedResults.length; i++) {
|
|
201
|
-
for (let j = i + 1; j < verifiedResults.length; j++) {
|
|
202
|
-
if (
|
|
203
|
-
verifiedResults[i].signingTime.getTime() === verifiedResults[j].signingTime.getTime() &&
|
|
204
|
-
verifiedResults[i].signerSerialNumber === verifiedResults[j].signerSerialNumber
|
|
205
|
-
) {
|
|
206
|
-
throw new Error("Duplicate TSA timestamp detected");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return verifiedResults;
|
|
212
|
-
}
|
package/src/tlog/body.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Transparency log body verification
|
|
3
|
-
*
|
|
4
|
-
* Based on sigstore-js:
|
|
5
|
-
* https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/tlog/index.ts
|
|
6
|
-
*
|
|
7
|
-
* Key differences from sigstore-js:
|
|
8
|
-
* - Browser-compatible: uses Uint8Array instead of Buffer for binary data
|
|
9
|
-
* - Direct bundle comparison instead of SignatureContent abstraction
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { base64Decode, base64ToUint8Array, Uint8ArrayToString } from "@freedomofpress/crypto-browser";
|
|
13
|
-
import type { SigstoreBundle, TLogEntry } from "../bundle.js";
|
|
14
|
-
import { X509Certificate } from "../x509/cert.js";
|
|
15
|
-
import { verifyHashedRekordBody } from "./hashedrekord.js";
|
|
16
|
-
import { verifyDSSEBody } from "./dsse.js";
|
|
17
|
-
import { verifyIntotoBody } from "./intoto.js";
|
|
18
|
-
|
|
19
|
-
export interface RekorEntry {
|
|
20
|
-
apiVersion: string;
|
|
21
|
-
kind: string;
|
|
22
|
-
spec: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Checks that the entry body matches the bundle content and was logged under the signing certificate.
|
|
26
|
-
export async function verifyTLogBody(
|
|
27
|
-
entry: TLogEntry,
|
|
28
|
-
bundle: SigstoreBundle,
|
|
29
|
-
cert: X509Certificate,
|
|
30
|
-
): Promise<void> {
|
|
31
|
-
const rekorEntry = parseCanonicalBody(entry);
|
|
32
|
-
|
|
33
|
-
const { kind, version } = entry.kindVersion;
|
|
34
|
-
|
|
35
|
-
if (kind !== rekorEntry.kind || version !== rekorEntry.apiVersion) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
`kind/version mismatch - expected: ${kind}/${version}, received: ${rekorEntry.kind}/${rekorEntry.apiVersion}`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
switch (rekorEntry.kind) {
|
|
42
|
-
case "hashedrekord":
|
|
43
|
-
return verifyHashedRekordBody(rekorEntry, bundle, cert);
|
|
44
|
-
case "dsse":
|
|
45
|
-
return verifyDSSEBody(rekorEntry, bundle, cert);
|
|
46
|
-
case "intoto":
|
|
47
|
-
return verifyIntotoBody(rekorEntry, bundle, cert);
|
|
48
|
-
default:
|
|
49
|
-
throw new Error(`Unsupported TLog entry kind: ${rekorEntry.kind}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Compares the certificate recorded in the entry (base64 DER, or base64 PEM for Rekor v1) with the signing certificate.
|
|
54
|
-
export function assertLoggedCertificate(
|
|
55
|
-
cert: X509Certificate,
|
|
56
|
-
encoded: unknown,
|
|
57
|
-
pem: boolean,
|
|
58
|
-
): void {
|
|
59
|
-
if (typeof encoded !== "string" || encoded === "") {
|
|
60
|
-
throw new Error("TLog entry does not record the signing certificate");
|
|
61
|
-
}
|
|
62
|
-
const bytes = base64ToUint8Array(encoded);
|
|
63
|
-
const logged = X509Certificate.parse(pem ? Uint8ArrayToString(bytes) : bytes);
|
|
64
|
-
if (!cert.equals(logged)) {
|
|
65
|
-
throw new Error("Certificate in TLog entry does not match the signing certificate");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function parseCanonicalBody(entry: TLogEntry): RekorEntry {
|
|
70
|
-
try {
|
|
71
|
-
const decodedBody = base64Decode(entry.canonicalizedBody);
|
|
72
|
-
const rekorEntry = JSON.parse(decodedBody) as RekorEntry;
|
|
73
|
-
|
|
74
|
-
if (!rekorEntry.apiVersion || !rekorEntry.kind || !rekorEntry.spec) {
|
|
75
|
-
throw new Error("Invalid Rekor entry structure");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return rekorEntry;
|
|
79
|
-
} catch (error) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Failed to parse canonicalized body: ${error instanceof Error ? error.message : String(error)}`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
package/src/tlog/checkpoint.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Checkpoint verification for transparency log entries.
|
|
3
|
-
|
|
4
|
-
Adapted from sigstore-js for browser compatibility:
|
|
5
|
-
https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/timestamp/checkpoint.ts
|
|
6
|
-
|
|
7
|
-
Key differences:
|
|
8
|
-
- Uses Uint8Array instead of Buffer
|
|
9
|
-
- Uses Web Crypto API instead of Node.js crypto module
|
|
10
|
-
|
|
11
|
-
Follows the signed note format specification:
|
|
12
|
-
https://github.com/transparency-dev/formats/blob/main/log/README.md
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
base64ToUint8Array,
|
|
17
|
-
stringToUint8Array,
|
|
18
|
-
uint8ArrayEqual,
|
|
19
|
-
verifySignature,
|
|
20
|
-
} from "@freedomofpress/crypto-browser";
|
|
21
|
-
import type { TLogEntry } from "../bundle.js";
|
|
22
|
-
import type { RekorKeyInfo } from "../interfaces.js";
|
|
23
|
-
|
|
24
|
-
// Signed note format per https://github.com/transparency-dev/formats
|
|
25
|
-
// Body is separated from signatures by a blank line
|
|
26
|
-
const CHECKPOINT_SEPARATOR = "\n\n";
|
|
27
|
-
// Signature lines format: "— <identity> <base64(key_hint+signature)>\n"
|
|
28
|
-
// \u2014 is the em-dash character (—)
|
|
29
|
-
const SIGNATURE_REGEX = /\u2014 (\S+) (\S+)\n/g;
|
|
30
|
-
|
|
31
|
-
export interface TLogSignature {
|
|
32
|
-
name: string;
|
|
33
|
-
keyHint: Uint8Array;
|
|
34
|
-
signature: Uint8Array;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Signed checkpoint note with cryptographic signatures
|
|
38
|
-
export class SignedNote {
|
|
39
|
-
readonly note: string;
|
|
40
|
-
readonly signatures: TLogSignature[];
|
|
41
|
-
|
|
42
|
-
constructor(note: string, signatures: TLogSignature[]) {
|
|
43
|
-
this.note = note;
|
|
44
|
-
this.signatures = signatures;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static fromString(envelope: string): SignedNote {
|
|
48
|
-
if (!envelope.includes(CHECKPOINT_SEPARATOR)) {
|
|
49
|
-
throw new Error("Missing checkpoint separator");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Split body from signature lines at blank line
|
|
53
|
-
const split = envelope.indexOf(CHECKPOINT_SEPARATOR);
|
|
54
|
-
const header = envelope.slice(0, split + 1);
|
|
55
|
-
const data = envelope.slice(split + CHECKPOINT_SEPARATOR.length);
|
|
56
|
-
|
|
57
|
-
// Parse signature lines: "— <identity> <base64(key_hint+signature)>\n"
|
|
58
|
-
const matches = data.matchAll(SIGNATURE_REGEX);
|
|
59
|
-
|
|
60
|
-
const signatures: TLogSignature[] = [];
|
|
61
|
-
for (const match of matches) {
|
|
62
|
-
const [, name, signature] = match;
|
|
63
|
-
const sigBytes = base64ToUint8Array(signature);
|
|
64
|
-
|
|
65
|
-
// First 4 bytes are key hint (SHA256 hash prefix), rest is signature
|
|
66
|
-
if (sigBytes.length < 5) {
|
|
67
|
-
throw new Error("Malformed checkpoint signature");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
signatures.push({
|
|
71
|
-
name,
|
|
72
|
-
keyHint: sigBytes.subarray(0, 4),
|
|
73
|
-
signature: sigBytes.subarray(4),
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (signatures.length === 0) {
|
|
78
|
-
throw new Error("No signatures found in checkpoint");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return new SignedNote(header, signatures);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Parsed checkpoint containing tree state (origin, size, root hash)
|
|
86
|
-
export class LogCheckpoint {
|
|
87
|
-
readonly origin: string;
|
|
88
|
-
readonly logSize: bigint;
|
|
89
|
-
readonly logHash: Uint8Array;
|
|
90
|
-
readonly rest: string[];
|
|
91
|
-
|
|
92
|
-
constructor(
|
|
93
|
-
origin: string,
|
|
94
|
-
logSize: bigint,
|
|
95
|
-
logHash: Uint8Array,
|
|
96
|
-
rest: string[]
|
|
97
|
-
) {
|
|
98
|
-
this.origin = origin;
|
|
99
|
-
this.logSize = logSize;
|
|
100
|
-
this.logHash = logHash;
|
|
101
|
-
this.rest = rest;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
static fromString(note: string): LogCheckpoint {
|
|
105
|
-
const lines = note.trimEnd().split("\n");
|
|
106
|
-
|
|
107
|
-
if (lines.length < 3) {
|
|
108
|
-
throw new Error("Too few lines in checkpoint header");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const origin = lines[0];
|
|
112
|
-
const logSize = BigInt(lines[1]);
|
|
113
|
-
const rootHash = base64ToUint8Array(lines[2]);
|
|
114
|
-
const rest = lines.slice(3);
|
|
115
|
-
|
|
116
|
-
return new LogCheckpoint(origin, logSize, rootHash, rest);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Verifies that the checkpoint is signed by the entry's log and describes the same tree as the inclusion proof.
|
|
121
|
-
export async function verifyCheckpoint(
|
|
122
|
-
entry: TLogEntry,
|
|
123
|
-
log: RekorKeyInfo
|
|
124
|
-
): Promise<void> {
|
|
125
|
-
const proof = entry.inclusionProof;
|
|
126
|
-
if (!proof?.checkpoint?.envelope) {
|
|
127
|
-
throw new Error("Missing checkpoint in inclusion proof");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const signedNote = SignedNote.fromString(proof.checkpoint.envelope);
|
|
131
|
-
const checkpoint = LogCheckpoint.fromString(signedNote.note);
|
|
132
|
-
const data = stringToUint8Array(signedNote.note);
|
|
133
|
-
|
|
134
|
-
// Signatures from other parties (e.g. witnesses) are ignored; the key hint is the first 4 bytes of the log ID.
|
|
135
|
-
let valid = false;
|
|
136
|
-
for (const sig of signedNote.signatures) {
|
|
137
|
-
if (
|
|
138
|
-
uint8ArrayEqual(sig.keyHint, log.logId.subarray(0, 4)) &&
|
|
139
|
-
(await verifySignature(log.publicKey, data, sig.signature, log.hashAlgorithm))
|
|
140
|
-
) {
|
|
141
|
-
valid = true;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (!valid) {
|
|
145
|
-
throw new Error("Invalid checkpoint signature");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
checkpoint.logSize !== BigInt(proof.treeSize) ||
|
|
150
|
-
!uint8ArrayEqual(checkpoint.logHash, base64ToUint8Array(proof.rootHash))
|
|
151
|
-
) {
|
|
152
|
-
throw new Error("Checkpoint does not match inclusion proof");
|
|
153
|
-
}
|
|
154
|
-
}
|
package/src/tlog/dsse.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* DSSE transparency log entry verification
|
|
3
|
-
*
|
|
4
|
-
* Based on sigstore-js:
|
|
5
|
-
* https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/tlog/dsse.ts
|
|
6
|
-
*
|
|
7
|
-
* Key differences from sigstore-js:
|
|
8
|
-
* - Browser-compatible: uses Uint8Array instead of Buffer for binary data
|
|
9
|
-
* - Direct bundle field comparison instead of SignatureContent abstraction
|
|
10
|
-
* - Uses crypto.subtle.digest for hash computation instead of Node.js crypto
|
|
11
|
-
* - Adds verifyDSSE002Body() - new functionality (reference only supports v0.0.1)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
15
|
-
import { getHashAlgorithm } from "../interfaces.js";
|
|
16
|
-
import type { SigstoreBundle } from "../bundle.js";
|
|
17
|
-
import type { X509Certificate } from "../x509/cert.js";
|
|
18
|
-
import { assertLoggedCertificate, type RekorEntry } from "./body.js";
|
|
19
|
-
|
|
20
|
-
interface DSSESpec {
|
|
21
|
-
signatures?: Array<{
|
|
22
|
-
signature: string;
|
|
23
|
-
verifier?: string;
|
|
24
|
-
keyid?: string;
|
|
25
|
-
}>;
|
|
26
|
-
payloadHash?: {
|
|
27
|
-
algorithm: string;
|
|
28
|
-
value: string;
|
|
29
|
-
};
|
|
30
|
-
envelopeHash?: {
|
|
31
|
-
algorithm: string;
|
|
32
|
-
value: string;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface DSSEEntry extends RekorEntry {
|
|
37
|
-
apiVersion: "0.0.1" | "0.0.2";
|
|
38
|
-
kind: "dsse";
|
|
39
|
-
spec: DSSESpec;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function verifyDSSEBody(
|
|
43
|
-
entry: RekorEntry,
|
|
44
|
-
bundle: SigstoreBundle,
|
|
45
|
-
cert: X509Certificate,
|
|
46
|
-
): Promise<void> {
|
|
47
|
-
const dsseEntry = entry as DSSEEntry;
|
|
48
|
-
|
|
49
|
-
switch (dsseEntry.apiVersion) {
|
|
50
|
-
case "0.0.1":
|
|
51
|
-
return verifyDSSE001Body(dsseEntry, bundle, cert);
|
|
52
|
-
case "0.0.2":
|
|
53
|
-
return verifyDSSE002Body(dsseEntry, bundle, cert);
|
|
54
|
-
default:
|
|
55
|
-
throw new Error(
|
|
56
|
-
`Unsupported dsse version: ${dsseEntry.apiVersion}`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function verifyDSSE001Body(
|
|
62
|
-
entry: DSSEEntry,
|
|
63
|
-
bundle: SigstoreBundle,
|
|
64
|
-
cert: X509Certificate,
|
|
65
|
-
): Promise<void> {
|
|
66
|
-
if (!bundle.dsseEnvelope) {
|
|
67
|
-
throw new Error("Bundle missing dsseEnvelope for DSSE entry");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!entry.spec.signatures || entry.spec.signatures.length !== 1) {
|
|
71
|
-
throw new Error("DSSE entry must have exactly one signature");
|
|
72
|
-
}
|
|
73
|
-
assertLoggedCertificate(cert, entry.spec.signatures[0].verifier, true);
|
|
74
|
-
|
|
75
|
-
const tlogSig = entry.spec.signatures[0].signature;
|
|
76
|
-
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
77
|
-
|
|
78
|
-
if (bundle.dsseEnvelope.signatures.length === 0) {
|
|
79
|
-
throw new Error("Bundle DSSE envelope missing signatures");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const bundleSigBytes = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
|
|
83
|
-
|
|
84
|
-
if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
|
|
85
|
-
throw new Error("DSSE signature mismatch between TLog entry and bundle");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!entry.spec.payloadHash?.value || !entry.spec.payloadHash?.algorithm) {
|
|
89
|
-
throw new Error("DSSE entry missing payloadHash or algorithm");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const hashAlg = getHashAlgorithm(entry.spec.payloadHash.algorithm);
|
|
93
|
-
const tlogHashBytes = hexToUint8Array(entry.spec.payloadHash.value);
|
|
94
|
-
|
|
95
|
-
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
96
|
-
const bundleHashBytes = new Uint8Array(
|
|
97
|
-
await crypto.subtle.digest(hashAlg, payloadBytes as BufferSource)
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
|
101
|
-
throw new Error("DSSE payload hash mismatch between TLog entry and bundle");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// New functionality for DSSE v0.0.2 (not in sigstore-js reference, which only supports v0.0.1)
|
|
106
|
-
async function verifyDSSE002Body(
|
|
107
|
-
entry: DSSEEntry,
|
|
108
|
-
bundle: SigstoreBundle,
|
|
109
|
-
cert: X509Certificate,
|
|
110
|
-
): Promise<void> {
|
|
111
|
-
if (!bundle.dsseEnvelope) {
|
|
112
|
-
throw new Error("Bundle missing dsseEnvelope for DSSE v0.0.2 entry");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const spec = (entry.spec as any).dsseV002;
|
|
116
|
-
if (!spec) {
|
|
117
|
-
throw new Error("DSSE v0.0.2 entry missing dsseV002 spec");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!spec.signatures || spec.signatures.length !== 1) {
|
|
121
|
-
throw new Error("DSSE v0.0.2 entry must have exactly one signature");
|
|
122
|
-
}
|
|
123
|
-
assertLoggedCertificate(cert, spec.signatures[0].verifier?.x509Certificate?.rawBytes, false);
|
|
124
|
-
|
|
125
|
-
const tlogSig = spec.signatures[0].content;
|
|
126
|
-
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
127
|
-
|
|
128
|
-
if (bundle.dsseEnvelope.signatures.length === 0) {
|
|
129
|
-
throw new Error("Bundle DSSE envelope missing signatures");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const bundleSigBytes = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
|
|
133
|
-
|
|
134
|
-
if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
|
|
135
|
-
throw new Error("DSSE signature mismatch between TLog entry and bundle (v0.0.2)");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!spec.payloadHash?.digest || !spec.payloadHash?.algorithm) {
|
|
139
|
-
throw new Error("DSSE v0.0.2 entry missing payloadHash or algorithm");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const hashAlg = getHashAlgorithm(spec.payloadHash.algorithm);
|
|
143
|
-
const tlogHashBytes = base64ToUint8Array(spec.payloadHash.digest);
|
|
144
|
-
|
|
145
|
-
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
146
|
-
const bundleHashBytes = new Uint8Array(
|
|
147
|
-
await crypto.subtle.digest(hashAlg, payloadBytes as BufferSource)
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
|
151
|
-
throw new Error("DSSE payload hash mismatch between TLog entry and bundle (v0.0.2)");
|
|
152
|
-
}
|
|
153
|
-
}
|