@noz-ele/edgca 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/LICENSE +21 -0
- package/README.md +270 -0
- package/SECURITY.md +34 -0
- package/dist/bytes.d.ts +9 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +58 -0
- package/dist/ca.d.ts +6 -0
- package/dist/ca.d.ts.map +1 -0
- package/dist/ca.js +193 -0
- package/dist/crypto.d.ts +16 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +104 -0
- package/dist/der.d.ts +45 -0
- package/dist/der.d.ts.map +1 -0
- package/dist/der.js +269 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/ip.d.ts +2 -0
- package/dist/ip.d.ts.map +1 -0
- package/dist/ip.js +55 -0
- package/dist/name.d.ts +4 -0
- package/dist/name.d.ts.map +1 -0
- package/dist/name.js +71 -0
- package/dist/oids.d.ts +15 -0
- package/dist/oids.d.ts.map +1 -0
- package/dist/oids.js +48 -0
- package/dist/parser.d.ts +17 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +146 -0
- package/dist/pem.d.ts +7 -0
- package/dist/pem.d.ts.map +1 -0
- package/dist/pem.js +52 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/verify.d.ts +46 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +73 -0
- package/dist/x509.d.ts +34 -0
- package/dist/x509.d.ts.map +1 -0
- package/dist/x509.js +197 -0
- package/package.json +66 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { bytesEqual } from "./bytes.js";
|
|
2
|
+
import { decodeInteger, decodeOid, readChildren, readElement, readSequenceChildren, TAG } from "./der.js";
|
|
3
|
+
import { importPublicKeySpki } from "./crypto.js";
|
|
4
|
+
import { OID } from "./oids.js";
|
|
5
|
+
export async function parseCertificateDer(der) {
|
|
6
|
+
const certificate = readElement(der);
|
|
7
|
+
if (certificate.tag !== TAG.SEQUENCE || certificate.end !== der.length) {
|
|
8
|
+
throw new Error("Invalid certificate DER");
|
|
9
|
+
}
|
|
10
|
+
const [tbsCertificate, signatureAlgorithm, signatureValue] = readSequenceChildren(certificate);
|
|
11
|
+
if (!tbsCertificate || !signatureAlgorithm || !signatureValue) {
|
|
12
|
+
throw new Error("Invalid certificate structure");
|
|
13
|
+
}
|
|
14
|
+
if (signatureValue.tag !== TAG.BIT_STRING || signatureValue.value[0] !== 0) {
|
|
15
|
+
throw new Error("Invalid certificate signature value");
|
|
16
|
+
}
|
|
17
|
+
const tbsChildren = readSequenceChildren(tbsCertificate);
|
|
18
|
+
// RFC 5280 §4.1.2.1: version is [0] EXPLICIT INTEGER. EdgCA only emits and
|
|
19
|
+
// accepts v3 (= INTEGER 2); v1 (field omitted) and v2 (INTEGER 1) are rejected.
|
|
20
|
+
// See docs/NON_GOALS.md §4.
|
|
21
|
+
const versionTag = tbsChildren[0];
|
|
22
|
+
if (!versionTag || versionTag.tag !== 0xa0) {
|
|
23
|
+
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
24
|
+
}
|
|
25
|
+
const versionInner = readElement(versionTag.value);
|
|
26
|
+
if (versionInner.tag !== TAG.INTEGER || decodeInteger(versionInner.value) !== 2n) {
|
|
27
|
+
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
28
|
+
}
|
|
29
|
+
let index = 1;
|
|
30
|
+
index += 1; // serialNumber
|
|
31
|
+
index += 1; // signature
|
|
32
|
+
const issuer = tbsChildren[index++];
|
|
33
|
+
index += 1; // validity
|
|
34
|
+
const subject = tbsChildren[index++];
|
|
35
|
+
const subjectPublicKeyInfo = tbsChildren[index++];
|
|
36
|
+
if (!issuer || !subject || !subjectPublicKeyInfo) {
|
|
37
|
+
throw new Error("Invalid certificate TBSCertificate structure");
|
|
38
|
+
}
|
|
39
|
+
const extensions = tbsChildren.find((element) => element.tag === 0xa3);
|
|
40
|
+
const parsedExtensions = extensions ? parseExtensions(extensions.value) : {};
|
|
41
|
+
const publicKey = await importPublicKeySpki(subjectPublicKeyInfo.raw);
|
|
42
|
+
const parsed = {
|
|
43
|
+
der,
|
|
44
|
+
tbsCertificateDer: tbsCertificate.raw,
|
|
45
|
+
signatureDer: signatureValue.value.subarray(1),
|
|
46
|
+
issuerNameDer: issuer.raw,
|
|
47
|
+
subjectNameDer: subject.raw,
|
|
48
|
+
subjectPublicKeyInfoDer: subjectPublicKeyInfo.raw,
|
|
49
|
+
publicKey,
|
|
50
|
+
isCA: parsedExtensions.isCA ?? false,
|
|
51
|
+
keyCertSign: parsedExtensions.keyCertSign ?? false
|
|
52
|
+
};
|
|
53
|
+
if (parsedExtensions.pathLenConstraint !== undefined) {
|
|
54
|
+
parsed.pathLenConstraint = parsedExtensions.pathLenConstraint;
|
|
55
|
+
}
|
|
56
|
+
if (parsedExtensions.subjectKeyIdentifier !== undefined) {
|
|
57
|
+
parsed.subjectKeyIdentifier = parsedExtensions.subjectKeyIdentifier;
|
|
58
|
+
}
|
|
59
|
+
if (parsedExtensions.authorityKeyIdentifier !== undefined) {
|
|
60
|
+
parsed.authorityKeyIdentifier = parsedExtensions.authorityKeyIdentifier;
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
export function assertIssuerSubjectMatches(issuer, issued) {
|
|
65
|
+
if (!bytesEqual(issuer.subjectNameDer, issued.issuerNameDer)) {
|
|
66
|
+
throw new Error("Issued certificate issuer does not match CA subject");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function parseExtensions(explicitValue) {
|
|
70
|
+
const outer = readElement(explicitValue);
|
|
71
|
+
if (outer.tag !== TAG.SEQUENCE) {
|
|
72
|
+
throw new Error("Invalid extensions structure");
|
|
73
|
+
}
|
|
74
|
+
const parsed = {};
|
|
75
|
+
for (const extension of readSequenceChildren(outer)) {
|
|
76
|
+
const children = readSequenceChildren(extension);
|
|
77
|
+
const oidElement = children[0];
|
|
78
|
+
if (!oidElement || oidElement.tag !== TAG.OBJECT_IDENTIFIER) {
|
|
79
|
+
throw new Error("Invalid extension OID");
|
|
80
|
+
}
|
|
81
|
+
let valueIndex = 1;
|
|
82
|
+
if (children[valueIndex]?.tag === TAG.BOOLEAN) {
|
|
83
|
+
valueIndex += 1;
|
|
84
|
+
}
|
|
85
|
+
const value = children[valueIndex];
|
|
86
|
+
if (!value || value.tag !== TAG.OCTET_STRING) {
|
|
87
|
+
throw new Error("Invalid extension value");
|
|
88
|
+
}
|
|
89
|
+
const extensionOid = decodeOid(oidElement.value);
|
|
90
|
+
if (extensionOid === OID.basicConstraints) {
|
|
91
|
+
Object.assign(parsed, parseBasicConstraints(value.value));
|
|
92
|
+
}
|
|
93
|
+
else if (extensionOid === OID.keyUsage) {
|
|
94
|
+
parsed.keyCertSign = parseKeyUsage(value.value).keyCertSign;
|
|
95
|
+
}
|
|
96
|
+
else if (extensionOid === OID.subjectKeyIdentifier) {
|
|
97
|
+
parsed.subjectKeyIdentifier = parseOctetString(value.value);
|
|
98
|
+
}
|
|
99
|
+
else if (extensionOid === OID.authorityKeyIdentifier) {
|
|
100
|
+
const keyIdentifier = parseAuthorityKeyIdentifier(value.value);
|
|
101
|
+
if (keyIdentifier !== undefined) {
|
|
102
|
+
parsed.authorityKeyIdentifier = keyIdentifier;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
function parseBasicConstraints(value) {
|
|
109
|
+
const root = readElement(value);
|
|
110
|
+
const children = readSequenceChildren(root);
|
|
111
|
+
const result = { isCA: false };
|
|
112
|
+
if (children[0]?.tag === TAG.BOOLEAN) {
|
|
113
|
+
result.isCA = children[0].value[0] !== 0;
|
|
114
|
+
}
|
|
115
|
+
const pathLen = children.find((child) => child.tag === TAG.INTEGER);
|
|
116
|
+
if (pathLen) {
|
|
117
|
+
result.pathLenConstraint = Number(decodeInteger(pathLen.value));
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
function parseKeyUsage(value) {
|
|
122
|
+
const root = readElement(value);
|
|
123
|
+
if (root.tag !== TAG.BIT_STRING || root.value.length < 2) {
|
|
124
|
+
throw new Error("Invalid keyUsage extension");
|
|
125
|
+
}
|
|
126
|
+
const bytes = root.value.subarray(1);
|
|
127
|
+
return {
|
|
128
|
+
keyCertSign: (bytes[0] & 0x04) !== 0
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function parseOctetString(value) {
|
|
132
|
+
const root = readElement(value);
|
|
133
|
+
if (root.tag !== TAG.OCTET_STRING) {
|
|
134
|
+
throw new Error("Invalid OCTET STRING extension payload");
|
|
135
|
+
}
|
|
136
|
+
return root.value;
|
|
137
|
+
}
|
|
138
|
+
function parseAuthorityKeyIdentifier(value) {
|
|
139
|
+
const root = readElement(value);
|
|
140
|
+
for (const child of readSequenceChildren(root)) {
|
|
141
|
+
if (child.tag === 0x80) {
|
|
142
|
+
return child.value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
package/dist/pem.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function certificateToPem(der: Uint8Array): string;
|
|
2
|
+
export declare function privateKeyDerToPem(der: Uint8Array): string;
|
|
3
|
+
export declare function publicKeyDerToPem(der: Uint8Array): string;
|
|
4
|
+
export declare function pemToDer(pem: string): Uint8Array;
|
|
5
|
+
export declare function pemToDerWithLabel(pem: string, label: string): Uint8Array;
|
|
6
|
+
export declare function splitPemBlocks(pem: string): string[];
|
|
7
|
+
//# sourceMappingURL=pem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pem.d.ts","sourceRoot":"","sources":["../src/pem.ts"],"names":[],"mappings":"AAIA,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAExD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAE1D;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAEzD;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAWhD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAaxE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAUpD"}
|
package/dist/pem.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { binaryToBytes, bytesToBinary } from "./bytes.js";
|
|
2
|
+
const PEM_LINE_LENGTH = 64;
|
|
3
|
+
export function certificateToPem(der) {
|
|
4
|
+
return encodePem("CERTIFICATE", der);
|
|
5
|
+
}
|
|
6
|
+
export function privateKeyDerToPem(der) {
|
|
7
|
+
return encodePem("PRIVATE KEY", der);
|
|
8
|
+
}
|
|
9
|
+
export function publicKeyDerToPem(der) {
|
|
10
|
+
return encodePem("PUBLIC KEY", der);
|
|
11
|
+
}
|
|
12
|
+
export function pemToDer(pem) {
|
|
13
|
+
const match = /-----BEGIN (.+?)-----([\s\S]*?)-----END \1-----/.exec(pem);
|
|
14
|
+
if (!match || !match[2]) {
|
|
15
|
+
throw new Error("Invalid PEM block");
|
|
16
|
+
}
|
|
17
|
+
const base64 = match[2].replace(/\s+/g, "");
|
|
18
|
+
if (base64.length === 0) {
|
|
19
|
+
throw new Error("Invalid PEM block: empty body");
|
|
20
|
+
}
|
|
21
|
+
return binaryToBytes(atob(base64));
|
|
22
|
+
}
|
|
23
|
+
export function pemToDerWithLabel(pem, label) {
|
|
24
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
const pattern = new RegExp(`-----BEGIN ${escaped}-----([\\s\\S]*?)-----END ${escaped}-----`);
|
|
26
|
+
const match = pattern.exec(pem);
|
|
27
|
+
if (!match?.[1]) {
|
|
28
|
+
throw new Error(`Invalid PEM block: expected ${label}`);
|
|
29
|
+
}
|
|
30
|
+
const base64 = match[1].replace(/\s+/g, "");
|
|
31
|
+
if (base64.length === 0) {
|
|
32
|
+
throw new Error(`Invalid PEM block: empty ${label} body`);
|
|
33
|
+
}
|
|
34
|
+
return binaryToBytes(atob(base64));
|
|
35
|
+
}
|
|
36
|
+
export function splitPemBlocks(pem) {
|
|
37
|
+
const blocks = [];
|
|
38
|
+
const pattern = /-----BEGIN (.+?)-----[\s\S]*?-----END \1-----/g;
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = pattern.exec(pem)) !== null) {
|
|
41
|
+
blocks.push(match[0]);
|
|
42
|
+
}
|
|
43
|
+
return blocks;
|
|
44
|
+
}
|
|
45
|
+
function encodePem(label, der) {
|
|
46
|
+
const base64 = btoa(bytesToBinary(der));
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (let i = 0; i < base64.length; i += PEM_LINE_LENGTH) {
|
|
49
|
+
lines.push(base64.slice(i, i + PEM_LINE_LENGTH));
|
|
50
|
+
}
|
|
51
|
+
return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
|
|
52
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type ShortSubjectAttributeType = "CN" | "O" | "OU" | "C" | "ST" | "L" | "E" | "DC" | "SERIALNUMBER" | "STREET" | "POSTALCODE" | "TITLE" | "GIVENNAME" | "SURNAME" | "UID";
|
|
2
|
+
export type DottedOid = `${number}.${number}${string}`;
|
|
3
|
+
export type SubjectAttributeType = ShortSubjectAttributeType | DottedOid;
|
|
4
|
+
export interface SubjectAttribute {
|
|
5
|
+
type: SubjectAttributeType;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
export type Subject = SubjectAttribute[];
|
|
9
|
+
export type SerialNumber = bigint | number | string | Uint8Array;
|
|
10
|
+
export interface CertificateAuthority {
|
|
11
|
+
certPem: string;
|
|
12
|
+
privateKeyPem: string;
|
|
13
|
+
publicKeyPem: string;
|
|
14
|
+
certDer: Uint8Array;
|
|
15
|
+
privateKey: CryptoKey;
|
|
16
|
+
publicKey: CryptoKey;
|
|
17
|
+
issuerChainPem: string;
|
|
18
|
+
}
|
|
19
|
+
export interface IssuedClientCertificate {
|
|
20
|
+
certPem: string;
|
|
21
|
+
privateKeyPem: string;
|
|
22
|
+
publicKeyPem: string;
|
|
23
|
+
certDer: Uint8Array;
|
|
24
|
+
privateKey: CryptoKey;
|
|
25
|
+
publicKey: CryptoKey;
|
|
26
|
+
certChainPem: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CreateRootCAOptions {
|
|
29
|
+
subject: Subject;
|
|
30
|
+
days: number;
|
|
31
|
+
notBefore?: Date;
|
|
32
|
+
serialNumber?: SerialNumber;
|
|
33
|
+
pathLenConstraint?: number;
|
|
34
|
+
privateKeyPem?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface IssueIntermediateCAOptions {
|
|
37
|
+
ca: CertificateAuthority;
|
|
38
|
+
subject: Subject;
|
|
39
|
+
days: number;
|
|
40
|
+
notBefore?: Date;
|
|
41
|
+
serialNumber?: SerialNumber;
|
|
42
|
+
pathLenConstraint?: number;
|
|
43
|
+
privateKeyPem?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface IssueClientCertOptions {
|
|
46
|
+
ca: CertificateAuthority;
|
|
47
|
+
subject: Subject;
|
|
48
|
+
days: number;
|
|
49
|
+
notBefore?: Date;
|
|
50
|
+
serialNumber?: SerialNumber;
|
|
51
|
+
dnsNames?: string[];
|
|
52
|
+
ipAddresses?: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface ImportCertificateAuthorityOptions {
|
|
55
|
+
certPem: string;
|
|
56
|
+
privateKeyPem: string;
|
|
57
|
+
issuerChainPem?: string;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,yBAAyB,GACjC,IAAI,GACJ,GAAG,GACH,IAAI,GACJ,GAAG,GACH,IAAI,GACJ,GAAG,GACH,GAAG,GACH,IAAI,GACJ,cAAc,GACd,QAAQ,GACR,YAAY,GACZ,OAAO,GACP,WAAW,GACX,SAAS,GACT,KAAK,CAAC;AAEV,MAAM,MAAM,SAAS,GAAG,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,EAAE,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,yBAAyB,GAAG,SAAS,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,oBAAoB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;AAEzC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,CAAC;IACpB,UAAU,EAAE,SAAS,CAAC;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,CAAC;IACpB,UAAU,EAAE,SAAS,CAAC;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,oBAAoB,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,oBAAoB,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,iCAAiC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CertificateAuthority } from "./types.js";
|
|
2
|
+
export interface VerifyClientCertificateValidity {
|
|
3
|
+
notBefore: Date | number;
|
|
4
|
+
notAfter: Date | number;
|
|
5
|
+
now?: Date | number;
|
|
6
|
+
}
|
|
7
|
+
export interface VerifyClientCertificateIssuedByOptions {
|
|
8
|
+
ca: CertificateAuthority;
|
|
9
|
+
certPem: string;
|
|
10
|
+
validity?: VerifyClientCertificateValidity;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Confirms that `options.certPem` was issued by `options.ca`.
|
|
14
|
+
*
|
|
15
|
+
* This is **not** mTLS verification, and does not even attempt to be.
|
|
16
|
+
* At most it is *issuance verification*: "the presented certificate was
|
|
17
|
+
* issued by the specified CA" — which is **not** the same as authenticating
|
|
18
|
+
* that the presenter is the certificate's legitimate owner.
|
|
19
|
+
*
|
|
20
|
+
* A client certificate is, by design, presentable to anyone, and its
|
|
21
|
+
* contents are trivially copyable. You must assume that anyone could be
|
|
22
|
+
* holding a valid copy. Therefore possession of valid certificate data
|
|
23
|
+
* **never** proves legitimate ownership.
|
|
24
|
+
*
|
|
25
|
+
* Proving legitimate ownership additionally requires verifying possession
|
|
26
|
+
* of the corresponding private key (a signature made by it, verified
|
|
27
|
+
* against the certificate's public key). The TLS handshake's
|
|
28
|
+
* `CertificateVerify` message normally provides this, but the Cloudflare
|
|
29
|
+
* Workers runtime does not expose that signature to the application. On
|
|
30
|
+
* non-Enterprise plans, Cloudflare's TLS layer also does not know about
|
|
31
|
+
* your self-managed CA, so `request.cf.tlsClientAuth.certVerified` will
|
|
32
|
+
* not be `"SUCCESS"` for certificates EdgCA issued. Application code on
|
|
33
|
+
* Workers (Enterprise excluded) has no way to verify proof-of-possession.
|
|
34
|
+
*
|
|
35
|
+
* Implication: anyone who has obtained a copy of a valid certificate
|
|
36
|
+
* (logs, leaked storage, network capture, etc.) can present it and pass
|
|
37
|
+
* this check. Use this as a minimum identity-check layer, not as
|
|
38
|
+
* authentication. For real authentication, use Cloudflare Enterprise mTLS
|
|
39
|
+
* at the TLS layer, or add an application-layer challenge-response that
|
|
40
|
+
* has the client sign a server-issued nonce with its private key.
|
|
41
|
+
*
|
|
42
|
+
* Also out of scope (not checked here): `BasicConstraints CA=false`,
|
|
43
|
+
* `EKU clientAuth`, revocation, and chain walking.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyClientCertificateIssuedBy(options: VerifyClientCertificateIssuedByOptions): Promise<boolean>;
|
|
46
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sCAAsC;IACrD,EAAE,EAAE,oBAAoB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,+BAA+B,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,sCAAsC,GAC9C,OAAO,CAAC,OAAO,CAAC,CAwBlB"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { bytesEqual } from "./bytes.js";
|
|
2
|
+
import { keyIdentifierFromSpki, verifyDer } from "./crypto.js";
|
|
3
|
+
import { pemToDerWithLabel } from "./pem.js";
|
|
4
|
+
import { parseCertificateDer } from "./parser.js";
|
|
5
|
+
/**
|
|
6
|
+
* Confirms that `options.certPem` was issued by `options.ca`.
|
|
7
|
+
*
|
|
8
|
+
* This is **not** mTLS verification, and does not even attempt to be.
|
|
9
|
+
* At most it is *issuance verification*: "the presented certificate was
|
|
10
|
+
* issued by the specified CA" — which is **not** the same as authenticating
|
|
11
|
+
* that the presenter is the certificate's legitimate owner.
|
|
12
|
+
*
|
|
13
|
+
* A client certificate is, by design, presentable to anyone, and its
|
|
14
|
+
* contents are trivially copyable. You must assume that anyone could be
|
|
15
|
+
* holding a valid copy. Therefore possession of valid certificate data
|
|
16
|
+
* **never** proves legitimate ownership.
|
|
17
|
+
*
|
|
18
|
+
* Proving legitimate ownership additionally requires verifying possession
|
|
19
|
+
* of the corresponding private key (a signature made by it, verified
|
|
20
|
+
* against the certificate's public key). The TLS handshake's
|
|
21
|
+
* `CertificateVerify` message normally provides this, but the Cloudflare
|
|
22
|
+
* Workers runtime does not expose that signature to the application. On
|
|
23
|
+
* non-Enterprise plans, Cloudflare's TLS layer also does not know about
|
|
24
|
+
* your self-managed CA, so `request.cf.tlsClientAuth.certVerified` will
|
|
25
|
+
* not be `"SUCCESS"` for certificates EdgCA issued. Application code on
|
|
26
|
+
* Workers (Enterprise excluded) has no way to verify proof-of-possession.
|
|
27
|
+
*
|
|
28
|
+
* Implication: anyone who has obtained a copy of a valid certificate
|
|
29
|
+
* (logs, leaked storage, network capture, etc.) can present it and pass
|
|
30
|
+
* this check. Use this as a minimum identity-check layer, not as
|
|
31
|
+
* authentication. For real authentication, use Cloudflare Enterprise mTLS
|
|
32
|
+
* at the TLS layer, or add an application-layer challenge-response that
|
|
33
|
+
* has the client sign a server-issued nonce with its private key.
|
|
34
|
+
*
|
|
35
|
+
* Also out of scope (not checked here): `BasicConstraints CA=false`,
|
|
36
|
+
* `EKU clientAuth`, revocation, and chain walking.
|
|
37
|
+
*/
|
|
38
|
+
export async function verifyClientCertificateIssuedBy(options) {
|
|
39
|
+
if (options.validity && !isWithinValidity(options.validity)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const certDer = pemToDerWithLabel(options.certPem, "CERTIFICATE");
|
|
43
|
+
const cert = await parseCertificateDer(certDer);
|
|
44
|
+
const issuer = await parseCertificateDer(options.ca.certDer);
|
|
45
|
+
if (!bytesEqual(cert.issuerNameDer, issuer.subjectNameDer)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (!cert.authorityKeyIdentifier) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const issuerSki = issuer.subjectKeyIdentifier ??
|
|
52
|
+
(await keyIdentifierFromSpki(issuer.subjectPublicKeyInfoDer));
|
|
53
|
+
if (!bytesEqual(cert.authorityKeyIdentifier, issuerSki)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return verifyDer(options.ca.publicKey, cert.signatureDer, cert.tbsCertificateDer);
|
|
57
|
+
}
|
|
58
|
+
function isWithinValidity(validity) {
|
|
59
|
+
const notBefore = toEpochMs(validity.notBefore, "validity.notBefore");
|
|
60
|
+
const notAfter = toEpochMs(validity.notAfter, "validity.notAfter");
|
|
61
|
+
const now = validity.now !== undefined ? toEpochMs(validity.now, "validity.now") : Date.now();
|
|
62
|
+
if (notBefore > notAfter) {
|
|
63
|
+
throw new Error("validity.notBefore must be less than or equal to validity.notAfter");
|
|
64
|
+
}
|
|
65
|
+
return notBefore <= now && now <= notAfter;
|
|
66
|
+
}
|
|
67
|
+
function toEpochMs(value, name) {
|
|
68
|
+
const ms = value instanceof Date ? value.getTime() : value;
|
|
69
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) {
|
|
70
|
+
throw new Error(`${name} must be a finite Date or epoch milliseconds number`);
|
|
71
|
+
}
|
|
72
|
+
return ms;
|
|
73
|
+
}
|
package/dist/x509.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SerialNumber } from "./types.js";
|
|
2
|
+
export interface CertificateBuildInput {
|
|
3
|
+
serialNumber?: SerialNumber | undefined;
|
|
4
|
+
notBefore?: Date | undefined;
|
|
5
|
+
days: number;
|
|
6
|
+
issuerNameDer: Uint8Array;
|
|
7
|
+
subjectNameDer: Uint8Array;
|
|
8
|
+
subjectPublicKeyInfoDer: Uint8Array;
|
|
9
|
+
extensions: Uint8Array[];
|
|
10
|
+
}
|
|
11
|
+
export interface TbsCertificateResult {
|
|
12
|
+
tbsCertificateDer: Uint8Array;
|
|
13
|
+
serialNumberDer: Uint8Array;
|
|
14
|
+
notBefore: Date;
|
|
15
|
+
notAfter: Date;
|
|
16
|
+
}
|
|
17
|
+
export declare function buildTbsCertificate(input: CertificateBuildInput): TbsCertificateResult;
|
|
18
|
+
export declare function buildCertificate(tbsCertificateDer: Uint8Array, signatureDer: Uint8Array): Uint8Array;
|
|
19
|
+
export declare function ecdsaWithSha256AlgorithmIdentifier(): Uint8Array;
|
|
20
|
+
export declare function basicConstraintsCaExtension(pathLenConstraint: number): Uint8Array;
|
|
21
|
+
export declare function basicConstraintsLeafExtension(): Uint8Array;
|
|
22
|
+
export declare function keyUsageExtension(usages: readonly KeyUsageBit[]): Uint8Array;
|
|
23
|
+
export declare function extendedKeyUsageClientAuthExtension(): Uint8Array;
|
|
24
|
+
export declare function subjectKeyIdentifierExtension(keyIdentifier: Uint8Array): Uint8Array;
|
|
25
|
+
export declare function authorityKeyIdentifierExtension(keyIdentifier: Uint8Array): Uint8Array;
|
|
26
|
+
export declare function subjectAltNameExtension(dnsNames?: readonly string[], ipAddresses?: readonly string[]): Uint8Array | undefined;
|
|
27
|
+
declare const KEY_USAGE_BITS: {
|
|
28
|
+
readonly digitalSignature: 0;
|
|
29
|
+
readonly keyCertSign: 5;
|
|
30
|
+
readonly cRLSign: 6;
|
|
31
|
+
};
|
|
32
|
+
type KeyUsageBit = keyof typeof KEY_USAGE_BITS;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=x509.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"x509.d.ts","sourceRoot":"","sources":["../src/x509.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IACxC,SAAS,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,UAAU,CAAC;IAC1B,cAAc,EAAE,UAAU,CAAC;IAC3B,uBAAuB,EAAE,UAAU,CAAC;IACpC,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,iBAAiB,EAAE,UAAU,CAAC;IAC9B,eAAe,EAAE,UAAU,CAAC;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,IAAI,CAAC;CAChB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,qBAAqB,GAAG,oBAAoB,CAoBtF;AAED,wBAAgB,gBAAgB,CAAC,iBAAiB,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,GAAG,UAAU,CAEpG;AAED,wBAAgB,kCAAkC,IAAI,UAAU,CAE/D;AAED,wBAAgB,2BAA2B,CAAC,iBAAiB,EAAE,MAAM,GAAG,UAAU,CAMjF;AAED,wBAAgB,6BAA6B,IAAI,UAAU,CAE1D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,UAAU,CAgB5E;AAED,wBAAgB,mCAAmC,IAAI,UAAU,CAEhE;AAED,wBAAgB,6BAA6B,CAAC,aAAa,EAAE,UAAU,GAAG,UAAU,CAEnF;AAED,wBAAgB,+BAA+B,CAAC,aAAa,EAAE,UAAU,GAAG,UAAU,CAErF;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,SAAS,CAmC7H;AAwID,QAAA,MAAM,cAAc;;;;CAIV,CAAC;AAEX,KAAK,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC"}
|
package/dist/x509.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { asciiBytes, concatBytes } from "./bytes.js";
|
|
2
|
+
import { bitString, boolean, contextPrimitive, der, explicit, generalizedTime, integer, octetString, oid, readElement, sequence, TAG, utcTime } from "./der.js";
|
|
3
|
+
import { encodeIpAddress } from "./ip.js";
|
|
4
|
+
import { OID } from "./oids.js";
|
|
5
|
+
export function buildTbsCertificate(input) {
|
|
6
|
+
const { notBefore, notAfter } = resolveValidity(input.notBefore, input.days);
|
|
7
|
+
const serialNumberDer = encodeSerialNumber(input.serialNumber);
|
|
8
|
+
const tbsCertificateDer = sequence(explicit(0, integer(2)), serialNumberDer, ecdsaWithSha256AlgorithmIdentifier(), input.issuerNameDer, sequence(encodeTime(notBefore), encodeTime(notAfter)), input.subjectNameDer, input.subjectPublicKeyInfoDer, explicit(3, sequence(...input.extensions)));
|
|
9
|
+
return {
|
|
10
|
+
tbsCertificateDer,
|
|
11
|
+
serialNumberDer,
|
|
12
|
+
notBefore,
|
|
13
|
+
notAfter
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function buildCertificate(tbsCertificateDer, signatureDer) {
|
|
17
|
+
return sequence(tbsCertificateDer, ecdsaWithSha256AlgorithmIdentifier(), bitString(signatureDer));
|
|
18
|
+
}
|
|
19
|
+
export function ecdsaWithSha256AlgorithmIdentifier() {
|
|
20
|
+
return sequence(oid(OID.ecdsaWithSha256));
|
|
21
|
+
}
|
|
22
|
+
export function basicConstraintsCaExtension(pathLenConstraint) {
|
|
23
|
+
return extension(OID.basicConstraints, true, sequence(boolean(true), integer(pathLenConstraint)));
|
|
24
|
+
}
|
|
25
|
+
export function basicConstraintsLeafExtension() {
|
|
26
|
+
return extension(OID.basicConstraints, true, sequence());
|
|
27
|
+
}
|
|
28
|
+
export function keyUsageExtension(usages) {
|
|
29
|
+
let maxBit = 0;
|
|
30
|
+
for (const usage of usages) {
|
|
31
|
+
maxBit = Math.max(maxBit, KEY_USAGE_BITS[usage]);
|
|
32
|
+
}
|
|
33
|
+
const byteLength = Math.floor(maxBit / 8) + 1;
|
|
34
|
+
const bytes = new Uint8Array(byteLength);
|
|
35
|
+
for (const usage of usages) {
|
|
36
|
+
const bit = KEY_USAGE_BITS[usage];
|
|
37
|
+
bytes[Math.floor(bit / 8)] |= 0x80 >> (bit % 8);
|
|
38
|
+
}
|
|
39
|
+
const unusedBits = byteLength * 8 - maxBit - 1;
|
|
40
|
+
return extension(OID.keyUsage, true, bitString(bytes, unusedBits));
|
|
41
|
+
}
|
|
42
|
+
export function extendedKeyUsageClientAuthExtension() {
|
|
43
|
+
return extension(OID.extendedKeyUsage, false, sequence(oid(OID.clientAuth)));
|
|
44
|
+
}
|
|
45
|
+
export function subjectKeyIdentifierExtension(keyIdentifier) {
|
|
46
|
+
return extension(OID.subjectKeyIdentifier, false, octetString(keyIdentifier));
|
|
47
|
+
}
|
|
48
|
+
export function authorityKeyIdentifierExtension(keyIdentifier) {
|
|
49
|
+
return extension(OID.authorityKeyIdentifier, false, sequence(contextPrimitive(0, keyIdentifier)));
|
|
50
|
+
}
|
|
51
|
+
export function subjectAltNameExtension(dnsNames, ipAddresses) {
|
|
52
|
+
assertOptionalStringArray("dnsNames", dnsNames);
|
|
53
|
+
assertOptionalStringArray("ipAddresses", ipAddresses);
|
|
54
|
+
const names = [];
|
|
55
|
+
const seenDnsNames = new Set();
|
|
56
|
+
const seenIpAddresses = new Set();
|
|
57
|
+
for (const dnsName of dnsNames ?? []) {
|
|
58
|
+
if (typeof dnsName !== "string" || dnsName.length > 253 || !DNS_NAME_PATTERN.test(dnsName)) {
|
|
59
|
+
throw new Error(`Invalid SAN dNSName: ${dnsName}`);
|
|
60
|
+
}
|
|
61
|
+
if (seenDnsNames.has(dnsName)) {
|
|
62
|
+
throw new Error(`Duplicate SAN dNSName: ${dnsName}`);
|
|
63
|
+
}
|
|
64
|
+
seenDnsNames.add(dnsName);
|
|
65
|
+
names.push(der(0x82, asciiBytes(dnsName)));
|
|
66
|
+
}
|
|
67
|
+
for (const ipAddress of ipAddresses ?? []) {
|
|
68
|
+
if (typeof ipAddress !== "string") {
|
|
69
|
+
throw new Error(`Invalid SAN iPAddress: ${ipAddress}`);
|
|
70
|
+
}
|
|
71
|
+
const encoded = encodeIpAddress(ipAddress);
|
|
72
|
+
// Compare by encoded bytes so that textually different forms of the same IP
|
|
73
|
+
// (e.g. "::1" vs "0:0:0:0:0:0:0:1") are correctly recognized as duplicates.
|
|
74
|
+
const key = encoded.join(",");
|
|
75
|
+
if (seenIpAddresses.has(key)) {
|
|
76
|
+
throw new Error(`Duplicate SAN iPAddress: ${ipAddress}`);
|
|
77
|
+
}
|
|
78
|
+
seenIpAddresses.add(key);
|
|
79
|
+
names.push(der(0x87, encoded));
|
|
80
|
+
}
|
|
81
|
+
return names.length > 0 ? extension(OID.subjectAltName, false, sequence(...names)) : undefined;
|
|
82
|
+
}
|
|
83
|
+
// RFC 1035 §2.3.1 preferred name syntax: each label starts and ends with [A-Za-z0-9],
|
|
84
|
+
// may contain hyphens internally, and is at most 63 characters. Optional leading "*." wildcard.
|
|
85
|
+
const DNS_LABEL = /[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?/;
|
|
86
|
+
const DNS_NAME_PATTERN = new RegExp(`^(\\*\\.)?${DNS_LABEL.source}(?:\\.${DNS_LABEL.source})*$`);
|
|
87
|
+
function assertOptionalStringArray(name, values) {
|
|
88
|
+
if (values !== undefined && !Array.isArray(values)) {
|
|
89
|
+
throw new Error(`${name} must be an array`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function extension(extensionOid, critical, valueDer) {
|
|
93
|
+
return sequence(oid(extensionOid), ...(critical ? [boolean(true)] : []), octetString(valueDer));
|
|
94
|
+
}
|
|
95
|
+
function encodeSerialNumber(serialNumber) {
|
|
96
|
+
const encoded = encodeSerialNumberDer(serialNumber);
|
|
97
|
+
const element = readElement(encoded);
|
|
98
|
+
if (element.length > 20) {
|
|
99
|
+
throw new Error("serialNumber encoded value must not exceed 20 octets");
|
|
100
|
+
}
|
|
101
|
+
if (element.value.every((byte) => byte === 0)) {
|
|
102
|
+
throw new Error("serialNumber must be a positive integer");
|
|
103
|
+
}
|
|
104
|
+
return encoded;
|
|
105
|
+
}
|
|
106
|
+
function encodeSerialNumberDer(serialNumber) {
|
|
107
|
+
if (serialNumber === undefined) {
|
|
108
|
+
const bytes = new Uint8Array(16);
|
|
109
|
+
crypto.getRandomValues(bytes);
|
|
110
|
+
bytes[0] = bytes[0] & 0x7f;
|
|
111
|
+
if (bytes.every((byte) => byte === 0)) {
|
|
112
|
+
bytes[15] = 1;
|
|
113
|
+
}
|
|
114
|
+
return integer(bytes);
|
|
115
|
+
}
|
|
116
|
+
if (serialNumber instanceof Uint8Array) {
|
|
117
|
+
if (serialNumber.length === 0) {
|
|
118
|
+
throw new Error("serialNumber Uint8Array must not be empty");
|
|
119
|
+
}
|
|
120
|
+
if (serialNumber.length > 20) {
|
|
121
|
+
throw new Error("serialNumber Uint8Array must not exceed 20 octets");
|
|
122
|
+
}
|
|
123
|
+
return integer(serialNumber);
|
|
124
|
+
}
|
|
125
|
+
if (typeof serialNumber === "string") {
|
|
126
|
+
if (/^\d+$/.test(serialNumber)) {
|
|
127
|
+
if (serialNumber.length > SERIAL_NUMBER_MAX_DECIMAL_DIGITS) {
|
|
128
|
+
throw new Error(`serialNumber decimal string must not exceed ${SERIAL_NUMBER_MAX_DECIMAL_DIGITS} digits`);
|
|
129
|
+
}
|
|
130
|
+
return integer(BigInt(serialNumber));
|
|
131
|
+
}
|
|
132
|
+
if (/^[0-9a-fA-F]+$/.test(serialNumber)) {
|
|
133
|
+
if (serialNumber.length > SERIAL_NUMBER_MAX_HEX_CHARS) {
|
|
134
|
+
throw new Error(`serialNumber hex string must not exceed ${SERIAL_NUMBER_MAX_HEX_CHARS} characters (20 octets)`);
|
|
135
|
+
}
|
|
136
|
+
const normalized = serialNumber.length % 2 === 0 ? serialNumber : `0${serialNumber}`;
|
|
137
|
+
const bytes = new Uint8Array(normalized.length / 2);
|
|
138
|
+
for (let i = 0; i < normalized.length; i += 2) {
|
|
139
|
+
bytes[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16);
|
|
140
|
+
}
|
|
141
|
+
return integer(bytes);
|
|
142
|
+
}
|
|
143
|
+
throw new Error("serialNumber string must be decimal digits or hex");
|
|
144
|
+
}
|
|
145
|
+
return integer(serialNumber);
|
|
146
|
+
}
|
|
147
|
+
// Maximum string-input lengths chosen so that any string passing this check
|
|
148
|
+
// also passes the post-encode 20-octet limit:
|
|
149
|
+
// - 47-digit decimal: max value 10^47 - 1 < 2^159, fits in 20 octets without leading 0x00.
|
|
150
|
+
// (48-digit values up to 10^48 - 1 ≈ 9.99e47 exceed 2^159 ≈ 7.27e47, so the leading 0x00
|
|
151
|
+
// sign byte would push the encoded length to 21 octets.)
|
|
152
|
+
// - 39-char hex: pads to 40 chars with a leading "0", giving 20 octets whose top byte
|
|
153
|
+
// 0x0X has the high bit clear, so no extra sign byte is added.
|
|
154
|
+
// (40-char hex with leading nibble 0x8-0xf would add a leading 0x00 → 21 octets.)
|
|
155
|
+
const SERIAL_NUMBER_MAX_DECIMAL_DIGITS = 47;
|
|
156
|
+
const SERIAL_NUMBER_MAX_HEX_CHARS = 39;
|
|
157
|
+
function resolveValidity(notBeforeInput, days) {
|
|
158
|
+
if (typeof days !== "number" || !Number.isInteger(days) || days <= 0) {
|
|
159
|
+
throw new Error("days must be a positive integer");
|
|
160
|
+
}
|
|
161
|
+
if (notBeforeInput !== undefined && !(notBeforeInput instanceof Date)) {
|
|
162
|
+
throw new Error("notBefore must be a Date");
|
|
163
|
+
}
|
|
164
|
+
const notBefore = notBeforeInput === undefined ? new Date() : new Date(notBeforeInput.getTime());
|
|
165
|
+
const notBeforeMs = notBefore.getTime();
|
|
166
|
+
if (!Number.isFinite(notBeforeMs)) {
|
|
167
|
+
throw new Error("notBefore must be a valid Date");
|
|
168
|
+
}
|
|
169
|
+
assertYearInRange(notBefore.getUTCFullYear(), "notBefore");
|
|
170
|
+
const notAfterMs = notBeforeMs + days * 86_400_000;
|
|
171
|
+
if (!Number.isFinite(notAfterMs)) {
|
|
172
|
+
throw new Error("notAfter must be a valid Date");
|
|
173
|
+
}
|
|
174
|
+
const notAfter = new Date(notAfterMs);
|
|
175
|
+
if (!Number.isFinite(notAfter.getTime())) {
|
|
176
|
+
throw new Error("notAfter must be a valid Date");
|
|
177
|
+
}
|
|
178
|
+
assertYearInRange(notAfter.getUTCFullYear(), "notAfter");
|
|
179
|
+
return {
|
|
180
|
+
notBefore,
|
|
181
|
+
notAfter
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function encodeTime(date) {
|
|
185
|
+
const year = date.getUTCFullYear();
|
|
186
|
+
return year >= 1950 && year <= 2049 ? utcTime(date) : generalizedTime(date);
|
|
187
|
+
}
|
|
188
|
+
function assertYearInRange(year, fieldName) {
|
|
189
|
+
if (year < 1 || year > 9999) {
|
|
190
|
+
throw new Error(`${fieldName} year must be between 0001 and 9999`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const KEY_USAGE_BITS = {
|
|
194
|
+
digitalSignature: 0,
|
|
195
|
+
keyCertSign: 5,
|
|
196
|
+
cRLSign: 6
|
|
197
|
+
};
|