@freedomofpress/sigstore-browser 0.1.14 → 0.1.16

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.
Files changed (132) hide show
  1. package/README.md +20 -16
  2. package/dist/bundle.d.ts +3 -2
  3. package/dist/bundle.js +66 -1
  4. package/dist/bundle.js.map +1 -1
  5. package/dist/cli.d.ts +0 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/dsse.d.ts +0 -1
  8. package/dist/dsse.js.map +1 -1
  9. package/dist/errors.d.ts +0 -1
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +9 -10
  12. package/dist/index.js +5 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/interfaces.d.ts +18 -4
  15. package/dist/interfaces.js +15 -1
  16. package/dist/interfaces.js.map +1 -1
  17. package/dist/oid.d.ts +0 -1
  18. package/dist/oid.js.map +1 -1
  19. package/dist/policy.d.ts +3 -4
  20. package/dist/policy.js +5 -5
  21. package/dist/policy.js.map +1 -1
  22. package/dist/rfc3161/error.d.ts +0 -1
  23. package/dist/rfc3161/error.js.map +1 -1
  24. package/dist/rfc3161/index.d.ts +0 -1
  25. package/dist/rfc3161/index.js.map +1 -1
  26. package/dist/rfc3161/timestamp.d.ts +0 -1
  27. package/dist/rfc3161/timestamp.js.map +1 -1
  28. package/dist/rfc3161/tstinfo.d.ts +0 -1
  29. package/dist/rfc3161/tstinfo.js.map +1 -1
  30. package/dist/sigstore.d.ts +10 -20
  31. package/dist/sigstore.js +167 -400
  32. package/dist/sigstore.js.map +1 -1
  33. package/dist/timestamp/tsa.d.ts +6 -3
  34. package/dist/timestamp/tsa.js +16 -19
  35. package/dist/timestamp/tsa.js.map +1 -1
  36. package/dist/tlog/body.d.ts +3 -2
  37. package/dist/tlog/body.js +19 -6
  38. package/dist/tlog/body.js.map +1 -1
  39. package/dist/tlog/checkpoint.d.ts +2 -3
  40. package/dist/tlog/checkpoint.js +17 -95
  41. package/dist/tlog/checkpoint.js.map +1 -1
  42. package/dist/tlog/dsse.d.ts +3 -3
  43. package/dist/tlog/dsse.js +11 -5
  44. package/dist/tlog/dsse.js.map +1 -1
  45. package/dist/tlog/hashedrekord.d.ts +3 -3
  46. package/dist/tlog/hashedrekord.js +8 -5
  47. package/dist/tlog/hashedrekord.js.map +1 -1
  48. package/dist/tlog/intoto.d.ts +3 -3
  49. package/dist/tlog/intoto.js +3 -1
  50. package/dist/tlog/intoto.js.map +1 -1
  51. package/dist/tlog/merkle.d.ts +0 -1
  52. package/dist/tlog/merkle.js.map +1 -1
  53. package/dist/trust/tuf-root.d.ts +0 -1
  54. package/dist/trust/tuf-root.js.map +1 -1
  55. package/dist/trust/tuf.d.ts +0 -1
  56. package/dist/trust/tuf.js.map +1 -1
  57. package/dist/x509/cert.d.ts +0 -1
  58. package/dist/x509/cert.js +1 -1
  59. package/dist/x509/cert.js.map +1 -1
  60. package/dist/x509/chain.d.ts +0 -1
  61. package/dist/x509/chain.js +2 -2
  62. package/dist/x509/chain.js.map +1 -1
  63. package/dist/x509/ext.d.ts +0 -1
  64. package/dist/x509/ext.js.map +1 -1
  65. package/dist/x509/index.d.ts +1 -2
  66. package/dist/x509/index.js +1 -1
  67. package/dist/x509/index.js.map +1 -1
  68. package/dist/x509/sct.d.ts +0 -1
  69. package/dist/x509/sct.js.map +1 -1
  70. package/package.json +15 -8
  71. package/.github/workflows/conformance.yml +0 -29
  72. package/.github/workflows/publish.yml +0 -47
  73. package/dist/bundle.d.ts.map +0 -1
  74. package/dist/cli.d.ts.map +0 -1
  75. package/dist/dsse.d.ts.map +0 -1
  76. package/dist/errors.d.ts.map +0 -1
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/interfaces.d.ts.map +0 -1
  79. package/dist/oid.d.ts.map +0 -1
  80. package/dist/policy.d.ts.map +0 -1
  81. package/dist/rfc3161/error.d.ts.map +0 -1
  82. package/dist/rfc3161/index.d.ts.map +0 -1
  83. package/dist/rfc3161/timestamp.d.ts.map +0 -1
  84. package/dist/rfc3161/tstinfo.d.ts.map +0 -1
  85. package/dist/sigstore.d.ts.map +0 -1
  86. package/dist/timestamp/tsa.d.ts.map +0 -1
  87. package/dist/tlog/body.d.ts.map +0 -1
  88. package/dist/tlog/checkpoint.d.ts.map +0 -1
  89. package/dist/tlog/dsse.d.ts.map +0 -1
  90. package/dist/tlog/hashedrekord.d.ts.map +0 -1
  91. package/dist/tlog/intoto.d.ts.map +0 -1
  92. package/dist/tlog/merkle.d.ts.map +0 -1
  93. package/dist/trust/tuf-root.d.ts.map +0 -1
  94. package/dist/trust/tuf.d.ts.map +0 -1
  95. package/dist/x509/cert.d.ts.map +0 -1
  96. package/dist/x509/chain.d.ts.map +0 -1
  97. package/dist/x509/ext.d.ts.map +0 -1
  98. package/dist/x509/index.d.ts.map +0 -1
  99. package/dist/x509/sct.d.ts.map +0 -1
  100. package/eslint.config.mjs +0 -31
  101. package/src/bundle.ts +0 -99
  102. package/src/cli.ts +0 -290
  103. package/src/dsse.ts +0 -49
  104. package/src/errors.ts +0 -82
  105. package/src/index.browser.test.ts +0 -290
  106. package/src/index.ts +0 -93
  107. package/src/interfaces.ts +0 -123
  108. package/src/oid.ts +0 -34
  109. package/src/policy.ts +0 -476
  110. package/src/rfc3161/error.ts +0 -16
  111. package/src/rfc3161/index.ts +0 -17
  112. package/src/rfc3161/timestamp.ts +0 -268
  113. package/src/rfc3161/tstinfo.ts +0 -78
  114. package/src/sigstore.ts +0 -948
  115. package/src/timestamp/tsa.ts +0 -211
  116. package/src/tlog/body.ts +0 -65
  117. package/src/tlog/checkpoint.ts +0 -254
  118. package/src/tlog/dsse.ts +0 -147
  119. package/src/tlog/hashedrekord.ts +0 -130
  120. package/src/tlog/intoto.ts +0 -106
  121. package/src/tlog/merkle.ts +0 -152
  122. package/src/trust/README.md +0 -38
  123. package/src/trust/tuf-root.ts +0 -3
  124. package/src/trust/tuf.ts +0 -233
  125. package/src/x509/cert.ts +0 -569
  126. package/src/x509/chain.ts +0 -212
  127. package/src/x509/ext.ts +0 -356
  128. package/src/x509/index.ts +0 -19
  129. package/src/x509/sct.ts +0 -156
  130. package/tsconfig.json +0 -18
  131. package/vite.config.playwright.ts +0 -14
  132. package/vite.config.ts +0 -18
