@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
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Certificate chain verification for browser environments
|
|
19
|
+
*
|
|
20
|
+
* Ported from sigstore-js:
|
|
21
|
+
* https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/key/certificate.ts
|
|
22
|
+
*
|
|
23
|
+
* Key differences from sigstore-js:
|
|
24
|
+
* - Browser-compatible: async verify() method for Web Crypto API
|
|
25
|
+
* - Uses Uint8Array instead of Buffer for binary data
|
|
26
|
+
* - Uses bufferEqual for Uint8Array comparisons
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { X509Certificate } from "./cert.js";
|
|
30
|
+
import { bufferEqual } from "../crypto.js";
|
|
31
|
+
|
|
32
|
+
interface CertificateChainVerifierOptions {
|
|
33
|
+
trustedCerts: X509Certificate[];
|
|
34
|
+
untrustedCert: X509Certificate;
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class CertificateChainVerifier {
|
|
39
|
+
private untrustedCert: X509Certificate;
|
|
40
|
+
private trustedCerts: X509Certificate[];
|
|
41
|
+
private localCerts: X509Certificate[];
|
|
42
|
+
private timestamp: Date;
|
|
43
|
+
|
|
44
|
+
constructor(opts: CertificateChainVerifierOptions) {
|
|
45
|
+
this.untrustedCert = opts.untrustedCert;
|
|
46
|
+
this.trustedCerts = opts.trustedCerts;
|
|
47
|
+
this.localCerts = dedupeCertificates([
|
|
48
|
+
...opts.trustedCerts,
|
|
49
|
+
opts.untrustedCert,
|
|
50
|
+
]);
|
|
51
|
+
this.timestamp = opts.timestamp;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async verify(): Promise<X509Certificate[]> {
|
|
55
|
+
const certificatePath = await this.sort();
|
|
56
|
+
|
|
57
|
+
this.checkPath(certificatePath);
|
|
58
|
+
|
|
59
|
+
const validForDate = certificatePath.every((cert) =>
|
|
60
|
+
cert.validForDate(this.timestamp)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!validForDate) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"certificate is not valid or expired at the specified date"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return certificatePath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async sort(): Promise<X509Certificate[]> {
|
|
73
|
+
const leafCert = this.untrustedCert;
|
|
74
|
+
|
|
75
|
+
let paths = await this.buildPaths(leafCert);
|
|
76
|
+
|
|
77
|
+
paths = paths.filter((path) =>
|
|
78
|
+
path.some((cert) => this.trustedCerts.includes(cert))
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (paths.length === 0) {
|
|
82
|
+
throw new Error("no trusted certificate path found");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const path = paths.reduce((prev, curr) =>
|
|
86
|
+
prev.length < curr.length ? prev : curr
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return [leafCert, ...path].slice(0, -1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async buildPaths(
|
|
93
|
+
certificate: X509Certificate
|
|
94
|
+
): Promise<X509Certificate[][]> {
|
|
95
|
+
const paths = [];
|
|
96
|
+
const issuers = await this.findIssuer(certificate);
|
|
97
|
+
|
|
98
|
+
if (issuers.length === 0) {
|
|
99
|
+
throw new Error("no valid certificate path found");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < issuers.length; i++) {
|
|
103
|
+
const issuer = issuers[i];
|
|
104
|
+
|
|
105
|
+
if (issuer.equals(certificate)) {
|
|
106
|
+
paths.push([certificate]);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const subPaths = await this.buildPaths(issuer);
|
|
111
|
+
|
|
112
|
+
for (let j = 0; j < subPaths.length; j++) {
|
|
113
|
+
paths.push([issuer, ...subPaths[j]]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return paths;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async findIssuer(
|
|
121
|
+
certificate: X509Certificate
|
|
122
|
+
): Promise<X509Certificate[]> {
|
|
123
|
+
let issuers: X509Certificate[] = [];
|
|
124
|
+
let keyIdentifier: Uint8Array | undefined;
|
|
125
|
+
|
|
126
|
+
if (bufferEqual(certificate.subject, certificate.issuer)) {
|
|
127
|
+
if (await certificate.verify()) {
|
|
128
|
+
return [certificate];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (certificate.extAuthorityKeyID) {
|
|
133
|
+
keyIdentifier = certificate.extAuthorityKeyID.keyIdentifier;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.localCerts.forEach((possibleIssuer) => {
|
|
137
|
+
if (keyIdentifier) {
|
|
138
|
+
if (possibleIssuer.extSubjectKeyID) {
|
|
139
|
+
if (
|
|
140
|
+
bufferEqual(
|
|
141
|
+
possibleIssuer.extSubjectKeyID.keyIdentifier,
|
|
142
|
+
keyIdentifier
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
issuers.push(possibleIssuer);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (bufferEqual(possibleIssuer.subject, certificate.issuer)) {
|
|
152
|
+
issuers.push(possibleIssuer);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const verifiedIssuers: X509Certificate[] = [];
|
|
157
|
+
for (const issuer of issuers) {
|
|
158
|
+
try {
|
|
159
|
+
if (await certificate.verify(issuer)) {
|
|
160
|
+
verifiedIssuers.push(issuer);
|
|
161
|
+
}
|
|
162
|
+
} catch (ex) {
|
|
163
|
+
// Ignore verification failures
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return verifiedIssuers;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private checkPath(path: X509Certificate[]): void {
|
|
171
|
+
if (path.length < 1) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"certificate chain must contain at least one certificate"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const validCAs = path.slice(1).every((cert) => cert.isCA);
|
|
178
|
+
if (!validCAs) {
|
|
179
|
+
throw new Error("intermediate certificate is not a CA");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
183
|
+
if (!bufferEqual(path[i].issuer, path[i + 1].subject)) {
|
|
184
|
+
throw new Error("incorrect certificate name chaining");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < path.length; i++) {
|
|
189
|
+
const cert = path[i];
|
|
190
|
+
|
|
191
|
+
if (cert.extBasicConstraints?.isCA) {
|
|
192
|
+
const pathLength = cert.extBasicConstraints.pathLenConstraint;
|
|
193
|
+
|
|
194
|
+
if (pathLength !== undefined && pathLength < BigInt(i - 1)) {
|
|
195
|
+
throw new Error("path length constraint exceeded");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function dedupeCertificates(certs: X509Certificate[]): X509Certificate[] {
|
|
203
|
+
for (let i = 0; i < certs.length; i++) {
|
|
204
|
+
for (let j = i + 1; j < certs.length; j++) {
|
|
205
|
+
if (certs[i].equals(certs[j])) {
|
|
206
|
+
certs.splice(j, 1);
|
|
207
|
+
j--;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return certs;
|
|
212
|
+
}
|
package/src/x509/ext.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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 { toArrayBuffer, Uint8ArrayToString } from "../encoding.js";
|
|
18
|
+
import { ByteStream } from "../stream.js";
|
|
19
|
+
import { SignedCertificateTimestamp } from "./sct.js";
|
|
20
|
+
|
|
21
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.1
|
|
22
|
+
export class X509Extension {
|
|
23
|
+
protected root: ASN1Obj;
|
|
24
|
+
|
|
25
|
+
constructor(asn1: ASN1Obj) {
|
|
26
|
+
this.root = asn1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get oid(): string {
|
|
30
|
+
return this.root.subs[0].toOID();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get critical(): boolean {
|
|
34
|
+
// The critical field is optional and will be the second element of the
|
|
35
|
+
// extension sequence if present. Default to false if not present.
|
|
36
|
+
return this.root.subs.length === 3 ? this.root.subs[1].toBoolean() : false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get value(): Uint8Array {
|
|
40
|
+
return this.extnValueObj.value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get valueObj(): ASN1Obj {
|
|
44
|
+
return this.extnValueObj;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected get extnValueObj(): ASN1Obj {
|
|
48
|
+
// The extnValue field will be the last element of the extension sequence
|
|
49
|
+
return this.root.subs[this.root.subs.length - 1];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9
|
|
54
|
+
export class X509BasicConstraintsExtension extends X509Extension {
|
|
55
|
+
get isCA(): boolean {
|
|
56
|
+
return this.sequence.subs[0]?.toBoolean() ?? false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get pathLenConstraint(): bigint | undefined {
|
|
60
|
+
return this.sequence.subs.length > 1
|
|
61
|
+
? this.sequence.subs[1].toInteger()
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The extnValue field contains a single sequence wrapping the isCA and
|
|
66
|
+
// pathLenConstraint.
|
|
67
|
+
private get sequence(): ASN1Obj {
|
|
68
|
+
return this.extnValueObj.subs[0];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
|
|
73
|
+
export class X509KeyUsageExtension extends X509Extension {
|
|
74
|
+
get digitalSignature(): boolean {
|
|
75
|
+
return this.bitString[0] === 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get keyCertSign(): boolean {
|
|
79
|
+
return this.bitString[5] === 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get crlSign(): boolean {
|
|
83
|
+
return this.bitString[6] === 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The extnValue field contains a single bit string which is a bit mask
|
|
87
|
+
// indicating which key usages are enabled.
|
|
88
|
+
private get bitString(): number[] {
|
|
89
|
+
return this.extnValueObj.subs[0].toBitString();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.6
|
|
94
|
+
export class X509SubjectAlternativeNameExtension extends X509Extension {
|
|
95
|
+
get rfc822Name(): string | undefined {
|
|
96
|
+
const rfc822Name = this.findGeneralName(0x01)?.value;
|
|
97
|
+
if (rfc822Name === undefined) {
|
|
98
|
+
return undefined;
|
|
99
|
+
} else {
|
|
100
|
+
return Uint8ArrayToString(rfc822Name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get uri(): string | undefined {
|
|
105
|
+
const uri = this.findGeneralName(0x06)?.value;
|
|
106
|
+
if (uri === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
} else {
|
|
109
|
+
return Uint8ArrayToString(uri);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Retrieve the value of an otherName with the given OID.
|
|
114
|
+
public otherName(oid: string): string | undefined {
|
|
115
|
+
const otherName = this.findGeneralName(0x00);
|
|
116
|
+
|
|
117
|
+
if (otherName === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The otherName is a sequence containing an OID and a value.
|
|
122
|
+
// Need to check that the OID matches the one we're looking for.
|
|
123
|
+
const otherNameOID = otherName.subs[0].toOID();
|
|
124
|
+
if (otherNameOID !== oid) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The otherNameValue is a sequence containing the actual value.
|
|
129
|
+
const otherNameValue = otherName.subs[1];
|
|
130
|
+
return Uint8ArrayToString(otherNameValue.subs[0].value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private findGeneralName(tag: number): ASN1Obj | undefined {
|
|
134
|
+
return this.generalNames.find((gn) => gn.tag.isContextSpecific(tag));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The extnValue field contains a sequence of GeneralNames.
|
|
138
|
+
private get generalNames(): ASN1Obj[] {
|
|
139
|
+
return this.extnValueObj.subs[0].subs;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1
|
|
144
|
+
export class X509AuthorityKeyIDExtension extends X509Extension {
|
|
145
|
+
get keyIdentifier(): Uint8Array | undefined {
|
|
146
|
+
return this.findSequenceMember(0x00)?.value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private findSequenceMember(tag: number): ASN1Obj | undefined {
|
|
150
|
+
return this.sequence.subs.find((el) => el.tag.isContextSpecific(tag));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The extnValue field contains a single sequence wrapping the keyIdentifier
|
|
154
|
+
private get sequence(): ASN1Obj {
|
|
155
|
+
return this.extnValueObj.subs[0];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2
|
|
160
|
+
export class X509SubjectKeyIDExtension extends X509Extension {
|
|
161
|
+
get keyIdentifier(): Uint8Array {
|
|
162
|
+
return this.extnValueObj.subs[0].value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fulcio Issuer V1 extension - OID 1.3.6.1.4.1.57264.1.1
|
|
167
|
+
// Simple OCTET STRING containing the issuer
|
|
168
|
+
export class X509FulcioIssuerV1 extends X509Extension {
|
|
169
|
+
get issuer(): string {
|
|
170
|
+
return Uint8ArrayToString(this.extnValueObj.value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Fulcio Issuer V2 extension - OID 1.3.6.1.4.1.57264.1.8
|
|
175
|
+
// SEQUENCE containing the issuer as the first element
|
|
176
|
+
export class X509FulcioIssuerV2 extends X509Extension {
|
|
177
|
+
get issuer(): string {
|
|
178
|
+
return Uint8ArrayToString(this.extnValueObj.subs[0].value);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// https://www.rfc-editor.org/rfc/rfc6962#section-3.3
|
|
183
|
+
export class X509SCTExtension extends X509Extension {
|
|
184
|
+
constructor(asn1: ASN1Obj) {
|
|
185
|
+
super(asn1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get signedCertificateTimestamps(): SignedCertificateTimestamp[] {
|
|
189
|
+
const buf = this.extnValueObj.subs[0].value;
|
|
190
|
+
const stream = new ByteStream(toArrayBuffer(buf));
|
|
191
|
+
|
|
192
|
+
// The overall list length is encoded in the first two bytes -- note this
|
|
193
|
+
// is the length of the list in bytes, NOT the number of SCTs in the list
|
|
194
|
+
const end = stream.getUint16() + 2;
|
|
195
|
+
|
|
196
|
+
const sctList: SignedCertificateTimestamp[] = [];
|
|
197
|
+
while (stream.position < end) {
|
|
198
|
+
// Read the length of the next SCT
|
|
199
|
+
const sctLength = stream.getUint16();
|
|
200
|
+
|
|
201
|
+
// Slice out the bytes for the next SCT and parse it
|
|
202
|
+
const sct = stream.getBlock(sctLength);
|
|
203
|
+
sctList.push(SignedCertificateTimestamp.parse(sct));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (stream.position !== end) {
|
|
207
|
+
throw new Error("SCT list length does not match actual length");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return sctList;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
17
|
+
export { EXTENSION_OID_SCT, X509Certificate } from "./cert.js";
|
|
18
|
+
export { X509SCTExtension } from "./ext.js";
|
|
19
|
+
export { CertificateChainVerifier } from "./chain.js";
|
package/src/x509/sct.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
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 { verifySignature } from "../crypto.js";
|
|
17
|
+
import { readBigInt64BE, toArrayBuffer } from "../encoding.js";
|
|
18
|
+
import { ByteStream } from "../stream.js";
|
|
19
|
+
|
|
20
|
+
interface SCTOptions {
|
|
21
|
+
version: number;
|
|
22
|
+
logID: Uint8Array;
|
|
23
|
+
timestamp: Uint8Array;
|
|
24
|
+
extensions: Uint8Array;
|
|
25
|
+
hashAlgorithm: number;
|
|
26
|
+
signatureAlgorithm: number;
|
|
27
|
+
signature: Uint8Array;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SignedCertificateTimestamp {
|
|
31
|
+
readonly version: number;
|
|
32
|
+
readonly logID: Uint8Array;
|
|
33
|
+
readonly timestamp: Uint8Array;
|
|
34
|
+
readonly extensions: Uint8Array;
|
|
35
|
+
readonly hashAlgorithm: number;
|
|
36
|
+
readonly signatureAlgorithm: number;
|
|
37
|
+
readonly signature: Uint8Array;
|
|
38
|
+
|
|
39
|
+
constructor(options: SCTOptions) {
|
|
40
|
+
this.version = options.version;
|
|
41
|
+
this.logID = options.logID;
|
|
42
|
+
this.timestamp = options.timestamp;
|
|
43
|
+
this.extensions = options.extensions;
|
|
44
|
+
this.hashAlgorithm = options.hashAlgorithm;
|
|
45
|
+
this.signatureAlgorithm = options.signatureAlgorithm;
|
|
46
|
+
this.signature = options.signature;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get datetime(): Date {
|
|
50
|
+
return new Date(Number(readBigInt64BE(this.timestamp)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Returns the hash algorithm used to generate the SCT's signature.
|
|
54
|
+
// https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.4.1
|
|
55
|
+
get algorithm(): string {
|
|
56
|
+
switch (this.hashAlgorithm) {
|
|
57
|
+
/* istanbul ignore next */
|
|
58
|
+
case 0:
|
|
59
|
+
return "none";
|
|
60
|
+
/* istanbul ignore next */
|
|
61
|
+
case 1:
|
|
62
|
+
return "md5";
|
|
63
|
+
/* istanbul ignore next */
|
|
64
|
+
case 2:
|
|
65
|
+
return "sha1";
|
|
66
|
+
/* istanbul ignore next */
|
|
67
|
+
case 3:
|
|
68
|
+
return "sha224";
|
|
69
|
+
case 4:
|
|
70
|
+
return "sha256";
|
|
71
|
+
/* istanbul ignore next */
|
|
72
|
+
case 5:
|
|
73
|
+
return "sha384";
|
|
74
|
+
/* istanbul ignore next */
|
|
75
|
+
case 6:
|
|
76
|
+
return "sha512";
|
|
77
|
+
/* istanbul ignore next */
|
|
78
|
+
default:
|
|
79
|
+
return "unknown";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async verify(preCert: Uint8Array, key: CryptoKey): Promise<boolean> {
|
|
84
|
+
// Assemble the digitally-signed struct (the data over which the signature
|
|
85
|
+
// was generated).
|
|
86
|
+
// https://www.rfc-editor.org/rfc/rfc6962#section-3.2
|
|
87
|
+
const stream = new ByteStream();
|
|
88
|
+
stream.appendChar(this.version);
|
|
89
|
+
stream.appendChar(0x00); // SignatureType = certificate_timestamp(0)
|
|
90
|
+
stream.appendView(this.timestamp);
|
|
91
|
+
stream.appendUint16(0x01); // LogEntryType = precert_entry(1)
|
|
92
|
+
stream.appendView(preCert);
|
|
93
|
+
stream.appendUint16(this.extensions.byteLength);
|
|
94
|
+
|
|
95
|
+
/* istanbul ignore next - extensions are very uncommon */
|
|
96
|
+
if (this.extensions.byteLength > 0) {
|
|
97
|
+
stream.appendView(this.extensions);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return await verifySignature(
|
|
101
|
+
key,
|
|
102
|
+
stream.buffer,
|
|
103
|
+
this.signature,
|
|
104
|
+
this.algorithm,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parses a SignedCertificateTimestamp from a buffer. SCTs are encoded using
|
|
109
|
+
// TLS encoding which means the fields and lengths of most fields are
|
|
110
|
+
// specified as part of the SCT and TLS specs.
|
|
111
|
+
// https://www.rfc-editor.org/rfc/rfc6962#section-3.2
|
|
112
|
+
// https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.4.1
|
|
113
|
+
public static parse(buf: Uint8Array): SignedCertificateTimestamp {
|
|
114
|
+
const stream = new ByteStream(toArrayBuffer(buf));
|
|
115
|
+
|
|
116
|
+
// Version - enum { v1(0), (255) }
|
|
117
|
+
const version = stream.getUint8();
|
|
118
|
+
|
|
119
|
+
// Log ID - struct { opaque key_id[32]; }
|
|
120
|
+
const logID = stream.getBlock(32);
|
|
121
|
+
|
|
122
|
+
// Timestamp - uint64
|
|
123
|
+
const timestamp = stream.getBlock(8);
|
|
124
|
+
|
|
125
|
+
// Extensions - opaque extensions<0..2^16-1>;
|
|
126
|
+
const extenstionLength = stream.getUint16();
|
|
127
|
+
const extensions = stream.getBlock(extenstionLength);
|
|
128
|
+
|
|
129
|
+
// Hash algo - enum { sha256(4), . . . (255) }
|
|
130
|
+
const hashAlgorithm = stream.getUint8();
|
|
131
|
+
|
|
132
|
+
// Signature algo - enum { anonymous(0), rsa(1), dsa(2), ecdsa(3), (255) }
|
|
133
|
+
const signatureAlgorithm = stream.getUint8();
|
|
134
|
+
|
|
135
|
+
// Signature - opaque signature<0..2^16-1>;
|
|
136
|
+
const sigLength = stream.getUint16();
|
|
137
|
+
const signature = stream.getBlock(sigLength);
|
|
138
|
+
|
|
139
|
+
// Check that we read the entire buffer
|
|
140
|
+
if (stream.position !== buf.length) {
|
|
141
|
+
throw new Error("SCT buffer length mismatch");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return new SignedCertificateTimestamp({
|
|
145
|
+
version,
|
|
146
|
+
logID,
|
|
147
|
+
timestamp,
|
|
148
|
+
extensions,
|
|
149
|
+
hashAlgorithm,
|
|
150
|
+
signatureAlgorithm,
|
|
151
|
+
signature,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["DOM", "ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"sourceMap": true,
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["./src/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
minify: false,
|
|
7
|
+
outDir: "dist",
|
|
8
|
+
target: "esnext",
|
|
9
|
+
lib: {
|
|
10
|
+
entry: "src/index.ts",
|
|
11
|
+
formats: ["es"],
|
|
12
|
+
fileName: "index",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
test: {
|
|
16
|
+
globals: true,
|
|
17
|
+
browser: {
|
|
18
|
+
provider: "playwright",
|
|
19
|
+
enabled: true,
|
|
20
|
+
headless: false,
|
|
21
|
+
instances: [
|
|
22
|
+
{ browser: "chromium" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
outDir: "dist",
|
|
7
|
+
target: "esnext",
|
|
8
|
+
lib: {
|
|
9
|
+
entry: "src/index.ts",
|
|
10
|
+
formats: ["es"],
|
|
11
|
+
fileName: "index",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
test: {
|
|
15
|
+
globals: true,
|
|
16
|
+
environment: "node",
|
|
17
|
+
},
|
|
18
|
+
});
|