@freedomofpress/sigstore-browser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/conformance.yml +29 -0
- package/.github/workflows/publish.yml +45 -0
- package/README.md +10 -0
- package/dist/asn1/error.d.ts +5 -0
- package/dist/asn1/error.d.ts.map +1 -0
- package/dist/asn1/error.js +20 -0
- package/dist/asn1/error.js.map +1 -0
- package/dist/asn1/index.d.ts +2 -0
- package/dist/asn1/index.d.ts.map +1 -0
- package/dist/asn1/index.js +17 -0
- package/dist/asn1/index.js.map +1 -0
- package/dist/asn1/length.d.ts +4 -0
- package/dist/asn1/length.d.ts.map +1 -0
- package/dist/asn1/length.js +59 -0
- package/dist/asn1/length.js.map +1 -0
- package/dist/asn1/obj.d.ts +15 -0
- package/dist/asn1/obj.d.ts.map +1 -0
- package/dist/asn1/obj.js +153 -0
- package/dist/asn1/obj.js.map +1 -0
- package/dist/asn1/parse.d.ts +7 -0
- package/dist/asn1/parse.d.ts.map +1 -0
- package/dist/asn1/parse.js +118 -0
- package/dist/asn1/parse.js.map +1 -0
- package/dist/asn1/tag.d.ts +17 -0
- package/dist/asn1/tag.d.ts.map +1 -0
- package/dist/asn1/tag.js +86 -0
- package/dist/asn1/tag.js.map +1 -0
- package/dist/bundle.d.ts +87 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +2 -0
- package/dist/bundle.js.map +1 -0
- package/dist/canonicalize.d.ts +2 -0
- package/dist/canonicalize.d.ts.map +1 -0
- package/dist/canonicalize.js +64 -0
- package/dist/canonicalize.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +217 -0
- package/dist/cli.js.map +1 -0
- package/dist/crypto.d.ts +5 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +260 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dsse.d.ts +2 -0
- package/dist/dsse.d.ts.map +1 -0
- package/dist/dsse.js +42 -0
- package/dist/dsse.js.map +1 -0
- package/dist/encoding.d.ts +17 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +112 -0
- package/dist/encoding.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +79 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.d.ts +104 -0
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/interfaces.js +26 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/oid.d.ts +5 -0
- package/dist/oid.d.ts.map +1 -0
- package/dist/oid.js +25 -0
- package/dist/oid.js.map +1 -0
- package/dist/pem.d.ts +3 -0
- package/dist/pem.d.ts.map +1 -0
- package/dist/pem.js +41 -0
- package/dist/pem.js.map +1 -0
- package/dist/rfc3161/error.d.ts +3 -0
- package/dist/rfc3161/error.d.ts.map +1 -0
- package/dist/rfc3161/error.js +18 -0
- package/dist/rfc3161/error.js.map +1 -0
- package/dist/rfc3161/index.d.ts +2 -0
- package/dist/rfc3161/index.d.ts.map +1 -0
- package/dist/rfc3161/index.js +17 -0
- package/dist/rfc3161/index.js.map +1 -0
- package/dist/rfc3161/timestamp.d.ts +36 -0
- package/dist/rfc3161/timestamp.d.ts.map +1 -0
- package/dist/rfc3161/timestamp.js +203 -0
- package/dist/rfc3161/timestamp.js.map +1 -0
- package/dist/rfc3161/tstinfo.d.ts +13 -0
- package/dist/rfc3161/tstinfo.d.ts.map +1 -0
- package/dist/rfc3161/tstinfo.js +47 -0
- package/dist/rfc3161/tstinfo.js.map +1 -0
- package/dist/sigstore.d.ts +33 -0
- package/dist/sigstore.d.ts.map +1 -0
- package/dist/sigstore.js +531 -0
- package/dist/sigstore.js.map +1 -0
- package/dist/stream.d.ts +22 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +114 -0
- package/dist/stream.js.map +1 -0
- package/dist/timestamp/tsa.d.ts +24 -0
- package/dist/timestamp/tsa.d.ts.map +1 -0
- package/dist/timestamp/tsa.js +150 -0
- package/dist/timestamp/tsa.js.map +1 -0
- package/dist/tlog/body.d.ts +8 -0
- package/dist/tlog/body.d.ts.map +1 -0
- package/dist/tlog/body.js +45 -0
- package/dist/tlog/body.js.map +1 -0
- package/dist/tlog/checkpoint.d.ts +23 -0
- package/dist/tlog/checkpoint.d.ts.map +1 -0
- package/dist/tlog/checkpoint.js +187 -0
- package/dist/tlog/checkpoint.js.map +1 -0
- package/dist/tlog/dsse.d.ts +4 -0
- package/dist/tlog/dsse.d.ts.map +1 -0
- package/dist/tlog/dsse.js +85 -0
- package/dist/tlog/dsse.js.map +1 -0
- package/dist/tlog/hashedrekord.d.ts +4 -0
- package/dist/tlog/hashedrekord.d.ts.map +1 -0
- package/dist/tlog/hashedrekord.js +63 -0
- package/dist/tlog/hashedrekord.js.map +1 -0
- package/dist/tlog/intoto.d.ts +4 -0
- package/dist/tlog/intoto.d.ts.map +1 -0
- package/dist/tlog/intoto.js +50 -0
- package/dist/tlog/intoto.js.map +1 -0
- package/dist/tlog/merkle.d.ts +3 -0
- package/dist/tlog/merkle.d.ts.map +1 -0
- package/dist/tlog/merkle.js +112 -0
- package/dist/tlog/merkle.js.map +1 -0
- package/dist/trust/tuf-root.json +1 -0
- package/dist/trust/tuf.d.ts +95 -0
- package/dist/trust/tuf.d.ts.map +1 -0
- package/dist/trust/tuf.js +150 -0
- package/dist/trust/tuf.js.map +1 -0
- package/dist/x509/cert.d.ts +52 -0
- package/dist/x509/cert.d.ts.map +1 -0
- package/dist/x509/cert.js +256 -0
- package/dist/x509/cert.js.map +1 -0
- package/dist/x509/chain.d.ts +20 -0
- package/dist/x509/chain.d.ts.map +1 -0
- package/dist/x509/chain.js +141 -0
- package/dist/x509/chain.js.map +1 -0
- package/dist/x509/ext.d.ts +48 -0
- package/dist/x509/ext.d.ts.map +1 -0
- package/dist/x509/ext.js +164 -0
- package/dist/x509/ext.js.map +1 -0
- package/dist/x509/index.d.ts +4 -0
- package/dist/x509/index.d.ts.map +1 -0
- package/dist/x509/index.js +19 -0
- package/dist/x509/index.js.map +1 -0
- package/dist/x509/sct.d.ts +25 -0
- package/dist/x509/sct.d.ts.map +1 -0
- package/dist/x509/sct.js +123 -0
- package/dist/x509/sct.js.map +1 -0
- package/eslint.config.mjs +31 -0
- package/package.json +39 -0
- package/src/asn1/error.ts +18 -0
- package/src/asn1/index.ts +16 -0
- package/src/asn1/length.ts +71 -0
- package/src/asn1/obj.ts +184 -0
- package/src/asn1/parse.ts +146 -0
- package/src/asn1/tag.ts +105 -0
- package/src/bundle.ts +99 -0
- package/src/canonicalize.ts +61 -0
- package/src/cli.ts +285 -0
- package/src/crypto.ts +310 -0
- package/src/dsse.ts +49 -0
- package/src/encoding.ts +141 -0
- package/src/errors.ts +82 -0
- package/src/index.browser.test.ts +219 -0
- package/src/index.ts +14 -0
- package/src/interfaces.ts +118 -0
- package/src/oid.ts +28 -0
- package/src/pem.ts +48 -0
- package/src/rfc3161/error.ts +16 -0
- package/src/rfc3161/index.ts +17 -0
- package/src/rfc3161/timestamp.ts +270 -0
- package/src/rfc3161/tstinfo.ts +80 -0
- package/src/sigstore.ts +728 -0
- package/src/stream.ts +141 -0
- package/src/timestamp/tsa.ts +205 -0
- package/src/tlog/body.ts +65 -0
- package/src/tlog/checkpoint.ts +248 -0
- package/src/tlog/dsse.ts +147 -0
- package/src/tlog/hashedrekord.ts +130 -0
- package/src/tlog/intoto.ts +102 -0
- package/src/tlog/merkle.ts +152 -0
- package/src/trust/README.md +38 -0
- package/src/trust/tuf-root.json +1 -0
- package/src/trust/tuf.ts +223 -0
- package/src/types/tuf-browser.d.ts +13 -0
- package/src/x509/cert.ts +319 -0
- package/src/x509/chain.ts +212 -0
- package/src/x509/ext.ts +212 -0
- package/src/x509/index.ts +19 -0
- package/src/x509/sct.ts +154 -0
- package/tsconfig.json +18 -0
- package/vite.config.playwright.ts +26 -0
- package/vite.config.ts +18 -0
package/src/trust/tuf.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* TUF (The Update Framework) integration for trusted root management
|
|
3
|
+
*
|
|
4
|
+
* Provides secure updates for Sigstore trusted root metadata using TUF.
|
|
5
|
+
* Uses tuf-browser for browser-compatible TUF client functionality.
|
|
6
|
+
*
|
|
7
|
+
* Reference: https://github.com/freedomofpress/tuf-browser
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TrustedRoot } from "../interfaces.js";
|
|
11
|
+
import { Uint8ArrayToString } from "../encoding.js";
|
|
12
|
+
import type { TUFClient } from "tuf-browser/dist/tuf.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for TrustedRootProvider configuration
|
|
16
|
+
*/
|
|
17
|
+
export interface TrustedRootProviderOptions {
|
|
18
|
+
/**
|
|
19
|
+
* TUF repository URL for metadata
|
|
20
|
+
* Default: Sigstore production TUF repository
|
|
21
|
+
*/
|
|
22
|
+
metadataUrl?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Target base URL for fetching target files
|
|
26
|
+
* If not specified, uses the same as metadataUrl
|
|
27
|
+
*/
|
|
28
|
+
targetBaseUrl?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initial root metadata (1.root.json content)
|
|
32
|
+
* If not provided, will use embedded default
|
|
33
|
+
*/
|
|
34
|
+
initialRoot?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Namespace for TUF cache storage
|
|
38
|
+
* Default: 'sigstore-browser'
|
|
39
|
+
*/
|
|
40
|
+
namespace?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Name of the trusted root target file
|
|
44
|
+
* Default: 'trusted_root.json'
|
|
45
|
+
*/
|
|
46
|
+
trustedRootTarget?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Cache TTL in milliseconds
|
|
50
|
+
* Default: 1 hour (3600000 ms)
|
|
51
|
+
*/
|
|
52
|
+
cacheTTL?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default configuration for Sigstore production TUF repository
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_CONFIG = {
|
|
59
|
+
metadataUrl: 'https://tuf-repo-cdn.sigstore.dev/',
|
|
60
|
+
namespace: 'sigstore-browser',
|
|
61
|
+
trustedRootTarget: 'trusted_root.json',
|
|
62
|
+
cacheTTL: 3600000, // 1 hour
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provides Sigstore trusted root via TUF for secure updates
|
|
67
|
+
*
|
|
68
|
+
* This class manages fetching and caching of Sigstore trusted root metadata
|
|
69
|
+
* using The Update Framework (TUF) for secure, verified updates.
|
|
70
|
+
*
|
|
71
|
+
* Example usage:
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const provider = new TrustedRootProvider();
|
|
74
|
+
* const trustedRoot = await provider.getTrustedRoot();
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export class TrustedRootProvider {
|
|
78
|
+
private metadataUrl: string;
|
|
79
|
+
private targetBaseUrl?: string;
|
|
80
|
+
private initialRoot?: string;
|
|
81
|
+
private namespace: string;
|
|
82
|
+
private trustedRootTarget: string;
|
|
83
|
+
private cacheTTL: number;
|
|
84
|
+
|
|
85
|
+
private tufClient?: TUFClient;
|
|
86
|
+
private cachedRoot?: TrustedRoot;
|
|
87
|
+
private cacheTimestamp?: number;
|
|
88
|
+
|
|
89
|
+
constructor(options: TrustedRootProviderOptions = {}) {
|
|
90
|
+
const metadataUrl = options.metadataUrl || DEFAULT_CONFIG.metadataUrl;
|
|
91
|
+
this.metadataUrl = metadataUrl.endsWith('/') ? metadataUrl : `${metadataUrl}/`;
|
|
92
|
+
this.targetBaseUrl = options.targetBaseUrl || this.metadataUrl;
|
|
93
|
+
this.initialRoot = options.initialRoot;
|
|
94
|
+
this.namespace = options.namespace || DEFAULT_CONFIG.namespace;
|
|
95
|
+
this.trustedRootTarget = options.trustedRootTarget || DEFAULT_CONFIG.trustedRootTarget;
|
|
96
|
+
this.cacheTTL = options.cacheTTL ?? DEFAULT_CONFIG.cacheTTL;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Initialize the TUF client
|
|
101
|
+
* Lazy initialization to avoid loading TUF client until needed
|
|
102
|
+
*/
|
|
103
|
+
private async initTUFClient(): Promise<void> {
|
|
104
|
+
if (this.tufClient) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const { TUFClient } = await import('tuf-browser/dist/tuf.js');
|
|
110
|
+
|
|
111
|
+
// Get initial root metadata
|
|
112
|
+
const rootMetadata = this.initialRoot || await this.getDefaultRoot();
|
|
113
|
+
|
|
114
|
+
this.tufClient = new TUFClient(
|
|
115
|
+
this.metadataUrl,
|
|
116
|
+
rootMetadata,
|
|
117
|
+
this.namespace,
|
|
118
|
+
this.targetBaseUrl
|
|
119
|
+
);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Failed to initialize TUF client: ${error instanceof Error ? error.message : String(error)}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the default embedded root metadata
|
|
129
|
+
* Returns the TUF root.json that bootstraps the TUF client
|
|
130
|
+
*/
|
|
131
|
+
private async getDefaultRoot(): Promise<string> {
|
|
132
|
+
// Import the embedded TUF root metadata (base64-encoded 1.root.json as JSON string)
|
|
133
|
+
// This is the root of trust for TUF, not to be confused with the Sigstore trusted root
|
|
134
|
+
const tufRootModule = await import('./tuf-root.json', { with: { type: 'json' } });
|
|
135
|
+
const tufRootBase64 = tufRootModule.default as string;
|
|
136
|
+
|
|
137
|
+
// Decode from base64 to get the actual root.json content
|
|
138
|
+
const decoder = new TextDecoder();
|
|
139
|
+
const rootBytes = Uint8Array.from(atob(tufRootBase64), c => c.charCodeAt(0));
|
|
140
|
+
return decoder.decode(rootBytes);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if cached trusted root is still valid
|
|
145
|
+
*/
|
|
146
|
+
private isCacheValid(): boolean {
|
|
147
|
+
if (!this.cachedRoot || !this.cacheTimestamp) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
return (now - this.cacheTimestamp) < this.cacheTTL;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the Sigstore trusted root metadata
|
|
157
|
+
* Uses TUF to securely fetch and verify the trusted root
|
|
158
|
+
*
|
|
159
|
+
* @returns Promise<TrustedRoot> The verified trusted root metadata
|
|
160
|
+
* @throws Error if TUF verification fails or root cannot be fetched
|
|
161
|
+
*/
|
|
162
|
+
async getTrustedRoot(): Promise<TrustedRoot> {
|
|
163
|
+
// Return cached root if still valid
|
|
164
|
+
if (this.isCacheValid() && this.cachedRoot) {
|
|
165
|
+
return this.cachedRoot;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Initialize TUF client if needed
|
|
169
|
+
await this.initTUFClient();
|
|
170
|
+
|
|
171
|
+
if (!this.tufClient) {
|
|
172
|
+
throw new Error('TUF client not initialized');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Update TUF metadata (root, timestamp, snapshot, targets)
|
|
177
|
+
// This must be called before getTarget() to populate the metadata cache
|
|
178
|
+
await this.tufClient.updateTUF();
|
|
179
|
+
|
|
180
|
+
// Fetch the trusted root target via TUF
|
|
181
|
+
// TUF will handle all verification (signatures, rollback protection, etc.)
|
|
182
|
+
const trustedRootBytes = await this.tufClient.getTarget(this.trustedRootTarget);
|
|
183
|
+
|
|
184
|
+
// Parse the trusted root JSON
|
|
185
|
+
const trustedRootJson = Uint8ArrayToString(trustedRootBytes);
|
|
186
|
+
const trustedRoot = JSON.parse(trustedRootJson) as TrustedRoot;
|
|
187
|
+
|
|
188
|
+
// Cache the result
|
|
189
|
+
this.cachedRoot = trustedRoot;
|
|
190
|
+
this.cacheTimestamp = Date.now();
|
|
191
|
+
|
|
192
|
+
return trustedRoot;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Failed to fetch trusted root via TUF: ${error instanceof Error ? error.message : String(error)}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Manually refresh the trusted root from TUF
|
|
202
|
+
* Bypasses cache and forces a fresh fetch
|
|
203
|
+
*
|
|
204
|
+
* @returns Promise<TrustedRoot> The updated trusted root metadata
|
|
205
|
+
*/
|
|
206
|
+
async refreshTrustedRoot(): Promise<TrustedRoot> {
|
|
207
|
+
// Clear cache
|
|
208
|
+
this.cachedRoot = undefined;
|
|
209
|
+
this.cacheTimestamp = undefined;
|
|
210
|
+
|
|
211
|
+
// Fetch fresh root
|
|
212
|
+
return await this.getTrustedRoot();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Clear the cached trusted root
|
|
217
|
+
* Next call to getTrustedRoot() will fetch fresh data
|
|
218
|
+
*/
|
|
219
|
+
clearCache(): void {
|
|
220
|
+
this.cachedRoot = undefined;
|
|
221
|
+
this.cacheTimestamp = undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare module 'tuf-browser/dist/tuf.js' {
|
|
2
|
+
export class TUFClient {
|
|
3
|
+
constructor(
|
|
4
|
+
repositoryUrl: string,
|
|
5
|
+
startingRoot: string,
|
|
6
|
+
namespace: string,
|
|
7
|
+
targetBaseUrl?: string
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
updateTUF(): Promise<void>;
|
|
11
|
+
getTarget(targetPath: string): Promise<Uint8Array>;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/x509/cert.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
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 } from "../asn1/index.js";
|
|
17
|
+
import { bufferEqual, importKey, verifySignature } from "../crypto.js";
|
|
18
|
+
import { Uint8ArrayToBase64 } from "../encoding.js";
|
|
19
|
+
import { KeyTypes } from "../interfaces.js";
|
|
20
|
+
import { ECDSA_CURVE_NAMES, ECDSA_SIGNATURE_ALGOS, RSA_SIGNATURE_ALGOS } from "../oid.js";
|
|
21
|
+
import * as pem from "../pem.js";
|
|
22
|
+
import {
|
|
23
|
+
X509AuthorityKeyIDExtension,
|
|
24
|
+
X509BasicConstraintsExtension,
|
|
25
|
+
X509Extension,
|
|
26
|
+
X509FulcioIssuerV1,
|
|
27
|
+
X509FulcioIssuerV2,
|
|
28
|
+
X509KeyUsageExtension,
|
|
29
|
+
X509SCTExtension,
|
|
30
|
+
X509SubjectAlternativeNameExtension,
|
|
31
|
+
X509SubjectKeyIDExtension,
|
|
32
|
+
} from "./ext.js";
|
|
33
|
+
|
|
34
|
+
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md
|
|
35
|
+
const EXTENSION_OID_SUBJECT_KEY_ID = "2.5.29.14";
|
|
36
|
+
const EXTENSION_OID_KEY_USAGE = "2.5.29.15";
|
|
37
|
+
const EXTENSION_OID_SUBJECT_ALT_NAME = "2.5.29.17";
|
|
38
|
+
const EXTENSION_OID_BASIC_CONSTRAINTS = "2.5.29.19";
|
|
39
|
+
const EXTENSION_OID_AUTHORITY_KEY_ID = "2.5.29.35";
|
|
40
|
+
const EXTENSION_OID_FULCIO_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
|
|
41
|
+
const EXTENSION_OID_FULCIO_ISSUER_V2 = "1.3.6.1.4.1.57264.1.8";
|
|
42
|
+
|
|
43
|
+
export const EXTENSION_OID_SCT = "1.3.6.1.4.1.11129.2.4.2";
|
|
44
|
+
|
|
45
|
+
export class X509Certificate {
|
|
46
|
+
public root: ASN1Obj;
|
|
47
|
+
|
|
48
|
+
constructor(asn1: ASN1Obj) {
|
|
49
|
+
this.root = asn1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static parse(cert: Uint8Array | string): X509Certificate {
|
|
53
|
+
const der = typeof cert === "string" ? pem.toDER(cert) : cert;
|
|
54
|
+
const asn1 = ASN1Obj.parseBuffer(der);
|
|
55
|
+
return new X509Certificate(asn1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get tbsCertificate(): ASN1Obj {
|
|
59
|
+
return this.tbsCertificateObj;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get version(): string {
|
|
63
|
+
// version number is the first element of the version context specific tag
|
|
64
|
+
const ver = this.versionObj.subs[0].toInteger();
|
|
65
|
+
return `v${(ver + BigInt(1)).toString()}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get serialNumber(): Uint8Array {
|
|
69
|
+
return this.serialNumberObj.value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get notBefore(): Date {
|
|
73
|
+
// notBefore is the first element of the validity sequence
|
|
74
|
+
return this.validityObj.subs[0].toDate();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get notAfter(): Date {
|
|
78
|
+
// notAfter is the second element of the validity sequence
|
|
79
|
+
return this.validityObj.subs[1].toDate();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get issuer(): Uint8Array {
|
|
83
|
+
return this.issuerObj.value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get subject(): Uint8Array {
|
|
87
|
+
return this.subjectObj.value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get publicKey(): Uint8Array {
|
|
91
|
+
return this.subjectPublicKeyInfoObj.toDER();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Import the public key with a specific hash algorithm for RSA keys
|
|
96
|
+
* For ECDSA keys, the hash parameter is ignored
|
|
97
|
+
*/
|
|
98
|
+
public async getPublicKeyObj(hashAlg?: string): Promise<CryptoKey> {
|
|
99
|
+
const publicKey = this.subjectPublicKeyInfoObj.toDER();
|
|
100
|
+
const spki = ASN1Obj.parseBuffer(publicKey);
|
|
101
|
+
const algorithmOID = spki.subs[0].subs[0].toOID();
|
|
102
|
+
|
|
103
|
+
// Check if it's an RSA key (OID 1.2.840.113549.1.1.1)
|
|
104
|
+
if (algorithmOID === "1.2.840.113549.1.1.1") {
|
|
105
|
+
// RSA key - use provided hash or default to SHA-256
|
|
106
|
+
const hash = hashAlg || "sha256";
|
|
107
|
+
let scheme: string;
|
|
108
|
+
if (hash.includes("384")) {
|
|
109
|
+
scheme = "SHA384";
|
|
110
|
+
} else if (hash.includes("512")) {
|
|
111
|
+
scheme = "SHA512";
|
|
112
|
+
} else {
|
|
113
|
+
scheme = "SHA256";
|
|
114
|
+
}
|
|
115
|
+
return importKey("RSA", `RSA_PKCS1V5_${scheme}`, Uint8ArrayToBase64(publicKey));
|
|
116
|
+
} else {
|
|
117
|
+
// ECDSA key - the curve OID is in the second element
|
|
118
|
+
const curveOID = spki.subs[0].subs[1]?.toOID();
|
|
119
|
+
const curve = ECDSA_CURVE_NAMES[curveOID];
|
|
120
|
+
if (!curve) {
|
|
121
|
+
throw new Error(`Unknown ECDSA curve OID: ${curveOID}`);
|
|
122
|
+
}
|
|
123
|
+
return importKey(KeyTypes.Ecdsa, curve, Uint8ArrayToBase64(publicKey));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get publicKeyObj(): Promise<CryptoKey> {
|
|
128
|
+
return this.getPublicKeyObj();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get signatureAlgorithm(): string {
|
|
132
|
+
const oid: string = this.signatureAlgorithmObj.subs[0].toOID();
|
|
133
|
+
return ECDSA_SIGNATURE_ALGOS[oid];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get signatureValue(): Uint8Array {
|
|
137
|
+
// Signature value is a bit string, so we need to skip the first byte
|
|
138
|
+
return this.signatureValueObj.value.subarray(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get subjectAltName(): string | undefined {
|
|
142
|
+
const ext = this.extSubjectAltName;
|
|
143
|
+
return ext?.uri || ext?.rfc822Name;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get extensions(): ASN1Obj[] {
|
|
147
|
+
// The extension list is the first (and only) element of the extensions
|
|
148
|
+
// context specific tag
|
|
149
|
+
const extSeq = this.extensionsObj?.subs[0];
|
|
150
|
+
return extSeq?.subs || /* istanbul ignore next */ [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get extKeyUsage(): X509KeyUsageExtension | undefined {
|
|
154
|
+
const ext = this.findExtension(EXTENSION_OID_KEY_USAGE);
|
|
155
|
+
return ext ? new X509KeyUsageExtension(ext) : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get extBasicConstraints(): X509BasicConstraintsExtension | undefined {
|
|
159
|
+
const ext = this.findExtension(EXTENSION_OID_BASIC_CONSTRAINTS);
|
|
160
|
+
return ext ? new X509BasicConstraintsExtension(ext) : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get extSubjectAltName(): X509SubjectAlternativeNameExtension | undefined {
|
|
164
|
+
const ext = this.findExtension(EXTENSION_OID_SUBJECT_ALT_NAME);
|
|
165
|
+
return ext ? new X509SubjectAlternativeNameExtension(ext) : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get extAuthorityKeyID(): X509AuthorityKeyIDExtension | undefined {
|
|
169
|
+
const ext = this.findExtension(EXTENSION_OID_AUTHORITY_KEY_ID);
|
|
170
|
+
return ext ? new X509AuthorityKeyIDExtension(ext) : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get extSubjectKeyID(): X509SubjectKeyIDExtension | undefined {
|
|
174
|
+
const ext = this.findExtension(EXTENSION_OID_SUBJECT_KEY_ID);
|
|
175
|
+
return ext
|
|
176
|
+
? new X509SubjectKeyIDExtension(ext)
|
|
177
|
+
: /* istanbul ignore next */ undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get extSCT(): X509SCTExtension | undefined {
|
|
181
|
+
const ext = this.findExtension(EXTENSION_OID_SCT);
|
|
182
|
+
return ext ? new X509SCTExtension(ext) : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get extFulcioIssuerV1(): X509FulcioIssuerV1 | undefined {
|
|
186
|
+
const ext = this.findExtension(EXTENSION_OID_FULCIO_ISSUER_V1);
|
|
187
|
+
return ext ? new X509FulcioIssuerV1(ext) : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get extFulcioIssuerV2(): X509FulcioIssuerV2 | undefined {
|
|
191
|
+
const ext = this.findExtension(EXTENSION_OID_FULCIO_ISSUER_V2);
|
|
192
|
+
return ext ? new X509FulcioIssuerV2(ext) : undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get isCA(): boolean {
|
|
196
|
+
const ca = this.extBasicConstraints?.isCA || false;
|
|
197
|
+
|
|
198
|
+
// If the KeyUsage extension is present, keyCertSign must be set
|
|
199
|
+
if (this.extKeyUsage) {
|
|
200
|
+
return ca && this.extKeyUsage.keyCertSign;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return ca;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public extension(oid: string): X509Extension | undefined {
|
|
207
|
+
const ext = this.findExtension(oid);
|
|
208
|
+
return ext ? new X509Extension(ext) : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public async verify(issuerCertificate?: X509Certificate): Promise<boolean> {
|
|
212
|
+
// Extract the hash algorithm from this certificate's signature algorithm
|
|
213
|
+
// (not the issuer's own signature algorithm)
|
|
214
|
+
const sigAlgOID = this.signatureAlgorithmObj.subs[0].toOID();
|
|
215
|
+
const hashAlg = RSA_SIGNATURE_ALGOS[sigAlgOID] || ECDSA_SIGNATURE_ALGOS[sigAlgOID];
|
|
216
|
+
|
|
217
|
+
// Use the issuer's public key if provided, otherwise use the subject's (for self-signed certs)
|
|
218
|
+
// Import with the correct hash algorithm for this certificate's signature
|
|
219
|
+
const publicKeyObj = issuerCertificate
|
|
220
|
+
? await issuerCertificate.getPublicKeyObj(hashAlg)
|
|
221
|
+
: await this.getPublicKeyObj(hashAlg);
|
|
222
|
+
|
|
223
|
+
return await verifySignature(
|
|
224
|
+
publicKeyObj,
|
|
225
|
+
this.tbsCertificate.toDER(),
|
|
226
|
+
this.signatureValue,
|
|
227
|
+
this.signatureAlgorithm,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public validForDate(date: Date): boolean {
|
|
232
|
+
return this.notBefore <= date && date <= this.notAfter;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public equals(other: X509Certificate): boolean {
|
|
236
|
+
return bufferEqual(this.root.toDER(), other.root.toDER());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Creates a copy of the certificate with a new buffer
|
|
240
|
+
public clone(): X509Certificate {
|
|
241
|
+
const der = this.root.toDER();
|
|
242
|
+
const clone = new Uint8Array(der);
|
|
243
|
+
return X509Certificate.parse(clone);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private findExtension(oid: string): ASN1Obj | undefined {
|
|
247
|
+
// Find the extension with the given OID. The OID will always be the first
|
|
248
|
+
// element of the extension sequence
|
|
249
|
+
return this.extensions.find((ext) => ext.subs[0].toOID() === oid);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
253
|
+
// The following properties use the documented x509 structure to locate the
|
|
254
|
+
// desired ASN.1 object
|
|
255
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1
|
|
256
|
+
|
|
257
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.1.1
|
|
258
|
+
private get tbsCertificateObj(): ASN1Obj {
|
|
259
|
+
// tbsCertificate is the first element of the certificate sequence
|
|
260
|
+
return this.root.subs[0];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.1.2
|
|
264
|
+
private get signatureAlgorithmObj(): ASN1Obj {
|
|
265
|
+
// signatureAlgorithm is the second element of the certificate sequence
|
|
266
|
+
return this.root.subs[1];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.1.3
|
|
270
|
+
private get signatureValueObj(): ASN1Obj {
|
|
271
|
+
// signatureValue is the third element of the certificate sequence
|
|
272
|
+
return this.root.subs[2];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.1
|
|
276
|
+
private get versionObj(): ASN1Obj {
|
|
277
|
+
// version is the first element of the tbsCertificate sequence
|
|
278
|
+
return this.tbsCertificateObj.subs[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.2
|
|
282
|
+
private get serialNumberObj(): ASN1Obj {
|
|
283
|
+
// serialNumber is the second element of the tbsCertificate sequence
|
|
284
|
+
return this.tbsCertificateObj.subs[1];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.4
|
|
288
|
+
private get issuerObj(): ASN1Obj {
|
|
289
|
+
// issuer is the fourth element of the tbsCertificate sequence
|
|
290
|
+
return this.tbsCertificateObj.subs[3];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.5
|
|
294
|
+
private get validityObj(): ASN1Obj {
|
|
295
|
+
// version is the fifth element of the tbsCertificate sequence
|
|
296
|
+
return this.tbsCertificateObj.subs[4];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.6
|
|
300
|
+
private get subjectObj(): ASN1Obj {
|
|
301
|
+
// subject is the sixth element of the tbsCertificate sequence
|
|
302
|
+
return this.tbsCertificateObj.subs[5];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.7
|
|
306
|
+
private get subjectPublicKeyInfoObj(): ASN1Obj {
|
|
307
|
+
// subjectPublicKeyInfo is the seventh element of the tbsCertificate sequence
|
|
308
|
+
return this.tbsCertificateObj.subs[6];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Extensions can't be located by index because their position varies. Instead,
|
|
312
|
+
// we need to find the extensions context specific tag
|
|
313
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.9
|
|
314
|
+
private get extensionsObj(): ASN1Obj | undefined {
|
|
315
|
+
return this.tbsCertificateObj.subs.find((sub) =>
|
|
316
|
+
sub.tag.isContextSpecific(0x03),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|