@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NOZ-ELE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # EdgCA
2
+
3
+ > [日本語](https://github.com/noz-ele/EdgCA/blob/main/docs/jp/README.md) | English
4
+
5
+ EdgCA is a small TypeScript library that issues mTLS client certificates from a self-managed CA on Cloudflare Workers-compatible runtimes.
6
+
7
+ The scope is intentionally narrow:
8
+
9
+ - Create a self-signed root CA.
10
+ - Issue an intermediate CA from a root CA.
11
+ - Issue an mTLS client certificate and private key from an intermediate CA.
12
+ - Decide whether a received client certificate was issued by your own CA.
13
+ - Encode/decode certificates and keys as PEM/DER.
14
+ - Delegate all cryptographic operations to `globalThis.crypto.subtle`.
15
+
16
+ > ⚠ **Not a PKI runtime.** EdgCA is an issuance toolkit, not a general-purpose PKI library or runtime. It does **not** provide chain validation, revocation (CRL/OCSP), key storage, or rotation. `verifyClientCertificateIssuedBy` is **not** mTLS verification and does **not** authenticate the presenter — see [Verify](#verify-cloudflare-worker) below. Operating a CA safely is the caller's responsibility. Full list: [docs/en/NON_GOALS.md](https://github.com/noz-ele/EdgCA/blob/main/docs/en/NON_GOALS.md).
17
+
18
+ ## Status
19
+
20
+ EdgCA is in **v0.1.x — early stabilization**. The author is currently validating the library against real Cloudflare Workers deployments, and the API surface may still shift. To keep that validation focused, **external Issues and PRs are temporarily restricted** and will be re-opened once the API settles. Reading, cloning, forking, and `npm install` are unaffected.
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ npm install @noz-ele/edgca
26
+ ```
27
+
28
+ ESM-only (`"type": "module"`). Runs on any runtime where `globalThis.crypto.subtle` is available (Cloudflare Workers, Node.js 20+, modern browsers, etc.). CommonJS `require` is not supported.
29
+
30
+ ## Quick Start
31
+
32
+ ```ts
33
+ import {
34
+ createRootCA,
35
+ issueIntermediateCA,
36
+ issueClientCert
37
+ } from "@noz-ele/edgca";
38
+
39
+ const root = await createRootCA({
40
+ subject: [{ type: "CN", value: "dev-root" }],
41
+ days: 3650
42
+ });
43
+
44
+ const intermediate = await issueIntermediateCA({
45
+ ca: root,
46
+ subject: [{ type: "CN", value: "dev-intermediate" }],
47
+ days: 365
48
+ });
49
+
50
+ const client = await issueClientCert({
51
+ ca: intermediate,
52
+ subject: [
53
+ { type: "CN", value: "worker-client" },
54
+ { type: "UID", value: "worker-001" }
55
+ ],
56
+ days: 30
57
+ });
58
+
59
+ // Persist these via your secrets manager / KV / vault.
60
+ // Treat client.privateKeyPem as a secret — never log or transmit it.
61
+ // client.certPem — public certificate
62
+ // client.certChainPem — full chain to present during mTLS
63
+ // client.privateKeyPem — secret, hand off only over a trusted channel
64
+ ```
65
+
66
+ The basic shape is:
67
+
68
+ ```text
69
+ root CA -> intermediate CA -> mTLS client certificate
70
+ ```
71
+
72
+ This is the deepest CA hierarchy EdgCA targets. Issuing further intermediate CAs from an intermediate is out of scope.
73
+
74
+ `client.certChainPem` is concatenated in this order:
75
+
76
+ ```text
77
+ client certificate
78
+ issuer certificate
79
+ issuer chain
80
+ ```
81
+
82
+ For a client certificate issued by an EdgCA-built intermediate, the result is `client + intermediate + root`.
83
+
84
+ ## Verify (Cloudflare Worker)
85
+
86
+ > ⚠ **What this is — and is not**
87
+ >
88
+ > `verifyClientCertificateIssuedBy` is **not** mTLS verification. (Real mTLS verification does not exist for a self-managed CA on Cloudflare Workers in the first place.) At most it is *issuance verification*: it confirms that the presented certificate was issued by the specified CA. **That is not the same as authenticating that the presenter is the certificate's legitimate owner.**
89
+ >
90
+ > A client certificate is, by design, presentable to anyone, and its contents are trivially copyable. You must assume that anyone can be holding a valid copy. Therefore possession of valid certificate data **never** proves legitimate ownership.
91
+ >
92
+ > Proving legitimate ownership additionally requires verifying possession of the corresponding private key — i.e., a signature made by the private key, verified against the certificate's public key. The TLS handshake's `CertificateVerify` message normally does this, but **the Cloudflare Workers runtime does not expose that signature to the application.** On non-Enterprise plans, Cloudflare's TLS layer also does not know about your self-managed CA, so `request.cf.tlsClientAuth.certVerified` will not be `"SUCCESS"` for certificates EdgCA issued. Workers application code (Enterprise plans excluded) has no way to verify proof-of-possession.
93
+ >
94
+ > Implication: an attacker who has obtained a copy of a valid certificate (logs, leaked storage, network capture, etc.) can present it and pass this check. Use this function as a *minimum* identity-check layer, not as authentication. For real authentication, either (a) use Cloudflare Enterprise with mTLS configured at the TLS layer (Cloudflare validates the handshake signature against your CA), or (b) add an application-layer challenge-response that has the client sign a server-issued nonce with its private key.
95
+ >
96
+ > Also out of scope (not checked by this function): `BasicConstraints CA=false`, `EKU clientAuth`, revocation, and chain walking.
97
+
98
+ This section assumes a deployment where **Cloudflare has already extracted the client certificate** and exposes it to your application via `request.cf.tlsClientAuth`. EdgCA participates in neither the TLS handshake nor DER parsing of the cert; it consumes the values Cloudflare hands you and performs the issuance check above.
99
+
100
+ ### Formats Cloudflare exposes after extraction
101
+
102
+ | field | format | example |
103
+ | --- | --- | --- |
104
+ | `certPresented` | whether a client cert was sent | `"1"` / `"0"` |
105
+ | `certVerified` | TLS-layer verification status string. **For self-managed CAs on non-Enterprise plans this will not be `"SUCCESS"`** — the TLS layer does not know about your CA. | `"SUCCESS"` / `"FAILED:..."` / `"NONE"` |
106
+ | `certRFC9440` | RFC 9440 Structured Field Item (Byte Sequence). Base64 wrapped in `:` | `":MIIB...:"` |
107
+ | `certNotBefore` / `certNotAfter` | OpenSSL-style textual format (always GMT). Single-digit day padded with two spaces | `"Dec 24 23:59:59 2025 GMT"` / `"Dec 4 23:59:59 2025 GMT"` |
108
+ | `certSubjectDN`, `certIssuerDN`, `certSerial`, etc. | strings | identity extraction |
109
+
110
+ `verifyClientCertificateIssuedBy` accepts PEM (`certPem: string`) and `Date` / epoch ms (`validity.notBefore` / `notAfter`). Those forms **do not match** what Cloudflare provides, so the application must convert:
111
+
112
+ - `certRFC9440` (`":...:"`) → strip the surrounding colons, wrap with PEM markers.
113
+ - `certNotBefore` / `certNotAfter` (textual) → `new Date(...)` (V8 / the Workers runtime parses this format).
114
+
115
+ These parsers live in the caller, not in the library, because (a) we do not want to track Cloudflare's output-format changes, (b) we do not want to rely on runtime-dependent `Date.parse` leniency, and (c) the caller already holds the values, so reimplementing them here would be redundant. See [docs/en/NON_GOALS.md](https://github.com/noz-ele/EdgCA/blob/main/docs/en/NON_GOALS.md) for the full rationale.
116
+
117
+ ### Example
118
+
119
+ ```ts
120
+ import { importCertificateAuthority, verifyClientCertificateIssuedBy } from "@noz-ele/edgca";
121
+
122
+ // At Worker startup: import the CA loaded from your vault once.
123
+ const ca = await importCertificateAuthority({
124
+ certPem: env.CA_CERT_PEM,
125
+ privateKeyPem: env.CA_PRIVATE_KEY_PEM
126
+ });
127
+
128
+ export default {
129
+ async fetch(request: Request): Promise<Response> {
130
+ const tls = request.cf?.tlsClientAuth;
131
+ if (!tls || tls.certPresented !== "1") {
132
+ return new Response("client certificate required", { status: 401 });
133
+ }
134
+ // Note: tls.certVerified !== "SUCCESS" is expected for self-managed CAs
135
+ // on non-Enterprise plans. The application performs the issuance check below.
136
+
137
+ // Convert Cloudflare's formats to the library's formats.
138
+ // certRFC9440 (":base64:") -> PEM string
139
+ // certNotBefore / certNotAfter -> Date
140
+ const b64 = tls.certRFC9440.replace(/^:|:$/g, "");
141
+ const certPem = `-----BEGIN CERTIFICATE-----\n${b64}\n-----END CERTIFICATE-----`;
142
+
143
+ const ok = await verifyClientCertificateIssuedBy({
144
+ ca,
145
+ certPem,
146
+ validity: {
147
+ notBefore: new Date(tls.certNotBefore),
148
+ notAfter: new Date(tls.certNotAfter)
149
+ // omit `now` to use Date.now()
150
+ }
151
+ });
152
+ if (!ok) {
153
+ return new Response("not issued by us, or expired", { status: 403 });
154
+ }
155
+
156
+ // Reminder: passing this check does NOT prove the presenter holds the
157
+ // private key. For real authentication, layer a challenge-response
158
+ // (nonce signed with the client's private key) on top.
159
+
160
+ // Authorization logic: derive identity from cf.tlsClientAuth.certSubjectDN, etc.
161
+ return new Response(`hello, ${tls.certSubjectDN}`);
162
+ }
163
+ };
164
+ ```
165
+
166
+ ### Notes
167
+
168
+ - Omitting `validity` performs only the identity check (issuer DN + AKI/SKI + signature). If you instead inline the time check as two comparisons in the application, the result is equivalent.
169
+ - "Not issued by us" and "outside the validity window" return `false`; malformed PEM/DER throws. The two error categories are deliberately split.
170
+ - Pass the **direct issuer (one cert)** as `ca`. Verifying a leaf issued via an intermediate against the root will return `false` — chain walking is not performed.
171
+
172
+ ## Subject
173
+
174
+ Subject only accepts a structured input. DN strings such as `CN=dev-root,O=Example` are not accepted.
175
+
176
+ ```ts
177
+ const subject = [
178
+ { type: "CN", value: "dev-root" },
179
+ { type: "O", value: "Example" },
180
+ { type: "1.2.3.4.5", value: "custom-value" }
181
+ ];
182
+ ```
183
+
184
+ Supported short names:
185
+
186
+ ```text
187
+ CN, O, OU, C, ST, L, E, DC, SERIALNUMBER, STREET,
188
+ POSTALCODE, TITLE, GIVENNAME, SURNAME, UID
189
+ ```
190
+
191
+ Dotted OID strings are also accepted. The ASN.1 string type for values is fixed at UTF8String, with `C` as PrintableString. Multi-valued RDNs are out of scope.
192
+
193
+ ## Scope
194
+
195
+ In scope:
196
+
197
+ - ECDSA P-256 + SHA-256.
198
+ - Key generation, signing, digest, and key import/export via WebCrypto.
199
+ - Root CA creation.
200
+ - Intermediate CA issuance.
201
+ - mTLS client certificate issuance.
202
+ - Identity check that a cert was issued by your own CA (`verifyClientCertificateIssuedBy`, with optional time-validity check).
203
+ - PEM/DER helpers.
204
+ - Basic Constraints, Key Usage, Extended Key Usage, Subject Alternative Name, SKI, AKI.
205
+
206
+ Intentionally out of scope:
207
+
208
+ - Server certificate issuance.
209
+ - Public chain-validation APIs.
210
+ - Extracting time fields from a cert. `verifyClientCertificateIssuedBy`'s `validity` option performs the time check, but the `notBefore` / `notAfter` values are passed in by the caller from `cf.tlsClientAuth`.
211
+ - CRL, OCSP, revocation databases, revocation checks.
212
+ - Key storage, encryption-at-rest, rotation-state persistence, and integration with KV/D1/R2/Secrets.
213
+ - RSA, EdDSA, other elliptic curves.
214
+ - DN string parsing.
215
+ - Multi-valued RDNs.
216
+
217
+ ## Key Handling
218
+
219
+ This library returns private keys as PEM, so generated keys are extractable.
220
+
221
+ EdgCA only handles key generation and import/export. Where keys are stored, how they are encrypted at rest, how rotation state is persisted, and how they integrate with Cloudflare storage products are all the application's responsibility.
222
+
223
+ ### Bringing your own CA key (recommended)
224
+
225
+ Root and intermediate CAs are long-lived. To keep key management on the caller's side, `createRootCA` and `issueIntermediateCA` accept an existing `privateKeyPem`. This lets the caller's key-management infrastructure handle the full key lifecycle (generation, storage, rotation) consistently, which is the recommended path.
226
+
227
+ ```ts
228
+ const root = await createRootCA({
229
+ subject: [{ type: "CN", value: "dev-root" }],
230
+ days: 3650,
231
+ privateKeyPem: loadFromVault("root") // PKCS#8 PEM already in your vault
232
+ });
233
+
234
+ const intermediate = await issueIntermediateCA({
235
+ ca: root,
236
+ subject: [{ type: "CN", value: "dev-intermediate" }],
237
+ days: 365,
238
+ privateKeyPem: loadFromVault("intermediate")
239
+ });
240
+ ```
241
+
242
+ Omitting `privateKeyPem` causes the library to generate a key internally — convenient for tests and PoCs. Client-certificate keys are intended to be ephemeral, so `issueClientCert` always generates internally.
243
+
244
+ ## Development
245
+
246
+ ```sh
247
+ npm run typecheck
248
+ npm run build
249
+ npm run test
250
+ npm audit
251
+ ```
252
+
253
+ Tests use `@cloudflare/vitest-pool-workers` to verify WebCrypto behavior on the Workers-compatible runtime.
254
+
255
+ ### Property-based tests
256
+
257
+ Round-trip invariants in the lower layers are expressed as `fast-check` property-based tests, kept one file per target module under `test/<module>.property.test.ts`.
258
+
259
+ - [test/der.property.test.ts](https://github.com/noz-ele/EdgCA/blob/main/test/der.property.test.ts) — TLV round-trip for INTEGER / OID / OCTET STRING / BIT STRING / SEQUENCE
260
+ - [test/bytes.property.test.ts](https://github.com/noz-ele/EdgCA/blob/main/test/bytes.property.test.ts) — `concatBytes`, `binaryToBytes`/`bytesToBinary`, `bytesEqual`, `cloneBytes`
261
+ - [test/ip.property.test.ts](https://github.com/noz-ele/EdgCA/blob/main/test/ip.property.test.ts) — IPv4 dotted-quad and IPv6 (full form / `::` compression) encoding
262
+ - [test/pem.property.test.ts](https://github.com/noz-ele/EdgCA/blob/main/test/pem.property.test.ts) — round-trip between `certificateToPem` / `privateKeyDerToPem` / `publicKeyDerToPem` and `pemToDer` / `pemToDerWithLabel` / `splitPemBlocks`
263
+
264
+ `vitest.config.ts` includes `test/**/*.test.ts`, so `npm run test` runs them all together. The certificate-assembly layer (`ca.ts` / `x509.ts`) is intentionally outside the PBT scope and stays example-based in [test/edgca.test.ts](https://github.com/noz-ele/EdgCA/blob/main/test/edgca.test.ts).
265
+
266
+ ## API Documentation
267
+
268
+ See [docs/en/API.md](https://github.com/noz-ele/EdgCA/blob/main/docs/en/API.md) for the full API reference.
269
+
270
+ The initial implementation plan is preserved as history in [docs/jp/PLAN_HISTORY.md](https://github.com/noz-ele/EdgCA/blob/main/docs/jp/PLAN_HISTORY.md) (Japanese only — archival material, not maintained in English).
package/SECURITY.md ADDED
@@ -0,0 +1,34 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please report suspected security issues **privately** via GitHub Security Advisories:
6
+
7
+ - https://github.com/noz-ele/EdgCA/security/advisories/new
8
+
9
+ Do not open public issues for security reports. We will respond on a best-effort basis; this is a small project without an SLA.
10
+
11
+ ## Scope
12
+
13
+ EdgCA is a stateless certificate-issuance toolkit for Cloudflare Workers-compatible runtimes. The following are **in scope** for security reports:
14
+
15
+ - Incorrect ASN.1 / DER encoding of issued certificates that could mislead a verifier.
16
+ - Cryptographic signature or KDF misuse, including incorrect use of `globalThis.crypto.subtle`.
17
+ - Memory-safety problems, infinite loops, or unbounded allocations triggered by malformed PEM/DER input to public functions.
18
+ - Public-API surface that allows a caller to produce a certificate that violates the documented invariants (e.g., `issueIntermediateCA` producing `pathLenConstraint > 0`).
19
+
20
+ The following are **out of scope** (see [docs/en/NON_GOALS.md](docs/en/NON_GOALS.md) for the full list and rationale):
21
+
22
+ - Chain validation, revocation (CRL/OCSP), key storage, rotation — EdgCA does not provide them.
23
+ - Operational misuse of issued material (leaked private keys, logging secrets, weak storage).
24
+ - Caller-controlled inputs producing wrong outputs by design ("garbage in, garbage out" behavior is documented; e.g., `importCertificateAuthority` does not cryptographically validate that `issuerChainPem` actually issued `certPem`).
25
+ - `verifyClientCertificateIssuedBy` not authenticating the presenter — by design, this function only verifies issuance, not proof-of-possession of the private key. See the Verify section of the README for details.
26
+ - Vulnerabilities in upstream dependencies (Workers runtime, Node.js, browser WebCrypto). Report those upstream.
27
+
28
+ ## Audit status
29
+
30
+ EdgCA has **not** been independently audited. The implementation is small and self-contained, but you should treat this software as best-effort and review it yourself before relying on it for production trust hierarchies.
31
+
32
+ ## Supported versions
33
+
34
+ Only the latest version published to npm is supported. Fixes will not be backported to older versions.
@@ -0,0 +1,9 @@
1
+ export declare function concatBytes(parts: readonly Uint8Array[]): Uint8Array;
2
+ export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
3
+ export declare function utf8Bytes(value: string): Uint8Array;
4
+ export declare function asciiBytes(value: string): Uint8Array;
5
+ export declare function bytesToBinary(bytes: Uint8Array): string;
6
+ export declare function binaryToBytes(binary: string): Uint8Array;
7
+ export declare function cloneBytes(bytes: Uint8Array): Uint8Array;
8
+ export declare function arrayBufferFromBytes(bytes: Uint8Array): ArrayBuffer;
9
+ //# sourceMappingURL=bytes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,CAWpE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAWhE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAEnD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAYpD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAUvD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAQxD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAExD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAInE"}
package/dist/bytes.js ADDED
@@ -0,0 +1,58 @@
1
+ export function concatBytes(parts) {
2
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
3
+ const out = new Uint8Array(total);
4
+ let offset = 0;
5
+ for (const part of parts) {
6
+ out.set(part, offset);
7
+ offset += part.length;
8
+ }
9
+ return out;
10
+ }
11
+ export function bytesEqual(a, b) {
12
+ if (a.length !== b.length) {
13
+ return false;
14
+ }
15
+ let diff = 0;
16
+ for (let i = 0; i < a.length; i += 1) {
17
+ diff |= a[i] ^ b[i];
18
+ }
19
+ return diff === 0;
20
+ }
21
+ export function utf8Bytes(value) {
22
+ return new TextEncoder().encode(value);
23
+ }
24
+ export function asciiBytes(value) {
25
+ const out = new Uint8Array(value.length);
26
+ for (let i = 0; i < value.length; i += 1) {
27
+ const code = value.charCodeAt(i);
28
+ if (code > 0x7f) {
29
+ throw new Error("Value must contain ASCII characters only");
30
+ }
31
+ out[i] = code;
32
+ }
33
+ return out;
34
+ }
35
+ export function bytesToBinary(bytes) {
36
+ const chunkSize = 0x8000;
37
+ let out = "";
38
+ for (let i = 0; i < bytes.length; i += chunkSize) {
39
+ const chunk = bytes.subarray(i, i + chunkSize);
40
+ out += String.fromCharCode(...chunk);
41
+ }
42
+ return out;
43
+ }
44
+ export function binaryToBytes(binary) {
45
+ const out = new Uint8Array(binary.length);
46
+ for (let i = 0; i < binary.length; i += 1) {
47
+ out[i] = binary.charCodeAt(i);
48
+ }
49
+ return out;
50
+ }
51
+ export function cloneBytes(bytes) {
52
+ return new Uint8Array(bytes);
53
+ }
54
+ export function arrayBufferFromBytes(bytes) {
55
+ const copy = new Uint8Array(bytes.length);
56
+ copy.set(bytes);
57
+ return copy.buffer;
58
+ }
package/dist/ca.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate } from "./types.js";
2
+ export declare function createRootCA(options: CreateRootCAOptions): Promise<CertificateAuthority>;
3
+ export declare function issueIntermediateCA(options: IssueIntermediateCAOptions): Promise<CertificateAuthority>;
4
+ export declare function issueClientCert(options: IssueClientCertOptions): Promise<IssuedClientCertificate>;
5
+ export declare function importCertificateAuthority(options: ImportCertificateAuthorityOptions): Promise<CertificateAuthority>;
6
+ //# sourceMappingURL=ca.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ca.d.ts","sourceRoot":"","sources":["../src/ca.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,EACjC,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAapB,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAwB9F;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA8B5G;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAyCvG;AAED,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkB1H"}
package/dist/ca.js ADDED
@@ -0,0 +1,193 @@
1
+ import { bytesEqual, cloneBytes } from "./bytes.js";
2
+ import { assertKeyPairMatches, exportSpki, generateKeyPair, importPrivateKeyPem, keyIdentifierFromSpki, keyPairFromPrivateKeyPem, privateKeyToPem, publicKeyToPem, signDer } from "./crypto.js";
3
+ import { encodeName } from "./name.js";
4
+ import { certificateToPem, pemToDer, pemToDerWithLabel, splitPemBlocks } from "./pem.js";
5
+ import { parseCertificateDer } from "./parser.js";
6
+ import { authorityKeyIdentifierExtension, basicConstraintsCaExtension, basicConstraintsLeafExtension, buildCertificate, buildTbsCertificate, extendedKeyUsageClientAuthExtension, keyUsageExtension, subjectAltNameExtension, subjectKeyIdentifierExtension } from "./x509.js";
7
+ export async function createRootCA(options) {
8
+ const keyPair = await resolveKeyPair(options.privateKeyPem);
9
+ const subjectNameDer = encodeName(options.subject);
10
+ const spki = await exportSpki(keyPair.publicKey);
11
+ const keyIdentifier = await keyIdentifierFromSpki(spki);
12
+ const pathLenConstraint = resolveRootPathLenConstraint(options.pathLenConstraint);
13
+ const extensions = [
14
+ basicConstraintsCaExtension(pathLenConstraint),
15
+ keyUsageExtension(["keyCertSign", "cRLSign"]),
16
+ subjectKeyIdentifierExtension(keyIdentifier),
17
+ authorityKeyIdentifierExtension(keyIdentifier)
18
+ ];
19
+ const { tbsCertificateDer } = buildTbsCertificate({
20
+ serialNumber: options.serialNumber,
21
+ notBefore: options.notBefore,
22
+ days: options.days,
23
+ issuerNameDer: subjectNameDer,
24
+ subjectNameDer,
25
+ subjectPublicKeyInfoDer: spki,
26
+ extensions
27
+ });
28
+ const signatureDer = await signDer(keyPair.privateKey, tbsCertificateDer);
29
+ const certDer = buildCertificate(tbsCertificateDer, signatureDer);
30
+ return assembleCertificateAuthority(certDer, keyPair, "");
31
+ }
32
+ export async function issueIntermediateCA(options) {
33
+ const issuer = await parseIssuer(options.ca);
34
+ const issuerChainPem = options.ca.issuerChainPem;
35
+ assertCanIssueCertificate(issuer);
36
+ assertCanIssueIntermediate(issuer, issuerChainPem, options.pathLenConstraint);
37
+ const keyPair = await resolveKeyPair(options.privateKeyPem);
38
+ const subjectNameDer = encodeName(options.subject);
39
+ const spki = await exportSpki(keyPair.publicKey);
40
+ const subjectKeyIdentifier = await keyIdentifierFromSpki(spki);
41
+ const authorityKeyIdentifier = issuer.subjectKeyIdentifier ?? await keyIdentifierFromSpki(issuer.subjectPublicKeyInfoDer);
42
+ const extensions = [
43
+ basicConstraintsCaExtension(0),
44
+ keyUsageExtension(["keyCertSign", "cRLSign"]),
45
+ subjectKeyIdentifierExtension(subjectKeyIdentifier),
46
+ authorityKeyIdentifierExtension(authorityKeyIdentifier)
47
+ ];
48
+ const { tbsCertificateDer } = buildTbsCertificate({
49
+ serialNumber: options.serialNumber,
50
+ notBefore: options.notBefore,
51
+ days: options.days,
52
+ issuerNameDer: issuer.subjectNameDer,
53
+ subjectNameDer,
54
+ subjectPublicKeyInfoDer: spki,
55
+ extensions
56
+ });
57
+ const signatureDer = await signDer(options.ca.privateKey, tbsCertificateDer);
58
+ const certDer = buildCertificate(tbsCertificateDer, signatureDer);
59
+ const childChainPem = joinPemChain([options.ca.certPem, issuerChainPem]);
60
+ return assembleCertificateAuthority(certDer, keyPair, childChainPem);
61
+ }
62
+ export async function issueClientCert(options) {
63
+ const issuer = await parseIssuer(options.ca);
64
+ const issuerChainPem = options.ca.issuerChainPem;
65
+ assertCanIssueCertificate(issuer);
66
+ const keyPair = await generateKeyPair();
67
+ const subjectNameDer = encodeName(options.subject);
68
+ const spki = await exportSpki(keyPair.publicKey);
69
+ const subjectKeyIdentifier = await keyIdentifierFromSpki(spki);
70
+ const authorityKeyIdentifier = issuer.subjectKeyIdentifier ?? await keyIdentifierFromSpki(issuer.subjectPublicKeyInfoDer);
71
+ const san = subjectAltNameExtension(options.dnsNames, options.ipAddresses);
72
+ const extensions = [
73
+ basicConstraintsLeafExtension(),
74
+ keyUsageExtension(["digitalSignature"]),
75
+ extendedKeyUsageClientAuthExtension(),
76
+ subjectKeyIdentifierExtension(subjectKeyIdentifier),
77
+ authorityKeyIdentifierExtension(authorityKeyIdentifier),
78
+ ...(san ? [san] : [])
79
+ ];
80
+ const { tbsCertificateDer } = buildTbsCertificate({
81
+ serialNumber: options.serialNumber,
82
+ notBefore: options.notBefore,
83
+ days: options.days,
84
+ issuerNameDer: issuer.subjectNameDer,
85
+ subjectNameDer,
86
+ subjectPublicKeyInfoDer: spki,
87
+ extensions
88
+ });
89
+ const signatureDer = await signDer(options.ca.privateKey, tbsCertificateDer);
90
+ const certDer = buildCertificate(tbsCertificateDer, signatureDer);
91
+ const certPem = certificateToPem(certDer);
92
+ return {
93
+ certPem,
94
+ privateKeyPem: await privateKeyToPem(keyPair.privateKey),
95
+ publicKeyPem: await publicKeyToPem(keyPair.publicKey),
96
+ certDer: cloneBytes(certDer),
97
+ privateKey: keyPair.privateKey,
98
+ publicKey: keyPair.publicKey,
99
+ certChainPem: joinPemChain([certPem, options.ca.certPem, issuerChainPem])
100
+ };
101
+ }
102
+ export async function importCertificateAuthority(options) {
103
+ const issuerChainPem = options.issuerChainPem ?? "";
104
+ assertIssuerChainPem(issuerChainPem);
105
+ const certDer = pemToDerWithLabel(options.certPem, "CERTIFICATE");
106
+ const parsed = await parseCertificateDer(certDer);
107
+ const privateKey = await importPrivateKeyPem(options.privateKeyPem);
108
+ await assertKeyPairMatches(privateKey, parsed.publicKey);
109
+ return {
110
+ certPem: options.certPem,
111
+ privateKeyPem: options.privateKeyPem,
112
+ publicKeyPem: await publicKeyToPem(parsed.publicKey),
113
+ certDer: cloneBytes(certDer),
114
+ privateKey,
115
+ publicKey: parsed.publicKey,
116
+ issuerChainPem
117
+ };
118
+ }
119
+ function assertIssuerChainPem(chainPem) {
120
+ if (chainPem.trim().length === 0) {
121
+ return;
122
+ }
123
+ const blocks = splitPemBlocks(chainPem);
124
+ if (blocks.length === 0) {
125
+ throw new Error("issuerChainPem must contain CERTIFICATE blocks");
126
+ }
127
+ for (const block of blocks) {
128
+ pemToDerWithLabel(block, "CERTIFICATE");
129
+ }
130
+ }
131
+ async function resolveKeyPair(privateKeyPem) {
132
+ if (privateKeyPem !== undefined) {
133
+ return keyPairFromPrivateKeyPem(privateKeyPem);
134
+ }
135
+ return generateKeyPair();
136
+ }
137
+ async function assembleCertificateAuthority(certDer, keyPair, issuerChainPem) {
138
+ return {
139
+ certPem: certificateToPem(certDer),
140
+ privateKeyPem: await privateKeyToPem(keyPair.privateKey),
141
+ publicKeyPem: await publicKeyToPem(keyPair.publicKey),
142
+ certDer: cloneBytes(certDer),
143
+ privateKey: keyPair.privateKey,
144
+ publicKey: keyPair.publicKey,
145
+ issuerChainPem
146
+ };
147
+ }
148
+ async function parseIssuer(ca) {
149
+ return await parseCertificateDer(ca.certDer?.length ? ca.certDer : pemToDer(ca.certPem));
150
+ }
151
+ function assertCanIssueCertificate(issuer) {
152
+ if (!issuer.isCA) {
153
+ throw new Error("Issuer certificate is not a CA");
154
+ }
155
+ if (!issuer.keyCertSign) {
156
+ throw new Error("Issuer certificate keyUsage does not allow certificate signing");
157
+ }
158
+ }
159
+ function assertCanIssueIntermediate(issuer, issuerChainPem, requestedPathLenConstraint) {
160
+ if (issuer.pathLenConstraint === 0) {
161
+ throw new Error("Issuer pathLenConstraint=0 does not allow issuing another intermediate CA");
162
+ }
163
+ if (!isRootCa(issuer, issuerChainPem)) {
164
+ throw new Error("Only root CAs may issue intermediate CAs");
165
+ }
166
+ if (requestedPathLenConstraint === undefined) {
167
+ return;
168
+ }
169
+ if (typeof requestedPathLenConstraint !== "number" || !Object.is(requestedPathLenConstraint, 0)) {
170
+ throw new Error("Intermediate pathLenConstraint must be 0");
171
+ }
172
+ }
173
+ function resolveRootPathLenConstraint(pathLenConstraint) {
174
+ if (pathLenConstraint === undefined) {
175
+ return 1;
176
+ }
177
+ if (typeof pathLenConstraint !== "number" || !Number.isInteger(pathLenConstraint)) {
178
+ throw new Error("Root pathLenConstraint must be 0 or 1");
179
+ }
180
+ if (!Object.is(pathLenConstraint, 0) && pathLenConstraint !== 1) {
181
+ throw new Error("Root pathLenConstraint must be 0 or 1");
182
+ }
183
+ return pathLenConstraint;
184
+ }
185
+ function isRootCa(issuer, issuerChainPem) {
186
+ return issuerChainPem.trim().length === 0 && bytesEqual(issuer.issuerNameDer, issuer.subjectNameDer);
187
+ }
188
+ function joinPemChain(parts) {
189
+ return parts
190
+ .map((part) => part.trim())
191
+ .filter((part) => part.length > 0)
192
+ .join("\n") + "\n";
193
+ }
@@ -0,0 +1,16 @@
1
+ export declare function generateKeyPair(): Promise<CryptoKeyPair>;
2
+ export declare function signDer(privateKey: CryptoKey, data: Uint8Array): Promise<Uint8Array>;
3
+ export declare function verifyDer(publicKey: CryptoKey, signatureDer: Uint8Array, data: Uint8Array): Promise<boolean>;
4
+ export declare function digestSha256(data: Uint8Array): Promise<Uint8Array>;
5
+ export declare function digestSha1(data: Uint8Array): Promise<Uint8Array>;
6
+ export declare function keyIdentifierFromSpki(spki: Uint8Array): Promise<Uint8Array>;
7
+ export declare function privateKeyToPem(key: CryptoKey): Promise<string>;
8
+ export declare function publicKeyToPem(key: CryptoKey): Promise<string>;
9
+ export declare function exportSpki(key: CryptoKey): Promise<Uint8Array>;
10
+ export declare function importPrivateKeyPem(pem: string): Promise<CryptoKey>;
11
+ export declare function keyPairFromPrivateKeyPem(pem: string): Promise<CryptoKeyPair>;
12
+ export declare function importPublicKeySpki(spki: Uint8Array): Promise<CryptoKey>;
13
+ export declare function assertKeyPairMatches(privateKey: CryptoKey, publicKey: CryptoKey): Promise<void>;
14
+ export declare function ecdsaRawToDer(raw: Uint8Array): Uint8Array;
15
+ export declare function ecdsaDerToRaw(signature: Uint8Array): Uint8Array;
16
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAcA,wBAAsB,eAAe,IAAI,OAAO,CAAC,aAAa,CAAC,CAE9D;AAED,wBAAsB,OAAO,CAAC,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAG1F;AAED,wBAAsB,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAOlH;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAExE;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAEtE;AAID,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAWjF;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAGpE;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAEpE;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAEzE;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAOlF;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAE9E;AAED,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAQrG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAMzD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,CAY/D"}