@@ -1,211 +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 { CertificateChainVerifier, X509Certificate } from "../x509/index.js";
18
- import { RawTimestampAuthority } from "../interfaces.js";
19
- import { RFC3161Timestamp } from "../rfc3161/index.js";
20
-
21
- /**
22
- * Verifies an RFC3161 timestamp against a set of timestamp authorities
23
- *
24
- * @param timestamp - The RFC3161 timestamp to verify
25
- * @param data - The data that was timestamped
26
- * @param timestampAuthorities - List of trusted timestamp authorities
27
- * @returns The verified signing time
28
- * @throws Error if timestamp cannot be verified
29
- */
30
- export async function verifyRFC3161Timestamp(
31
- timestamp: RFC3161Timestamp,
32
- data: Uint8Array,
33
- timestampAuthorities: RawTimestampAuthority[]
34
- ): Promise<Date> {
35
- const signingTime = timestamp.signingTime;
36
-
37
- // Filter for CAs which were valid at the time of signing
38
- let validAuthorities = filterCertAuthorities(
39
- timestampAuthorities,
40
- signingTime
41
- );
42
-
43
- // Filter for CAs which match serial and issuer embedded in the timestamp
44
- validAuthorities = filterCAsBySerialAndIssuer(validAuthorities, {
45
- serialNumber: timestamp.signerSerialNumber,
46
- issuer: timestamp.signerIssuer,
47
- });
48
-
49
- // Check that we can verify the timestamp with AT LEAST ONE of the remaining CAs
50
- const verificationResults = await Promise.allSettled(
51
- validAuthorities.map(ca => verifyTimestampForCA(timestamp, data, ca))
52
- );
53
-
54
- const verified = verificationResults.some(
55
- result => result.status === "fulfilled"
56
- );
57
-
58
- if (!verified) {
59
- const errors = verificationResults
60
- .filter(r => r.status === "rejected")
61
- .map(r => (r as PromiseRejectedResult).reason?.message || 'Unknown error');
62
- throw new Error(`Timestamp could not be verified against any trusted authority. Errors: ${errors.join(', ')}`);
63
- }
64
-
65
- return signingTime;
66
- }
67
-
68
- /**
69
- * Filters certificate authorities to those valid at a specific time
70
- *
71
- * Adapted from sigstore-js/packages/verify/src/trust/filter.ts
72
- */
73
- function filterCertAuthorities(
74
- authorities: RawTimestampAuthority[],
75
- validAt: Date
76
- ): RawTimestampAuthority[] {
77
- return authorities.filter(ca => {
78
- if (ca.validFor) {
79
- const start = ca.validFor.start ? new Date(ca.validFor.start) : null;
80
- const end = ca.validFor.end ? new Date(ca.validFor.end) : null;
81
-
82
- if (start && validAt < start) {
83
- return false;
84
- }
85
- if (end && validAt > end) {
86
- return false;
87
- }
88
- }
89
- return true;
90
- });
91
- }
92
-
93
- /**
94
- * Filters certificate authorities by serial number and issuer
95
- *
96
- * Only checks the LEAF certificate (TSA signing cert), not the entire chain.
97
- * This matches the sigstore-js reference implementation.
98
- */
99
- function filterCAsBySerialAndIssuer(
100
- timestampAuthorities: RawTimestampAuthority[],
101
- criteria: { serialNumber: Uint8Array; issuer: Uint8Array }
102
- ): RawTimestampAuthority[] {
103
- return timestampAuthorities.filter(ca => {
104
- if (!ca.certChain || ca.certChain.certificates.length === 0) {
105
- return false;
106
- }
107
-
108
- const leafCert = X509Certificate.parse(
109
- base64ToUint8Array(ca.certChain.certificates[0].rawBytes)
110
- );
111
-
112
- return uint8ArrayEqual(leafCert.serialNumber, criteria.serialNumber) &&
113
- uint8ArrayEqual(leafCert.issuer, criteria.issuer);
114
- });
115
- }
116
-
117
- /**
118
- * Verifies a timestamp against a specific certificate authority
119
- */
120
- async function verifyTimestampForCA(
121
- timestamp: RFC3161Timestamp,
122
- data: Uint8Array,
123
- ca: RawTimestampAuthority
124
- ): Promise<void> {
125
- if (!ca.certChain || ca.certChain.certificates.length === 0) {
126
- throw new Error("Certificate authority missing certificate chain");
127
- }
128
-
129
- const leafCert = X509Certificate.parse(
130
- base64ToUint8Array(ca.certChain.certificates[0].rawBytes)
131
- );
132
- const signingTime = timestamp.signingTime;
133
-
134
- const trustedCerts = ca.certChain.certificates
135
- .slice(1)
136
- .map(cert => X509Certificate.parse(base64ToUint8Array(cert.rawBytes)));
137
-
138
- try {
139
- const verifier = new CertificateChainVerifier({
140
- untrustedCert: leafCert,
141
- trustedCerts: trustedCerts,
142
- timestamp: signingTime,
143
- });
144
- await verifier.verify();
145
- } catch (e) {
146
- throw new Error(`TSA certificate chain verification failed: ${e instanceof Error ? e.message : String(e)}`);
147
- }
148
-
149
- const publicKey = await leafCert.publicKeyObj;
150
-
151
- await timestamp.verify(data, publicKey);
152
- }
153
-
154
- /**
155
- * Verifies all timestamps in a bundle's timestamp verification data
156
- *
157
- * Matches sigstore-js behavior: all timestamps must successfully verify.
158
- * Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L64-L108
159
- *
160
- * @param timestampData - The timestamp verification data from the bundle
161
- * @param signature - The signature being verified
162
- * @param timestampAuthorities - List of trusted timestamp authorities
163
- * @returns Array of verified signing times (empty if no timestamps present)
164
- * @throws Error if any timestamp fails verification or duplicates are found
165
- */
166
- export async function verifyBundleTimestamp(
167
- timestampData: any,
168
- signature: Uint8Array,
169
- timestampAuthorities: RawTimestampAuthority[]
170
- ): Promise<Date[]> {
171
- if (!timestampData?.rfc3161Timestamps?.length) {
172
- return [];
173
- }
174
-
175
- // Verify ALL timestamps - each must succeed (matches sigstore-js behavior)
176
- // Collect verification results for duplicate checking after verification
177
- // Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L86-L92
178
- const verifiedResults: Array<{ signingTime: Date; signerSerialNumber: string }> = [];
179
-
180
- for (const tsData of timestampData.rfc3161Timestamps) {
181
- // Decode the base64-encoded timestamp
182
- const timestampBytes = base64ToUint8Array(tsData.signedTimestamp);
183
- const timestamp = RFC3161Timestamp.parse(timestampBytes);
184
-
185
- const signingTime = await verifyRFC3161Timestamp(
186
- timestamp,
187
- signature,
188
- timestampAuthorities
189
- );
190
-
191
- verifiedResults.push({
192
- signingTime,
193
- signerSerialNumber: Array.from(timestamp.signerSerialNumber).join(','),
194
- });
195
- }
196
-
197
- // Check for duplicate timestamps using deep equality on parsed values
198
- // sigstore-js uses isDeepStrictEqual on {type, logID, timestamp} objects
199
- for (let i = 0; i < verifiedResults.length; i++) {
200
- for (let j = i + 1; j < verifiedResults.length; j++) {
201
- if (
202
- verifiedResults[i].signingTime.getTime() === verifiedResults[j].signingTime.getTime() &&
203
- verifiedResults[i].signerSerialNumber === verifiedResults[j].signerSerialNumber
204
- ) {
205
- throw new Error("Duplicate TSA timestamp detected");
206
- }
207
- }
208
- }
209
-
210
- return verifiedResults.map(r => r.signingTime);
211
- }
package/src/tlog/body.ts DELETED
@@ -1,65 +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 } from "@freedomofpress/crypto-browser";
13
- import type { SigstoreBundle, TLogEntry } from "../bundle.js";
14
- import { verifyHashedRekordBody } from "./hashedrekord.js";
15
- import { verifyDSSEBody } from "./dsse.js";
16
- import { verifyIntotoBody } from "./intoto.js";
17
-
18
- export interface RekorEntry {
19
- apiVersion: string;
20
- kind: string;
21
- spec: unknown;
22
- }
23
-
24
- export async function verifyTLogBody(
25
- entry: TLogEntry,
26
- bundle: SigstoreBundle
27
- ): Promise<void> {
28
- const rekorEntry = parseCanonicalBody(entry);
29
-
30
- const { kind, version } = entry.kindVersion;
31
-
32
- if (kind !== rekorEntry.kind || version !== rekorEntry.apiVersion) {
33
- throw new Error(
34
- `kind/version mismatch - expected: ${kind}/${version}, received: ${rekorEntry.kind}/${rekorEntry.apiVersion}`
35
- );
36
- }
37
-
38
- switch (rekorEntry.kind) {
39
- case "hashedrekord":
40
- return verifyHashedRekordBody(rekorEntry, bundle);
41
- case "dsse":
42
- return verifyDSSEBody(rekorEntry, bundle);
43
- case "intoto":
44
- return verifyIntotoBody(rekorEntry, bundle);
45
- default:
46
- throw new Error(`Unsupported TLog entry kind: ${rekorEntry.kind}`);
47
- }
48
- }
49
-
50
- function parseCanonicalBody(entry: TLogEntry): RekorEntry {
51
- try {
52
- const decodedBody = base64Decode(entry.canonicalizedBody);
53
- const rekorEntry = JSON.parse(decodedBody) as RekorEntry;
54
-
55
- if (!rekorEntry.apiVersion || !rekorEntry.kind || !rekorEntry.spec) {
56
- throw new Error("Invalid Rekor entry structure");
57
- }
58
-
59
- return rekorEntry;
60
- } catch (error) {
61
- throw new Error(
62
- `Failed to parse canonicalized body: ${error instanceof Error ? error.message : String(error)}`
63
- );
64
- }
65
- }
@@ -1,254 +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
- - Manual handling of ED25519 (raw) vs ECDSA (DER) signature formats
11
- - Inline key import from raw bytes
12
-
13
- Additional functionality not in reference:
14
- - filterTLogsByDate() - inlined from sigstore-js/packages/verify/src/trust/filter.ts
15
- - importTLogKey() - new browser-compatible key import with multiple format support
16
- - Special handling for Rekor v2 bundles without integratedTime
17
-
18
- Follows the signed note format specification:
19
- https://github.com/transparency-dev/formats/blob/main/log/README.md
20
- */
21
-
22
- import {
23
- base64ToUint8Array,
24
- importKey,
25
- KeyTypes,
26
- stringToUint8Array,
27
- uint8ArrayEqual,
28
- verifySignature,
29
- } from "@freedomofpress/crypto-browser";
30
- import type { TLogEntry } from "../bundle.js";
31
- import type { RawLogs } from "../interfaces.js";
32
-
33
- // Signed note format per https://github.com/transparency-dev/formats
34
- // Body is separated from signatures by a blank line
35
- const CHECKPOINT_SEPARATOR = "\n\n";
36
- // Signature lines format: "— <identity> <base64(key_hint+signature)>\n"
37
- // \u2014 is the em-dash character (—)
38
- const SIGNATURE_REGEX = /\u2014 (\S+) (\S+)\n/g;
39
-
40
- export interface TLogSignature {
41
- name: string;
42
- keyHint: Uint8Array;
43
- signature: Uint8Array;
44
- }
45
-
46
- // Signed checkpoint note with cryptographic signatures
47
- export class SignedNote {
48
- readonly note: string;
49
- readonly signatures: TLogSignature[];
50
-
51
- constructor(note: string, signatures: TLogSignature[]) {
52
- this.note = note;
53
- this.signatures = signatures;
54
- }
55
-
56
- static fromString(envelope: string): SignedNote {
57
- if (!envelope.includes(CHECKPOINT_SEPARATOR)) {
58
- throw new Error("Missing checkpoint separator");
59
- }
60
-
61
- // Split body from signature lines at blank line
62
- const split = envelope.indexOf(CHECKPOINT_SEPARATOR);
63
- const header = envelope.slice(0, split + 1);
64
- const data = envelope.slice(split + CHECKPOINT_SEPARATOR.length);
65
-
66
- // Parse signature lines: "— <identity> <base64(key_hint+signature)>\n"
67
- const matches = data.matchAll(SIGNATURE_REGEX);
68
-
69
- const signatures: TLogSignature[] = [];
70
- for (const match of matches) {
71
- const [, name, signature] = match;
72
- const sigBytes = base64ToUint8Array(signature);
73
-
74
- // First 4 bytes are key hint (SHA256 hash prefix), rest is signature
75
- if (sigBytes.length < 5) {
76
- throw new Error("Malformed checkpoint signature");
77
- }
78
-
79
- signatures.push({
80
- name,
81
- keyHint: sigBytes.subarray(0, 4),
82
- signature: sigBytes.subarray(4),
83
- });
84
- }
85
-
86
- if (signatures.length === 0) {
87
- throw new Error("No signatures found in checkpoint");
88
- }
89
-
90
- return new SignedNote(header, signatures);
91
- }
92
- }
93
-
94
- // Parsed checkpoint containing tree state (origin, size, root hash)
95
- export class LogCheckpoint {
96
- readonly origin: string;
97
- readonly logSize: bigint;
98
- readonly logHash: Uint8Array;
99
- readonly rest: string[];
100
-
101
- constructor(
102
- origin: string,
103
- logSize: bigint,
104
- logHash: Uint8Array,
105
- rest: string[]
106
- ) {
107
- this.origin = origin;
108
- this.logSize = logSize;
109
- this.logHash = logHash;
110
- this.rest = rest;
111
- }
112
-
113
- static fromString(note: string): LogCheckpoint {
114
- const lines = note.trimEnd().split("\n");
115
-
116
- if (lines.length < 3) {
117
- throw new Error("Too few lines in checkpoint header");
118
- }
119
-
120
- const origin = lines[0];
121
- const logSize = BigInt(lines[1]);
122
- const rootHash = base64ToUint8Array(lines[2]);
123
- const rest = lines.slice(3);
124
-
125
- return new LogCheckpoint(origin, logSize, rootHash, rest);
126
- }
127
- }
128
-
129
- // Verifies checkpoint signature and ensures root hash matches inclusion proof
130
- export async function verifyCheckpoint(
131
- entry: TLogEntry,
132
- tlogs: RawLogs
133
- ): Promise<void> {
134
- if (!entry.inclusionProof?.checkpoint) {
135
- throw new Error("Missing checkpoint in inclusion proof");
136
- }
137
-
138
- // Filter by log ID first (matches entry's log ID)
139
- const entryLogId = base64ToUint8Array(entry.logId.keyId);
140
- const matchingTLogs = tlogs.filter((tlog) => {
141
- const tlogId = base64ToUint8Array(tlog.logId.keyId);
142
- return uint8ArrayEqual(tlogId, entryLogId);
143
- });
144
-
145
- // Then filter by date if integratedTime is available
146
- // For Rekor v2 bundles, integratedTime may be null, so we skip date filtering
147
- const validTLogs = entry.integratedTime
148
- ? filterTLogsByDate(matchingTLogs, new Date(Number(entry.integratedTime) * 1000))
149
- : matchingTLogs;
150
-
151
- const inclusionProof = entry.inclusionProof;
152
- const signedNote = SignedNote.fromString(inclusionProof.checkpoint.envelope);
153
- const checkpoint = LogCheckpoint.fromString(signedNote.note);
154
-
155
- if (!(await verifySignedNote(signedNote, validTLogs))) {
156
- throw new Error("Invalid checkpoint signature");
157
- }
158
-
159
- const rootHash = base64ToUint8Array(inclusionProof.rootHash);
160
- if (!uint8ArrayEqual(checkpoint.logHash, rootHash)) {
161
- throw new Error("Root hash mismatch between checkpoint and inclusion proof");
162
- }
163
- }
164
-
165
- // Verifies checkpoint signatures using trusted TLog keys
166
- async function verifySignedNote(
167
- signedNote: SignedNote,
168
- tlogs: RawLogs
169
- ): Promise<boolean> {
170
- const data = stringToUint8Array(signedNote.note);
171
-
172
- // We need at least one valid signature from a known TLog
173
- let hasValidSignature = false;
174
-
175
- for (const signature of signedNote.signatures) {
176
- // Match signature to TLog using key hint (first 4 bytes of key ID)
177
- const tlog = tlogs.find((tlog) => {
178
- const logId = base64ToUint8Array(tlog.logId.keyId);
179
- return uint8ArrayEqual(logId.subarray(0, 4), signature.keyHint);
180
- });
181
-
182
- if (!tlog) {
183
- // Skip unknown signatures (e.g., from witnesses we don't know about)
184
- continue;
185
- }
186
-
187
- const publicKey = await importTLogKey(tlog);
188
-
189
- // Both ED25519 and ECDSA verification are now handled by verifySignature
190
- // ED25519 signatures are raw (64 bytes), ECDSA signatures are DER-encoded
191
- const verified = await verifySignature(
192
- publicKey,
193
- data,
194
- signature.signature,
195
- tlog.hashAlgorithm
196
- );
197
-
198
- if (verified) {
199
- hasValidSignature = true;
200
- }
201
- }
202
-
203
- return hasValidSignature;
204
- }
205
-
206
-
207
- // Inlined from sigstore-js/packages/verify/src/trust/filter.ts
208
- function filterTLogsByDate(tlogs: RawLogs, targetDate: Date): RawLogs {
209
- return tlogs.filter((tlog) => {
210
- const start = new Date(tlog.publicKey.validFor.start);
211
- const end = tlog.publicKey.validFor.end
212
- ? new Date(tlog.publicKey.validFor.end)
213
- : null;
214
-
215
- return targetDate >= start && (!end || targetDate <= end);
216
- });
217
- }
218
-
219
- // Browser-compatible key import supporting multiple formats (not in sigstore-js reference)
220
- async function importTLogKey(tlog: RawLogs[0]): Promise<CryptoKey> {
221
- // Parse keyDetails to extract key type and scheme
222
- // Formats can be:
223
- // - "PKIX_ECDSA_P256_SHA_256" (production format)
224
- // - "PKIX_ED25519"
225
- // - "ecdsa-sha2-nistp256" (SSH/test format)
226
- const keyDetails = tlog.publicKey.keyDetails;
227
- let keyType: string;
228
- let scheme: string;
229
-
230
- if (keyDetails === "ecdsa-sha2-nistp256") {
231
- // SSH-style ECDSA P-256 format used in tests
232
- keyType = KeyTypes.Ecdsa;
233
- scheme = "P256-SHA256";
234
- } else if (keyDetails.includes("ECDSA")) {
235
- keyType = KeyTypes.Ecdsa;
236
- // Extract the curve and hash, e.g., "P256_SHA_256" from "PKIX_ECDSA_P256_SHA_256"
237
- scheme = keyDetails.replace("PKIX_ECDSA_", "").replace(/_/g, "-")
238
- } else if (keyDetails.includes("ED25519")) {
239
- keyType = KeyTypes.Ed25519;
240
- scheme = KeyTypes.Ed25519;
241
- } else if (keyDetails.includes("RSA")) {
242
- keyType = KeyTypes.RSA;
243
- // Extract RSA details, e.g., "PSS_SHA_256" from "PKIX_RSA_PSS_SHA_256"
244
- scheme = keyDetails.replace("PKIX_RSA_", "").replace(/_/g, "-")
245
- } else {
246
- throw new Error(`Unsupported key type in keyDetails: ${keyDetails}`);
247
- }
248
-
249
- return importKey(
250
- keyType,
251
- scheme,
252
- tlog.publicKey.rawBytes
253
- );
254
- }
package/src/tlog/dsse.ts DELETED
@@ -1,147 +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 { RekorEntry } from "./body.js";
18
-
19
- interface DSSESpec {
20
- signatures?: Array<{
21
- signature: string;
22
- verifier?: string;
23
- keyid?: string;
24
- }>;
25
- payloadHash?: {
26
- algorithm: string;
27
- value: string;
28
- };
29
- envelopeHash?: {
30
- algorithm: string;
31
- value: string;
32
- };
33
- }
34
-
35
- interface DSSEEntry extends RekorEntry {
36
- apiVersion: "0.0.1" | "0.0.2";
37
- kind: "dsse";
38
- spec: DSSESpec;
39
- }
40
-
41
- export async function verifyDSSEBody(
42
- entry: RekorEntry,
43
- bundle: SigstoreBundle
44
- ): Promise<void> {
45
- const dsseEntry = entry as DSSEEntry;
46
-
47
- switch (dsseEntry.apiVersion) {
48
- case "0.0.1":
49
- return verifyDSSE001Body(dsseEntry, bundle);
50
- case "0.0.2":
51
- return verifyDSSE002Body(dsseEntry, bundle);
52
- default:
53
- throw new Error(
54
- `Unsupported dsse version: ${dsseEntry.apiVersion}`
55
- );
56
- }
57
- }
58
-
59
- async function verifyDSSE001Body(
60
- entry: DSSEEntry,
61
- bundle: SigstoreBundle
62
- ): Promise<void> {
63
- if (!bundle.dsseEnvelope) {
64
- throw new Error("Bundle missing dsseEnvelope for DSSE entry");
65
- }
66
-
67
- if (!entry.spec.signatures || entry.spec.signatures.length !== 1) {
68
- throw new Error("DSSE entry must have exactly one signature");
69
- }
70
-
71
- const tlogSig = entry.spec.signatures[0].signature;
72
- const tlogSigBytes = base64ToUint8Array(tlogSig);
73
-
74
- if (bundle.dsseEnvelope.signatures.length === 0) {
75
- throw new Error("Bundle DSSE envelope missing signatures");
76
- }
77
-
78
- const bundleSigBytes = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
79
-
80
- if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
81
- throw new Error("DSSE signature mismatch between TLog entry and bundle");
82
- }
83
-
84
- if (!entry.spec.payloadHash?.value || !entry.spec.payloadHash?.algorithm) {
85
- throw new Error("DSSE entry missing payloadHash or algorithm");
86
- }
87
-
88
- const hashAlg = getHashAlgorithm(entry.spec.payloadHash.algorithm);
89
- const tlogHashBytes = hexToUint8Array(entry.spec.payloadHash.value);
90
-
91
- const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
92
- const bundleHashBytes = new Uint8Array(
93
- await crypto.subtle.digest(hashAlg, payloadBytes as BufferSource)
94
- );
95
-
96
- if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
97
- throw new Error("DSSE payload hash mismatch between TLog entry and bundle");
98
- }
99
- }
100
-
101
- // New functionality for DSSE v0.0.2 (not in sigstore-js reference, which only supports v0.0.1)
102
- async function verifyDSSE002Body(
103
- entry: DSSEEntry,
104
- bundle: SigstoreBundle
105
- ): Promise<void> {
106
- if (!bundle.dsseEnvelope) {
107
- throw new Error("Bundle missing dsseEnvelope for DSSE v0.0.2 entry");
108
- }
109
-
110
- const spec = (entry.spec as any).dsseV002;
111
- if (!spec) {
112
- throw new Error("DSSE v0.0.2 entry missing dsseV002 spec");
113
- }
114
-
115
- if (!spec.signatures || spec.signatures.length !== 1) {
116
- throw new Error("DSSE v0.0.2 entry must have exactly one signature");
117
- }
118
-
119
- const tlogSig = spec.signatures[0].content;
120
- const tlogSigBytes = base64ToUint8Array(tlogSig);
121
-
122
- if (bundle.dsseEnvelope.signatures.length === 0) {
123
- throw new Error("Bundle DSSE envelope missing signatures");
124
- }
125
-
126
- const bundleSigBytes = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
127
-
128
- if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
129
- throw new Error("DSSE signature mismatch between TLog entry and bundle (v0.0.2)");
130
- }
131
-
132
- if (!spec.payloadHash?.digest || !spec.payloadHash?.algorithm) {
133
- throw new Error("DSSE v0.0.2 entry missing payloadHash or algorithm");
134
- }
135
-
136
- const hashAlg = getHashAlgorithm(spec.payloadHash.algorithm);
137
- const tlogHashBytes = base64ToUint8Array(spec.payloadHash.digest);
138
-
139
- const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
140
- const bundleHashBytes = new Uint8Array(
141
- await crypto.subtle.digest(hashAlg, payloadBytes as BufferSource)
142
- );
143
-
144
- if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
145
- throw new Error("DSSE payload hash mismatch between TLog entry and bundle (v0.0.2)");
146
- }
147
- }