@noz-ele/edgca 0.1.0 → 0.2.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/README.md CHANGED
@@ -9,15 +9,19 @@ The scope is intentionally narrow:
9
9
  - Create a self-signed root CA.
10
10
  - Issue an intermediate CA from a root CA.
11
11
  - Issue an mTLS client certificate and private key from an intermediate CA.
12
+ - Issue an mTLS client certificate from a caller-provided public key (no private key returned). Pairs with the CSR helpers below.
13
+ - Parse a PKCS#10 CSR (subject, requested SAN, public key, raw extensions/attributes) and verify its proof-of-possession signature.
12
14
  - Decide whether a received client certificate was issued by your own CA.
13
- - Encode/decode certificates and keys as PEM/DER.
15
+ - Encode/decode certificates as PEM/DER. Keys are exchanged as `CryptoKey` only — the library never returns or accepts string forms (PEM, JWK, etc.) of private keys.
14
16
  - Delegate all cryptographic operations to `globalThis.crypto.subtle`.
15
17
 
18
+ ECDSA on **NIST P-256, P-384, and P-521** is supported throughout (signing, verification, CSR parsing). RSA, EdDSA, and other curves are intentionally out of scope.
19
+
16
20
  > ⚠ **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
21
 
18
22
  ## Status
19
23
 
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.
24
+ EdgCA is in **v0.2.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
25
 
22
26
  ## Install
23
27
 
@@ -57,10 +61,12 @@ const client = await issueClientCert({
57
61
  });
58
62
 
59
63
  // Persist these via your secrets manager / KV / vault.
60
- // Treat client.privateKeyPem as a secret never log or transmit it.
64
+ // `client.privateKey` is a CryptoKey. To persist it, export with
65
+ // crypto.subtle.exportKey("pkcs8", client.privateKey) (or another form)
66
+ // and treat the resulting bytes as a secret — never log or transmit them.
61
67
  // client.certPem — public certificate
62
68
  // client.certChainPem — full chain to present during mTLS
63
- // client.privateKeyPem — secret, hand off only over a trusted channel
69
+ // client.privateKey — secret CryptoKey, hand off only over a trusted channel
64
70
  ```
65
71
 
66
72
  The basic shape is:
@@ -117,12 +123,22 @@ These parsers live in the caller, not in the library, because (a) we do not want
117
123
  ### Example
118
124
 
119
125
  ```ts
120
- import { importCertificateAuthority, verifyClientCertificateIssuedBy } from "@noz-ele/edgca";
126
+ import { importCertificateAuthority, pemToDer, verifyClientCertificateIssuedBy } from "@noz-ele/edgca";
121
127
 
122
128
  // At Worker startup: import the CA loaded from your vault once.
129
+ // The library accepts the private key as a CryptoKey only — convert from
130
+ // whatever persistence format you use (PKCS#8 PEM, JWK, raw bytes, ...).
131
+ const pkcs8Der = pemToDer(env.CA_PRIVATE_KEY_PEM);
132
+ const privateKey = await crypto.subtle.importKey(
133
+ "pkcs8",
134
+ pkcs8Der,
135
+ { name: "ECDSA", namedCurve: "P-256" },
136
+ /* extractable */ false,
137
+ ["sign"]
138
+ );
123
139
  const ca = await importCertificateAuthority({
124
140
  certPem: env.CA_CERT_PEM,
125
- privateKeyPem: env.CA_PRIVATE_KEY_PEM
141
+ privateKey
126
142
  });
127
143
 
128
144
  export default {
@@ -169,6 +185,42 @@ export default {
169
185
  - "Not issued by us" and "outside the validity window" return `false`; malformed PEM/DER throws. The two error categories are deliberately split.
170
186
  - 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
187
 
188
+ ## Issue from a CSR
189
+
190
+ When a client manages its own private key and submits a PKCS#10 CSR, EdgCA parses the CSR, verifies its proof-of-possession signature, and issues a certificate that embeds the CSR's public key. The library does **not** auto-adopt the CSR's claimed subject / SAN — the caller passes those explicitly, derived from whatever policy applies in the application layer.
191
+
192
+ ```ts
193
+ import {
194
+ importCertificateAuthority,
195
+ issueClientCertForPublicKey,
196
+ parseCertificateSigningRequest,
197
+ verifyCertificateSigningRequestSignature
198
+ } from "@noz-ele/edgca";
199
+
200
+ const csr = await parseCertificateSigningRequest(csrPemFromClient);
201
+ if (!await verifyCertificateSigningRequestSignature(csr)) {
202
+ return new Response("CSR proof-of-possession failed", { status: 400 });
203
+ }
204
+
205
+ // Application decides what subject and SAN to issue with. The CSR's claimed
206
+ // values are available on csr.subject / csr.requestedDnsNames /
207
+ // csr.requestedIpAddresses, but treating them as authoritative is a policy
208
+ // decision that lives outside EdgCA.
209
+ const issued = await issueClientCertForPublicKey({
210
+ ca,
211
+ publicKey: csr.publicKey,
212
+ subject: policyDerivedSubject,
213
+ days: 30,
214
+ dnsNames: policyDerivedDnsNames
215
+ });
216
+ // issued has certPem / certDer / certChainPem only — no privateKey, because
217
+ // the client owns it.
218
+ ```
219
+
220
+ CSRs signed with anything other than `ecdsa-with-SHA256` / `ecdsa-with-SHA384` / `ecdsa-with-SHA512` are rejected at parse time with an explicit error. CSR-level attributes other than `extensionRequest` are surfaced as raw DER under `csr.otherAttributes` for callers that need them; X.509 extensions other than SAN are surfaced under `csr.requestedExtensions` as `{ oid, critical, valueDer }` for caller-side decoding.
221
+
222
+ POP verification proves only that whoever produced the CSR holds the matching private key. **It is not authorization.** Combine it with whatever transport-level (mTLS) and application-level checks make sense for your enrollment flow.
223
+
172
224
  ## Subject
173
225
 
174
226
  Subject only accepts a structured input. DN strings such as `CN=dev-root,O=Example` are not accepted.
@@ -194,13 +246,14 @@ Dotted OID strings are also accepted. The ASN.1 string type for values is fixed
194
246
 
195
247
  In scope:
196
248
 
197
- - ECDSA P-256 + SHA-256.
249
+ - ECDSA on NIST P-256 / P-384 / P-521 (paired with SHA-256 / SHA-384 / SHA-512 respectively).
198
250
  - Key generation, signing, digest, and key import/export via WebCrypto.
199
251
  - Root CA creation.
200
252
  - Intermediate CA issuance.
201
- - mTLS client certificate issuance.
253
+ - mTLS client certificate issuance (with internal key generation, or from a caller-provided public key).
254
+ - CSR (PKCS#10) parsing and proof-of-possession signature verification.
202
255
  - Identity check that a cert was issued by your own CA (`verifyClientCertificateIssuedBy`, with optional time-validity check).
203
- - PEM/DER helpers.
256
+ - PEM/DER helpers (certificates only — keys are exchanged as `CryptoKey`).
204
257
  - Basic Constraints, Key Usage, Extended Key Usage, Subject Alternative Name, SKI, AKI.
205
258
 
206
259
  Intentionally out of scope:
@@ -210,36 +263,64 @@ Intentionally out of scope:
210
263
  - 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
264
  - CRL, OCSP, revocation databases, revocation checks.
212
265
  - Key storage, encryption-at-rest, rotation-state persistence, and integration with KV/D1/R2/Secrets.
213
- - RSA, EdDSA, other elliptic curves.
266
+ - RSA, EdDSA, other elliptic curves (CSRs signed with these algorithms are rejected at parse time).
267
+ - A general certificate parsing API (Cloudflare hands you parsed values via `cf.tlsClientAuth.cert*`; the library does not duplicate that).
268
+ - Issuance policy decisions (whether to honor a CSR's claimed subject/SAN, deduplicate, etc.) — caller's responsibility.
214
269
  - DN string parsing.
215
270
  - Multi-valued RDNs.
216
271
 
217
272
  ## Key Handling
218
273
 
219
- This library returns private keys as PEM, so generated keys are extractable.
274
+ EdgCA exchanges keys as `CryptoKey` only. The library never returns or accepts string forms (PEM, JWK, base64, ...) of private keys, so secret material does not live on the JS string heap at the library boundary. Internally generated keys are extractable so the caller can persist them by calling `crypto.subtle.exportKey` directly, but the choice of persistence format is the caller's.
220
275
 
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.
276
+ EdgCA only handles key generation, signing, and SPKI export of public keys. 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
277
 
223
278
  ### Bringing your own CA key (recommended)
224
279
 
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.
280
+ Root and intermediate CAs are long-lived. To keep key management on the caller's side, `createRootCA` and `issueIntermediateCA` accept an existing `keyPair: CryptoKeyPair`. This lets the caller's key-management infrastructure handle the full key lifecycle (generation, storage, rotation) consistently — including the choice of persistence format — which is the recommended path.
226
281
 
227
282
  ```ts
283
+ // Restore a CryptoKeyPair from whatever persistence format you use.
284
+ // Below is one example that converts PKCS#8 PEM stored in a vault.
285
+ async function loadKeyPair(label: string): Promise<CryptoKeyPair> {
286
+ const pkcs8 = pemToDer(loadFromVault(`${label}-private-pem`));
287
+ const privateKey = await crypto.subtle.importKey(
288
+ "pkcs8",
289
+ pkcs8,
290
+ { name: "ECDSA", namedCurve: "P-256" },
291
+ /* extractable */ false,
292
+ ["sign"]
293
+ );
294
+ // Derive the matching public key. If you also persist the public key as
295
+ // SPKI, import that directly instead of round-tripping through JWK.
296
+ const jwk = await crypto.subtle.exportKey("jwk", privateKey);
297
+ delete jwk.d;
298
+ jwk.key_ops = ["verify"];
299
+ const publicKey = await crypto.subtle.importKey(
300
+ "jwk",
301
+ jwk,
302
+ { name: "ECDSA", namedCurve: "P-256" },
303
+ true,
304
+ ["verify"]
305
+ );
306
+ return { privateKey, publicKey };
307
+ }
308
+
228
309
  const root = await createRootCA({
229
310
  subject: [{ type: "CN", value: "dev-root" }],
230
311
  days: 3650,
231
- privateKeyPem: loadFromVault("root") // PKCS#8 PEM already in your vault
312
+ keyPair: await loadKeyPair("root")
232
313
  });
233
314
 
234
315
  const intermediate = await issueIntermediateCA({
235
316
  ca: root,
236
317
  subject: [{ type: "CN", value: "dev-intermediate" }],
237
318
  days: 365,
238
- privateKeyPem: loadFromVault("intermediate")
319
+ keyPair: await loadKeyPair("intermediate")
239
320
  });
240
321
  ```
241
322
 
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.
323
+ Omitting `keyPair` causes the library to generate a key pair internally — convenient for tests and PoCs. Client-certificate keys are intended to be ephemeral, so `issueClientCert` always generates internally.
243
324
 
244
325
  ## Development
245
326
 
@@ -259,7 +340,7 @@ Round-trip invariants in the lower layers are expressed as `fast-check` property
259
340
  - [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
341
  - [test/bytes.property.test.ts](https://github.com/noz-ele/EdgCA/blob/main/test/bytes.property.test.ts) — `concatBytes`, `binaryToBytes`/`bytesToBinary`, `bytesEqual`, `cloneBytes`
261
342
  - [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`
343
+ - [test/pem.property.test.ts](https://github.com/noz-ele/EdgCA/blob/main/test/pem.property.test.ts) — round-trip between `certificateToPem` and `pemToDer` / `pemToDerWithLabel` / `splitPemBlocks`
263
344
 
264
345
  `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
346
 
package/dist/ca.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate } from "./types.js";
1
+ import type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey } from "./types.js";
2
2
  export declare function createRootCA(options: CreateRootCAOptions): Promise<CertificateAuthority>;
3
3
  export declare function issueIntermediateCA(options: IssueIntermediateCAOptions): Promise<CertificateAuthority>;
4
4
  export declare function issueClientCert(options: IssueClientCertOptions): Promise<IssuedClientCertificate>;
5
+ export declare function issueClientCertForPublicKey(options: IssueClientCertForPublicKeyOptions): Promise<IssuedClientCertificateForPublicKey>;
5
6
  export declare function importCertificateAuthority(options: ImportCertificateAuthorityOptions): Promise<CertificateAuthority>;
6
7
  //# sourceMappingURL=ca.d.ts.map
package/dist/ca.d.ts.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"ca.d.ts","sourceRoot":"","sources":["../src/ca.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,EACjC,kCAAkC,EAClC,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,mCAAmC,EAEpC,MAAM,YAAY,CAAC;AAapB,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA0B9F;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAgC5G;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAqBvG;AAED,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,kCAAkC,GAC1C,OAAO,CAAC,mCAAmC,CAAC,CAkB9C;AAuDD,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAe1H"}
package/dist/ca.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { bytesEqual, cloneBytes } from "./bytes.js";
2
- import { assertKeyPairMatches, exportSpki, generateKeyPair, importPrivateKeyPem, keyIdentifierFromSpki, keyPairFromPrivateKeyPem, privateKeyToPem, publicKeyToPem, signDer } from "./crypto.js";
2
+ import { assertKeyPairMatches, curveOf, exportSpki, generateKeyPair, keyIdentifierFromSpki, signDer } from "./crypto.js";
3
3
  import { encodeName } from "./name.js";
4
4
  import { certificateToPem, pemToDer, pemToDerWithLabel, splitPemBlocks } from "./pem.js";
5
5
  import { parseCertificateDer } from "./parser.js";
6
6
  import { authorityKeyIdentifierExtension, basicConstraintsCaExtension, basicConstraintsLeafExtension, buildCertificate, buildTbsCertificate, extendedKeyUsageClientAuthExtension, keyUsageExtension, subjectAltNameExtension, subjectKeyIdentifierExtension } from "./x509.js";
7
7
  export async function createRootCA(options) {
8
- const keyPair = await resolveKeyPair(options.privateKeyPem);
8
+ const keyPair = await resolveKeyPair(options.keyPair);
9
+ const issuerCurve = curveOf(keyPair.privateKey);
9
10
  const subjectNameDer = encodeName(options.subject);
10
11
  const spki = await exportSpki(keyPair.publicKey);
11
12
  const keyIdentifier = await keyIdentifierFromSpki(spki);
@@ -23,10 +24,11 @@ export async function createRootCA(options) {
23
24
  issuerNameDer: subjectNameDer,
24
25
  subjectNameDer,
25
26
  subjectPublicKeyInfoDer: spki,
26
- extensions
27
+ extensions,
28
+ issuerCurve
27
29
  });
28
30
  const signatureDer = await signDer(keyPair.privateKey, tbsCertificateDer);
29
- const certDer = buildCertificate(tbsCertificateDer, signatureDer);
31
+ const certDer = buildCertificate(tbsCertificateDer, signatureDer, issuerCurve);
30
32
  return assembleCertificateAuthority(certDer, keyPair, "");
31
33
  }
32
34
  export async function issueIntermediateCA(options) {
@@ -34,7 +36,8 @@ export async function issueIntermediateCA(options) {
34
36
  const issuerChainPem = options.ca.issuerChainPem;
35
37
  assertCanIssueCertificate(issuer);
36
38
  assertCanIssueIntermediate(issuer, issuerChainPem, options.pathLenConstraint);
37
- const keyPair = await resolveKeyPair(options.privateKeyPem);
39
+ const issuerCurve = curveOf(options.ca.privateKey);
40
+ const keyPair = await resolveKeyPair(options.keyPair);
38
41
  const subjectNameDer = encodeName(options.subject);
39
42
  const spki = await exportSpki(keyPair.publicKey);
40
43
  const subjectKeyIdentifier = await keyIdentifierFromSpki(spki);
@@ -52,23 +55,57 @@ export async function issueIntermediateCA(options) {
52
55
  issuerNameDer: issuer.subjectNameDer,
53
56
  subjectNameDer,
54
57
  subjectPublicKeyInfoDer: spki,
55
- extensions
58
+ extensions,
59
+ issuerCurve
56
60
  });
57
61
  const signatureDer = await signDer(options.ca.privateKey, tbsCertificateDer);
58
- const certDer = buildCertificate(tbsCertificateDer, signatureDer);
62
+ const certDer = buildCertificate(tbsCertificateDer, signatureDer, issuerCurve);
59
63
  const childChainPem = joinPemChain([options.ca.certPem, issuerChainPem]);
60
64
  return assembleCertificateAuthority(certDer, keyPair, childChainPem);
61
65
  }
62
66
  export async function issueClientCert(options) {
63
- const issuer = await parseIssuer(options.ca);
64
- const issuerChainPem = options.ca.issuerChainPem;
65
- assertCanIssueCertificate(issuer);
66
67
  const keyPair = await generateKeyPair();
67
- const subjectNameDer = encodeName(options.subject);
68
- const spki = await exportSpki(keyPair.publicKey);
68
+ const built = await buildClientCertificate(options.ca, keyPair.publicKey, {
69
+ subject: options.subject,
70
+ days: options.days,
71
+ notBefore: options.notBefore,
72
+ serialNumber: options.serialNumber,
73
+ dnsNames: options.dnsNames,
74
+ ipAddresses: options.ipAddresses
75
+ });
76
+ return {
77
+ certPem: built.certPem,
78
+ certDer: built.certDer,
79
+ privateKey: keyPair.privateKey,
80
+ publicKey: keyPair.publicKey,
81
+ certChainPem: built.certChainPem
82
+ };
83
+ }
84
+ export async function issueClientCertForPublicKey(options) {
85
+ const built = await buildClientCertificate(options.ca, options.publicKey, {
86
+ subject: options.subject,
87
+ days: options.days,
88
+ notBefore: options.notBefore,
89
+ serialNumber: options.serialNumber,
90
+ dnsNames: options.dnsNames,
91
+ ipAddresses: options.ipAddresses
92
+ });
93
+ return {
94
+ certPem: built.certPem,
95
+ certDer: built.certDer,
96
+ certChainPem: built.certChainPem
97
+ };
98
+ }
99
+ async function buildClientCertificate(ca, subjectPublicKey, content) {
100
+ const issuer = await parseIssuer(ca);
101
+ const issuerChainPem = ca.issuerChainPem;
102
+ assertCanIssueCertificate(issuer);
103
+ const issuerCurve = curveOf(ca.privateKey);
104
+ const subjectNameDer = encodeName(content.subject);
105
+ const spki = await exportSpki(subjectPublicKey);
69
106
  const subjectKeyIdentifier = await keyIdentifierFromSpki(spki);
70
107
  const authorityKeyIdentifier = issuer.subjectKeyIdentifier ?? await keyIdentifierFromSpki(issuer.subjectPublicKeyInfoDer);
71
- const san = subjectAltNameExtension(options.dnsNames, options.ipAddresses);
108
+ const san = subjectAltNameExtension(content.dnsNames, content.ipAddresses);
72
109
  const extensions = [
73
110
  basicConstraintsLeafExtension(),
74
111
  keyUsageExtension(["digitalSignature"]),
@@ -78,25 +115,22 @@ export async function issueClientCert(options) {
78
115
  ...(san ? [san] : [])
79
116
  ];
80
117
  const { tbsCertificateDer } = buildTbsCertificate({
81
- serialNumber: options.serialNumber,
82
- notBefore: options.notBefore,
83
- days: options.days,
118
+ serialNumber: content.serialNumber,
119
+ notBefore: content.notBefore,
120
+ days: content.days,
84
121
  issuerNameDer: issuer.subjectNameDer,
85
122
  subjectNameDer,
86
123
  subjectPublicKeyInfoDer: spki,
87
- extensions
124
+ extensions,
125
+ issuerCurve
88
126
  });
89
- const signatureDer = await signDer(options.ca.privateKey, tbsCertificateDer);
90
- const certDer = buildCertificate(tbsCertificateDer, signatureDer);
127
+ const signatureDer = await signDer(ca.privateKey, tbsCertificateDer);
128
+ const certDer = buildCertificate(tbsCertificateDer, signatureDer, issuerCurve);
91
129
  const certPem = certificateToPem(certDer);
92
130
  return {
93
131
  certPem,
94
- privateKeyPem: await privateKeyToPem(keyPair.privateKey),
95
- publicKeyPem: await publicKeyToPem(keyPair.publicKey),
96
132
  certDer: cloneBytes(certDer),
97
- privateKey: keyPair.privateKey,
98
- publicKey: keyPair.publicKey,
99
- certChainPem: joinPemChain([certPem, options.ca.certPem, issuerChainPem])
133
+ certChainPem: joinPemChain([certPem, ca.certPem, issuerChainPem])
100
134
  };
101
135
  }
102
136
  export async function importCertificateAuthority(options) {
@@ -104,14 +138,11 @@ export async function importCertificateAuthority(options) {
104
138
  assertIssuerChainPem(issuerChainPem);
105
139
  const certDer = pemToDerWithLabel(options.certPem, "CERTIFICATE");
106
140
  const parsed = await parseCertificateDer(certDer);
107
- const privateKey = await importPrivateKeyPem(options.privateKeyPem);
108
- await assertKeyPairMatches(privateKey, parsed.publicKey);
141
+ await assertKeyPairMatches(options.privateKey, parsed.publicKey);
109
142
  return {
110
143
  certPem: options.certPem,
111
- privateKeyPem: options.privateKeyPem,
112
- publicKeyPem: await publicKeyToPem(parsed.publicKey),
113
144
  certDer: cloneBytes(certDer),
114
- privateKey,
145
+ privateKey: options.privateKey,
115
146
  publicKey: parsed.publicKey,
116
147
  issuerChainPem
117
148
  };
@@ -128,17 +159,15 @@ function assertIssuerChainPem(chainPem) {
128
159
  pemToDerWithLabel(block, "CERTIFICATE");
129
160
  }
130
161
  }
131
- async function resolveKeyPair(privateKeyPem) {
132
- if (privateKeyPem !== undefined) {
133
- return keyPairFromPrivateKeyPem(privateKeyPem);
162
+ async function resolveKeyPair(provided) {
163
+ if (provided !== undefined) {
164
+ return provided;
134
165
  }
135
166
  return generateKeyPair();
136
167
  }
137
168
  async function assembleCertificateAuthority(certDer, keyPair, issuerChainPem) {
138
169
  return {
139
170
  certPem: certificateToPem(certDer),
140
- privateKeyPem: await privateKeyToPem(keyPair.privateKey),
141
- publicKeyPem: await publicKeyToPem(keyPair.publicKey),
142
171
  certDer: cloneBytes(certDer),
143
172
  privateKey: keyPair.privateKey,
144
173
  publicKey: keyPair.publicKey,
package/dist/crypto.d.ts CHANGED
@@ -1,16 +1,16 @@
1
- export declare function generateKeyPair(): Promise<CryptoKeyPair>;
1
+ export type SupportedCurve = "P-256" | "P-384" | "P-521";
2
+ export declare function curveOf(key: CryptoKey): SupportedCurve;
3
+ export declare function componentSizeForCurve(curve: SupportedCurve): number;
4
+ export declare function signatureAlgorithmOidForCurve(curve: SupportedCurve): string;
5
+ export declare function generateKeyPair(curve?: SupportedCurve): Promise<CryptoKeyPair>;
2
6
  export declare function signDer(privateKey: CryptoKey, data: Uint8Array): Promise<Uint8Array>;
3
7
  export declare function verifyDer(publicKey: CryptoKey, signatureDer: Uint8Array, data: Uint8Array): Promise<boolean>;
4
8
  export declare function digestSha256(data: Uint8Array): Promise<Uint8Array>;
5
9
  export declare function digestSha1(data: Uint8Array): Promise<Uint8Array>;
6
10
  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
11
  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
12
  export declare function importPublicKeySpki(spki: Uint8Array): Promise<CryptoKey>;
13
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;
14
+ export declare function ecdsaRawToDer(raw: Uint8Array, componentSize: number): Uint8Array;
15
+ export declare function ecdsaDerToRaw(signature: Uint8Array, componentSize: number): Uint8Array;
16
16
  //# sourceMappingURL=crypto.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAkCzD,wBAAgB,OAAO,CAAC,GAAG,EAAE,SAAS,GAAG,cAAc,CAUtD;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEnE;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAE3E;AAED,wBAAsB,eAAe,CAAC,KAAK,GAAE,cAAwB,GAAG,OAAO,CAAC,aAAa,CAAC,CAE7F;AAED,wBAAsB,OAAO,CAAC,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAO1F;AAED,wBAAsB,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CASlH;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,UAAU,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAEpE;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAS9E;AAkCD,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAQrG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,UAAU,CAMhF;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,UAAU,CAetF"}
package/dist/crypto.js CHANGED
@@ -1,23 +1,61 @@
1
1
  import { arrayBufferFromBytes, concatBytes } from "./bytes.js";
2
- import { integer, readChildren, readElement, readSequenceChildren, sequence, TAG } from "./der.js";
3
- import { pemToDerWithLabel, privateKeyDerToPem, publicKeyDerToPem } from "./pem.js";
4
- const EC_ALGORITHM = {
5
- name: "ECDSA",
6
- namedCurve: "P-256"
2
+ import { decodeOid, integer, readChildren, readElement, readSequenceChildren, sequence, TAG } from "./der.js";
3
+ import { OID } from "./oids.js";
4
+ const CURVE_PROFILE = {
5
+ "P-256": {
6
+ componentSize: 32,
7
+ hash: "SHA-256",
8
+ signatureAlgorithmOid: OID.ecdsaWithSha256,
9
+ curveOid: OID.secp256r1
10
+ },
11
+ "P-384": {
12
+ componentSize: 48,
13
+ hash: "SHA-384",
14
+ signatureAlgorithmOid: OID.ecdsaWithSha384,
15
+ curveOid: OID.secp384r1
16
+ },
17
+ "P-521": {
18
+ componentSize: 66,
19
+ hash: "SHA-512",
20
+ signatureAlgorithmOid: OID.ecdsaWithSha512,
21
+ curveOid: OID.secp521r1
22
+ }
7
23
  };
8
- const ECDSA_SIGN_ALGORITHM = {
9
- name: "ECDSA",
10
- hash: "SHA-256"
24
+ const CURVE_OID_TO_NAME = {
25
+ [OID.secp256r1]: "P-256",
26
+ [OID.secp384r1]: "P-384",
27
+ [OID.secp521r1]: "P-521"
11
28
  };
12
- export async function generateKeyPair() {
13
- return crypto.subtle.generateKey(EC_ALGORITHM, true, ["sign", "verify"]);
29
+ export function curveOf(key) {
30
+ const algorithm = key.algorithm;
31
+ if (algorithm.name !== "ECDSA") {
32
+ throw new Error(`Expected ECDSA key, got ${algorithm.name}`);
33
+ }
34
+ const curve = algorithm.namedCurve;
35
+ if (curve !== "P-256" && curve !== "P-384" && curve !== "P-521") {
36
+ throw new Error(`Unsupported ECDSA curve: ${curve}`);
37
+ }
38
+ return curve;
39
+ }
40
+ export function componentSizeForCurve(curve) {
41
+ return CURVE_PROFILE[curve].componentSize;
42
+ }
43
+ export function signatureAlgorithmOidForCurve(curve) {
44
+ return CURVE_PROFILE[curve].signatureAlgorithmOid;
45
+ }
46
+ export async function generateKeyPair(curve = "P-256") {
47
+ return crypto.subtle.generateKey({ name: "ECDSA", namedCurve: curve }, true, ["sign", "verify"]);
14
48
  }
15
49
  export async function signDer(privateKey, data) {
16
- const raw = new Uint8Array(await crypto.subtle.sign(ECDSA_SIGN_ALGORITHM, privateKey, arrayBufferFromBytes(data)));
17
- return ecdsaRawToDer(raw);
50
+ const curve = curveOf(privateKey);
51
+ const profile = CURVE_PROFILE[curve];
52
+ const raw = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: profile.hash }, privateKey, arrayBufferFromBytes(data)));
53
+ return ecdsaRawToDer(raw, profile.componentSize);
18
54
  }
19
55
  export async function verifyDer(publicKey, signatureDer, data) {
20
- return crypto.subtle.verify(ECDSA_SIGN_ALGORITHM, publicKey, arrayBufferFromBytes(ecdsaDerToRaw(signatureDer)), arrayBufferFromBytes(data));
56
+ const curve = curveOf(publicKey);
57
+ const profile = CURVE_PROFILE[curve];
58
+ return crypto.subtle.verify({ name: "ECDSA", hash: profile.hash }, publicKey, arrayBufferFromBytes(ecdsaDerToRaw(signatureDer, profile.componentSize)), arrayBufferFromBytes(data));
21
59
  }
22
60
  export async function digestSha256(data) {
23
61
  return new Uint8Array(await crypto.subtle.digest("SHA-256", arrayBufferFromBytes(data)));
@@ -39,30 +77,43 @@ export async function keyIdentifierFromSpki(spki) {
39
77
  }
40
78
  return digestSha1(subjectPublicKey.value.subarray(1));
41
79
  }
42
- export async function privateKeyToPem(key) {
43
- const der = new Uint8Array(await crypto.subtle.exportKey("pkcs8", key));
44
- return privateKeyDerToPem(der);
45
- }
46
- export async function publicKeyToPem(key) {
47
- const der = new Uint8Array(await crypto.subtle.exportKey("spki", key));
48
- return publicKeyDerToPem(der);
49
- }
50
80
  export async function exportSpki(key) {
51
81
  return new Uint8Array(await crypto.subtle.exportKey("spki", key));
52
82
  }
53
- export async function importPrivateKeyPem(pem) {
54
- return crypto.subtle.importKey("pkcs8", arrayBufferFromBytes(pemToDerWithLabel(pem, "PRIVATE KEY")), EC_ALGORITHM, true, ["sign"]);
55
- }
56
- export async function keyPairFromPrivateKeyPem(pem) {
57
- const privateKey = await importPrivateKeyPem(pem);
58
- const jwk = await crypto.subtle.exportKey("jwk", privateKey);
59
- delete jwk.d;
60
- jwk.key_ops = ["verify"];
61
- const publicKey = await crypto.subtle.importKey("jwk", jwk, EC_ALGORITHM, true, ["verify"]);
62
- return { privateKey, publicKey };
63
- }
64
83
  export async function importPublicKeySpki(spki) {
65
- return crypto.subtle.importKey("spki", arrayBufferFromBytes(spki), EC_ALGORITHM, true, ["verify"]);
84
+ const curve = curveFromSpki(spki);
85
+ return crypto.subtle.importKey("spki", arrayBufferFromBytes(spki), { name: "ECDSA", namedCurve: curve }, true, ["verify"]);
86
+ }
87
+ // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }
88
+ // AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY }
89
+ // For EC keys, parameters is the named-curve OID.
90
+ function curveFromSpki(spki) {
91
+ const root = readElement(spki);
92
+ if (root.tag !== TAG.SEQUENCE) {
93
+ throw new Error("Invalid SubjectPublicKeyInfo");
94
+ }
95
+ const algorithm = readSequenceChildren(root)[0];
96
+ if (!algorithm || algorithm.tag !== TAG.SEQUENCE) {
97
+ throw new Error("Invalid SubjectPublicKeyInfo algorithm");
98
+ }
99
+ const algorithmChildren = readSequenceChildren(algorithm);
100
+ const algorithmOidElement = algorithmChildren[0];
101
+ const parametersElement = algorithmChildren[1];
102
+ if (!algorithmOidElement || algorithmOidElement.tag !== TAG.OBJECT_IDENTIFIER) {
103
+ throw new Error("Invalid SubjectPublicKeyInfo algorithm OID");
104
+ }
105
+ if (decodeOid(algorithmOidElement.value) !== OID.ecPublicKey) {
106
+ throw new Error("SubjectPublicKeyInfo is not an EC public key");
107
+ }
108
+ if (!parametersElement || parametersElement.tag !== TAG.OBJECT_IDENTIFIER) {
109
+ throw new Error("EC SubjectPublicKeyInfo parameters must be a named-curve OID");
110
+ }
111
+ const curveOid = decodeOid(parametersElement.value);
112
+ const curve = CURVE_OID_TO_NAME[curveOid];
113
+ if (!curve) {
114
+ throw new Error(`Unsupported EC named curve OID: ${curveOid}`);
115
+ }
116
+ return curve;
66
117
  }
67
118
  export async function assertKeyPairMatches(privateKey, publicKey) {
68
119
  const data = new TextEncoder().encode("edgca-key-pair-check");
@@ -72,13 +123,13 @@ export async function assertKeyPairMatches(privateKey, publicKey) {
72
123
  throw new Error("Private key does not match CA certificate public key");
73
124
  }
74
125
  }
75
- export function ecdsaRawToDer(raw) {
76
- if (raw.length !== 64) {
77
- throw new Error("P-256 ECDSA raw signature must be 64 bytes");
126
+ export function ecdsaRawToDer(raw, componentSize) {
127
+ if (raw.length !== componentSize * 2) {
128
+ throw new Error(`ECDSA raw signature must be ${componentSize * 2} bytes`);
78
129
  }
79
- return sequence(integer(raw.subarray(0, 32)), integer(raw.subarray(32)));
130
+ return sequence(integer(raw.subarray(0, componentSize)), integer(raw.subarray(componentSize)));
80
131
  }
81
- export function ecdsaDerToRaw(signature) {
132
+ export function ecdsaDerToRaw(signature, componentSize) {
82
133
  const root = readElement(signature);
83
134
  if (root.tag !== TAG.SEQUENCE || root.end !== signature.length) {
84
135
  throw new Error("Invalid DER ECDSA signature");
@@ -87,18 +138,21 @@ export function ecdsaDerToRaw(signature) {
87
138
  if (!r || !s || r.tag !== TAG.INTEGER || s.tag !== TAG.INTEGER) {
88
139
  throw new Error("Invalid DER ECDSA signature integers");
89
140
  }
90
- return concatBytes([integerToFixedWidth(r.value), integerToFixedWidth(s.value)]);
141
+ return concatBytes([
142
+ integerToFixedWidth(r.value, componentSize),
143
+ integerToFixedWidth(s.value, componentSize)
144
+ ]);
91
145
  }
92
- function integerToFixedWidth(value) {
146
+ function integerToFixedWidth(value, size) {
93
147
  let start = 0;
94
148
  while (start < value.length - 1 && value[start] === 0) {
95
149
  start += 1;
96
150
  }
97
151
  const trimmed = value.subarray(start);
98
- if (trimmed.length > 32) {
99
- throw new Error("ECDSA integer is wider than P-256");
152
+ if (trimmed.length > size) {
153
+ throw new Error(`ECDSA integer is wider than ${size} bytes`);
100
154
  }
101
- const out = new Uint8Array(32);
102
- out.set(trimmed, 32 - trimmed.length);
155
+ const out = new Uint8Array(size);
156
+ out.set(trimmed, size - trimmed.length);
103
157
  return out;
104
158
  }
package/dist/csr.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Subject } from "./types.js";
2
+ export interface CertificateSigningRequestExtension {
3
+ oid: string;
4
+ critical: boolean;
5
+ valueDer: Uint8Array;
6
+ }
7
+ export interface CertificateSigningRequestAttribute {
8
+ oid: string;
9
+ valuesDer: ReadonlyArray<Uint8Array>;
10
+ }
11
+ export interface ParsedCertificateSigningRequest {
12
+ subject: Subject;
13
+ publicKey: CryptoKey;
14
+ subjectPublicKeyInfoDer: Uint8Array;
15
+ requestedDnsNames: readonly string[];
16
+ requestedIpAddresses: readonly string[];
17
+ requestedExtensions: readonly CertificateSigningRequestExtension[];
18
+ otherAttributes: readonly CertificateSigningRequestAttribute[];
19
+ signatureAlgorithmOid: string;
20
+ signatureDer: Uint8Array;
21
+ certificationRequestInfoDer: Uint8Array;
22
+ }
23
+ export declare function parseCertificateSigningRequest(input: string | Uint8Array): Promise<ParsedCertificateSigningRequest>;
24
+ export declare function verifyCertificateSigningRequestSignature(csr: ParsedCertificateSigningRequest): Promise<boolean>;
25
+ //# sourceMappingURL=csr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csr.d.ts","sourceRoot":"","sources":["../src/csr.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAwB,MAAM,YAAY,CAAC;AAkBhE,MAAM,WAAW,kCAAkC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,kCAAkC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;IACrB,uBAAuB,EAAE,UAAU,CAAC;IACpC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,mBAAmB,EAAE,SAAS,kCAAkC,EAAE,CAAC;IACnE,eAAe,EAAE,SAAS,kCAAkC,EAAE,CAAC;IAC/D,qBAAqB,EAAE,MAAM,CAAC;IAC9B,YAAY,EAAE,UAAU,CAAC;IACzB,2BAA2B,EAAE,UAAU,CAAC;CACzC;AAED,wBAAsB,8BAA8B,CAClD,KAAK,EAAE,MAAM,GAAG,UAAU,GACzB,OAAO,CAAC,+BAA+B,CAAC,CAwE1C;AAED,wBAAsB,wCAAwC,CAC5D,GAAG,EAAE,+BAA+B,GACnC,OAAO,CAAC,OAAO,CAAC,CAElB"}
package/dist/csr.js ADDED
@@ -0,0 +1,266 @@
1
+ import { cloneBytes } from "./bytes.js";
2
+ import { importPublicKeySpki, verifyDer } from "./crypto.js";
3
+ import { decodeInteger, decodeOid, readChildren, readElement, readSequenceChildren, TAG } from "./der.js";
4
+ import { OID, SUBJECT_ATTRIBUTE_OIDS } from "./oids.js";
5
+ import { pemToDerWithLabel, splitPemBlocks } from "./pem.js";
6
+ const SHORT_NAME_BY_OID = Object.fromEntries(Object.entries(SUBJECT_ATTRIBUTE_OIDS).map(([shortName, oid]) => [oid, shortName]));
7
+ const VALUE_DECODERS = new Map([
8
+ [TAG.UTF8_STRING, (b) => new TextDecoder("utf-8", { fatal: true }).decode(b)],
9
+ [TAG.PRINTABLE_STRING, (b) => decodeAscii(b, "PrintableString")],
10
+ [TAG.IA5_STRING, (b) => decodeAscii(b, "IA5String")]
11
+ ]);
12
+ const SUPPORTED_SIGNATURE_OIDS = new Set([
13
+ OID.ecdsaWithSha256,
14
+ OID.ecdsaWithSha384,
15
+ OID.ecdsaWithSha512
16
+ ]);
17
+ export async function parseCertificateSigningRequest(input) {
18
+ const der = typeof input === "string" ? csrPemToDer(input) : input;
19
+ const root = readElement(der);
20
+ if (root.tag !== TAG.SEQUENCE || root.end !== der.length) {
21
+ throw new Error("Invalid CSR DER");
22
+ }
23
+ const [requestInfo, signatureAlgorithm, signatureValue] = readSequenceChildren(root);
24
+ if (!requestInfo || !signatureAlgorithm || !signatureValue) {
25
+ throw new Error("Invalid CSR structure");
26
+ }
27
+ if (requestInfo.tag !== TAG.SEQUENCE) {
28
+ throw new Error("Invalid CSR certificationRequestInfo");
29
+ }
30
+ if (signatureAlgorithm.tag !== TAG.SEQUENCE) {
31
+ throw new Error("Invalid CSR signatureAlgorithm");
32
+ }
33
+ if (signatureValue.tag !== TAG.BIT_STRING || signatureValue.value[0] !== 0) {
34
+ throw new Error("Invalid CSR signature value");
35
+ }
36
+ const signatureAlgorithmOid = readAlgorithmOid(signatureAlgorithm);
37
+ if (!SUPPORTED_SIGNATURE_OIDS.has(signatureAlgorithmOid)) {
38
+ throw new Error(`Unsupported CSR signatureAlgorithm: ${signatureAlgorithmOid}`);
39
+ }
40
+ const infoChildren = readSequenceChildren(requestInfo);
41
+ const versionElement = infoChildren[0];
42
+ const subjectElement = infoChildren[1];
43
+ const spkiElement = infoChildren[2];
44
+ const attributesElement = infoChildren[3];
45
+ if (!versionElement || !subjectElement || !spkiElement) {
46
+ throw new Error("Invalid CSR certificationRequestInfo structure");
47
+ }
48
+ if (versionElement.tag !== TAG.INTEGER || decodeInteger(versionElement.value) !== 0n) {
49
+ throw new Error("Unsupported CSR version (only v1 / INTEGER 0 is supported)");
50
+ }
51
+ if (spkiElement.tag !== TAG.SEQUENCE) {
52
+ throw new Error("Invalid CSR subjectPublicKeyInfo");
53
+ }
54
+ if (attributesElement && attributesElement.tag !== 0xa0) {
55
+ throw new Error("Invalid CSR attributes tag (must be IMPLICIT [0])");
56
+ }
57
+ const subject = decodeName(subjectElement);
58
+ const publicKey = await importPublicKeySpki(spkiElement.raw);
59
+ const allAttributes = attributesElement ? decodeAttributes(attributesElement.value) : [];
60
+ const extensionRequest = allAttributes.find((attribute) => attribute.oid === OID.extensionRequest);
61
+ const otherAttributes = allAttributes.filter((attribute) => attribute.oid !== OID.extensionRequest);
62
+ const requestedExtensions = extensionRequest
63
+ ? decodeRequestedExtensions(extensionRequest.valuesDer)
64
+ : [];
65
+ const sanExtension = requestedExtensions.find((extension) => extension.oid === OID.subjectAltName);
66
+ const { dnsNames, ipAddresses } = sanExtension
67
+ ? decodeSubjectAltName(sanExtension.valueDer)
68
+ : { dnsNames: [], ipAddresses: [] };
69
+ return {
70
+ subject,
71
+ publicKey,
72
+ subjectPublicKeyInfoDer: cloneBytes(spkiElement.raw),
73
+ requestedDnsNames: dnsNames,
74
+ requestedIpAddresses: ipAddresses,
75
+ requestedExtensions,
76
+ otherAttributes,
77
+ signatureAlgorithmOid,
78
+ signatureDer: cloneBytes(signatureValue.value.subarray(1)),
79
+ certificationRequestInfoDer: cloneBytes(requestInfo.raw)
80
+ };
81
+ }
82
+ export async function verifyCertificateSigningRequestSignature(csr) {
83
+ return verifyDer(csr.publicKey, csr.signatureDer, csr.certificationRequestInfoDer);
84
+ }
85
+ function csrPemToDer(pem) {
86
+ // RFC 7468 §7 uses "CERTIFICATE REQUEST"; some legacy tools emit "NEW CERTIFICATE REQUEST".
87
+ const blocks = splitPemBlocks(pem);
88
+ for (const block of blocks) {
89
+ const labelMatch = /-----BEGIN (.+?)-----/.exec(block);
90
+ const label = labelMatch?.[1];
91
+ if (label === "CERTIFICATE REQUEST" || label === "NEW CERTIFICATE REQUEST") {
92
+ return pemToDerWithLabel(block, label);
93
+ }
94
+ }
95
+ throw new Error("Invalid CSR PEM: expected CERTIFICATE REQUEST or NEW CERTIFICATE REQUEST block");
96
+ }
97
+ function readAlgorithmOid(algorithm) {
98
+ const children = readSequenceChildren(algorithm);
99
+ const oidElement = children[0];
100
+ if (!oidElement || oidElement.tag !== TAG.OBJECT_IDENTIFIER) {
101
+ throw new Error("Invalid AlgorithmIdentifier OID");
102
+ }
103
+ return decodeOid(oidElement.value);
104
+ }
105
+ function decodeName(nameElement) {
106
+ if (nameElement.tag !== TAG.SEQUENCE) {
107
+ throw new Error("Invalid Name structure");
108
+ }
109
+ const subject = [];
110
+ for (const rdn of readSequenceChildren(nameElement)) {
111
+ if (rdn.tag !== TAG.SET) {
112
+ throw new Error("Invalid RDN (expected SET)");
113
+ }
114
+ const attributes = readChildren(rdn.value);
115
+ if (attributes.length !== 1) {
116
+ throw new Error("Multi-valued RDNs are not supported");
117
+ }
118
+ const attribute = attributes[0];
119
+ if (attribute.tag !== TAG.SEQUENCE) {
120
+ throw new Error("Invalid AttributeTypeAndValue");
121
+ }
122
+ const [oidElement, valueElement] = readSequenceChildren(attribute);
123
+ if (!oidElement || !valueElement || oidElement.tag !== TAG.OBJECT_IDENTIFIER) {
124
+ throw new Error("Invalid AttributeTypeAndValue contents");
125
+ }
126
+ const attributeOid = decodeOid(oidElement.value);
127
+ const decoder = VALUE_DECODERS.get(valueElement.tag);
128
+ if (!decoder) {
129
+ throw new Error(`Unsupported AttributeValue string type: tag 0x${valueElement.tag.toString(16)}`);
130
+ }
131
+ const value = decoder(valueElement.value);
132
+ const shortName = SHORT_NAME_BY_OID[attributeOid];
133
+ subject.push({
134
+ type: shortName ?? attributeOid,
135
+ value
136
+ });
137
+ }
138
+ return subject;
139
+ }
140
+ function decodeAttributes(value) {
141
+ const attributes = [];
142
+ for (const attribute of readChildren(value)) {
143
+ if (attribute.tag !== TAG.SEQUENCE) {
144
+ throw new Error("Invalid CSR attribute");
145
+ }
146
+ const children = readSequenceChildren(attribute);
147
+ const oidElement = children[0];
148
+ const valuesElement = children[1];
149
+ if (!oidElement || oidElement.tag !== TAG.OBJECT_IDENTIFIER) {
150
+ throw new Error("Invalid CSR attribute OID");
151
+ }
152
+ if (!valuesElement || valuesElement.tag !== TAG.SET) {
153
+ throw new Error("Invalid CSR attribute values (expected SET)");
154
+ }
155
+ const valuesDer = readChildren(valuesElement.value).map((value) => cloneBytes(value.raw));
156
+ attributes.push({
157
+ oid: decodeOid(oidElement.value),
158
+ valuesDer
159
+ });
160
+ }
161
+ return attributes;
162
+ }
163
+ function decodeRequestedExtensions(valuesDer) {
164
+ if (valuesDer.length !== 1) {
165
+ throw new Error("extensionRequest attribute must contain exactly one SEQUENCE OF Extension");
166
+ }
167
+ const outer = readElement(valuesDer[0]);
168
+ if (outer.tag !== TAG.SEQUENCE) {
169
+ throw new Error("extensionRequest value must be a SEQUENCE OF Extension");
170
+ }
171
+ const extensions = [];
172
+ for (const extension of readSequenceChildren(outer)) {
173
+ if (extension.tag !== TAG.SEQUENCE) {
174
+ throw new Error("Invalid Extension");
175
+ }
176
+ const children = readSequenceChildren(extension);
177
+ const oidElement = children[0];
178
+ if (!oidElement || oidElement.tag !== TAG.OBJECT_IDENTIFIER) {
179
+ throw new Error("Invalid Extension OID");
180
+ }
181
+ let cursor = 1;
182
+ let critical = false;
183
+ if (children[cursor]?.tag === TAG.BOOLEAN) {
184
+ critical = children[cursor].value[0] !== 0;
185
+ cursor += 1;
186
+ }
187
+ const valueElement = children[cursor];
188
+ if (!valueElement || valueElement.tag !== TAG.OCTET_STRING) {
189
+ throw new Error("Invalid Extension value");
190
+ }
191
+ extensions.push({
192
+ oid: decodeOid(oidElement.value),
193
+ critical,
194
+ valueDer: cloneBytes(valueElement.value)
195
+ });
196
+ }
197
+ return extensions;
198
+ }
199
+ function decodeSubjectAltName(value) {
200
+ const root = readElement(value);
201
+ if (root.tag !== TAG.SEQUENCE) {
202
+ throw new Error("Invalid SubjectAltName extension");
203
+ }
204
+ const dnsNames = [];
205
+ const ipAddresses = [];
206
+ for (const generalName of readChildren(root.value)) {
207
+ if (generalName.tag === 0x82) {
208
+ dnsNames.push(decodeAscii(generalName.value, "SAN dNSName"));
209
+ }
210
+ else if (generalName.tag === 0x87) {
211
+ ipAddresses.push(formatIpAddress(generalName.value));
212
+ }
213
+ }
214
+ return { dnsNames, ipAddresses };
215
+ }
216
+ function decodeAscii(bytes, label) {
217
+ for (const byte of bytes) {
218
+ if (byte > 0x7f) {
219
+ throw new Error(`${label} contains a non-ASCII byte`);
220
+ }
221
+ }
222
+ return new TextDecoder("ascii").decode(bytes);
223
+ }
224
+ function formatIpAddress(bytes) {
225
+ if (bytes.length === 4) {
226
+ return Array.from(bytes).join(".");
227
+ }
228
+ if (bytes.length === 16) {
229
+ const groups = [];
230
+ for (let i = 0; i < 16; i += 2) {
231
+ groups.push(((bytes[i] << 8) | bytes[i + 1]).toString(16));
232
+ }
233
+ return compressIpv6(groups);
234
+ }
235
+ throw new Error(`Invalid SAN iPAddress length: ${bytes.length}`);
236
+ }
237
+ // RFC 5952: collapse the longest run of consecutive "0" groups (length ≥ 2) into "::".
238
+ function compressIpv6(groups) {
239
+ let bestStart = -1;
240
+ let bestLength = 0;
241
+ let currentStart = -1;
242
+ let currentLength = 0;
243
+ for (let i = 0; i < groups.length; i += 1) {
244
+ if (groups[i] === "0") {
245
+ if (currentStart === -1) {
246
+ currentStart = i;
247
+ currentLength = 0;
248
+ }
249
+ currentLength += 1;
250
+ if (currentLength > bestLength) {
251
+ bestStart = currentStart;
252
+ bestLength = currentLength;
253
+ }
254
+ }
255
+ else {
256
+ currentStart = -1;
257
+ currentLength = 0;
258
+ }
259
+ }
260
+ if (bestLength < 2) {
261
+ return groups.join(":");
262
+ }
263
+ const head = groups.slice(0, bestStart).join(":");
264
+ const tail = groups.slice(bestStart + bestLength).join(":");
265
+ return `${head}::${tail}`;
266
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { createRootCA, importCertificateAuthority, issueClientCert, issueIntermediateCA } from "./ca.js";
1
+ export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueIntermediateCA } from "./ca.js";
2
+ export { parseCertificateSigningRequest, verifyCertificateSigningRequestSignature, type ParsedCertificateSigningRequest, type CertificateSigningRequestExtension, type CertificateSigningRequestAttribute } from "./csr.js";
2
3
  export { pemToDer, certificateToPem } from "./pem.js";
3
- export { privateKeyToPem, publicKeyToPem } from "./crypto.js";
4
4
  export { verifyClientCertificateIssuedBy, type VerifyClientCertificateIssuedByOptions, type VerifyClientCertificateValidity } from "./verify.js";
5
- export type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, SerialNumber, ShortSubjectAttributeType, Subject, SubjectAttribute, SubjectAttributeType } from "./types.js";
5
+ export type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey, SerialNumber, ShortSubjectAttributeType, Subject, SubjectAttribute, SubjectAttributeType } from "./types.js";
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC1B,eAAe,EACf,mBAAmB,EACpB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EACL,+BAA+B,EAC/B,KAAK,sCAAsC,EAC3C,KAAK,+BAA+B,EACrC,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,EACjC,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,YAAY,EACZ,yBAAyB,EACzB,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC1B,eAAe,EACf,2BAA2B,EAC3B,mBAAmB,EACpB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,8BAA8B,EAC9B,wCAAwC,EACxC,KAAK,+BAA+B,EACpC,KAAK,kCAAkC,EACvC,KAAK,kCAAkC,EACxC,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EACL,+BAA+B,EAC/B,KAAK,sCAAsC,EAC3C,KAAK,+BAA+B,EACrC,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,EACjC,kCAAkC,EAClC,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,mCAAmC,EACnC,YAAY,EACZ,yBAAyB,EACzB,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { createRootCA, importCertificateAuthority, issueClientCert, issueIntermediateCA } from "./ca.js";
1
+ export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueIntermediateCA } from "./ca.js";
2
+ export { parseCertificateSigningRequest, verifyCertificateSigningRequestSignature } from "./csr.js";
2
3
  export { pemToDer, certificateToPem } from "./pem.js";
3
- export { privateKeyToPem, publicKeyToPem } from "./crypto.js";
4
4
  export { verifyClientCertificateIssuedBy } from "./verify.js";
package/dist/oids.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { ShortSubjectAttributeType } from "./types.js";
2
2
  export declare const OID: {
3
+ readonly ecPublicKey: "1.2.840.10045.2.1";
4
+ readonly secp256r1: "1.2.840.10045.3.1.7";
5
+ readonly secp384r1: "1.3.132.0.34";
6
+ readonly secp521r1: "1.3.132.0.35";
3
7
  readonly ecdsaWithSha256: "1.2.840.10045.4.3.2";
8
+ readonly ecdsaWithSha384: "1.2.840.10045.4.3.3";
9
+ readonly ecdsaWithSha512: "1.2.840.10045.4.3.4";
4
10
  readonly basicConstraints: "2.5.29.19";
5
11
  readonly keyUsage: "2.5.29.15";
6
12
  readonly extendedKeyUsage: "2.5.29.37";
@@ -8,6 +14,7 @@ export declare const OID: {
8
14
  readonly subjectKeyIdentifier: "2.5.29.14";
9
15
  readonly authorityKeyIdentifier: "2.5.29.35";
10
16
  readonly clientAuth: "1.3.6.1.5.5.7.3.2";
17
+ readonly extensionRequest: "1.2.840.113549.1.9.14";
11
18
  };
12
19
  export declare const SUBJECT_ATTRIBUTE_OIDS: Record<ShortSubjectAttributeType, string>;
13
20
  export declare const SUBJECT_VALUE_LENGTH_LIMITS: Record<ShortSubjectAttributeType, number>;
@@ -1 +1 @@
1
- {"version":3,"file":"oids.d.ts","sourceRoot":"","sources":["../src/oids.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE5D,eAAO,MAAM,GAAG;;;;;;;;;CASN,CAAC;AAEX,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAgB5E,CAAC;AAGF,eAAO,MAAM,2BAA2B,EAAE,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAgBjF,CAAC;AAIF,eAAO,MAAM,wBAAwB,MAAM,CAAC"}
1
+ {"version":3,"file":"oids.d.ts","sourceRoot":"","sources":["../src/oids.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE5D,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;CAgBN,CAAC;AAEX,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAgB5E,CAAC;AAGF,eAAO,MAAM,2BAA2B,EAAE,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAgBjF,CAAC;AAIF,eAAO,MAAM,wBAAwB,MAAM,CAAC"}
package/dist/oids.js CHANGED
@@ -1,12 +1,19 @@
1
1
  export const OID = {
2
+ ecPublicKey: "1.2.840.10045.2.1",
3
+ secp256r1: "1.2.840.10045.3.1.7",
4
+ secp384r1: "1.3.132.0.34",
5
+ secp521r1: "1.3.132.0.35",
2
6
  ecdsaWithSha256: "1.2.840.10045.4.3.2",
7
+ ecdsaWithSha384: "1.2.840.10045.4.3.3",
8
+ ecdsaWithSha512: "1.2.840.10045.4.3.4",
3
9
  basicConstraints: "2.5.29.19",
4
10
  keyUsage: "2.5.29.15",
5
11
  extendedKeyUsage: "2.5.29.37",
6
12
  subjectAltName: "2.5.29.17",
7
13
  subjectKeyIdentifier: "2.5.29.14",
8
14
  authorityKeyIdentifier: "2.5.29.35",
9
- clientAuth: "1.3.6.1.5.5.7.3.2"
15
+ clientAuth: "1.3.6.1.5.5.7.3.2",
16
+ extensionRequest: "1.2.840.113549.1.9.14"
10
17
  };
11
18
  export const SUBJECT_ATTRIBUTE_OIDS = {
12
19
  CN: "2.5.4.3",
package/dist/pem.d.ts CHANGED
@@ -1,6 +1,4 @@
1
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
2
  export declare function pemToDer(pem: string): Uint8Array;
5
3
  export declare function pemToDerWithLabel(pem: string, label: string): Uint8Array;
6
4
  export declare function splitPemBlocks(pem: string): string[];
package/dist/pem.d.ts.map CHANGED
@@ -1 +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"}
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,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 CHANGED
@@ -3,12 +3,6 @@ const PEM_LINE_LENGTH = 64;
3
3
  export function certificateToPem(der) {
4
4
  return encodePem("CERTIFICATE", der);
5
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
6
  export function pemToDer(pem) {
13
7
  const match = /-----BEGIN (.+?)-----([\s\S]*?)-----END \1-----/.exec(pem);
14
8
  if (!match || !match[2]) {
package/dist/types.d.ts CHANGED
@@ -9,8 +9,6 @@ export type Subject = SubjectAttribute[];
9
9
  export type SerialNumber = bigint | number | string | Uint8Array;
10
10
  export interface CertificateAuthority {
11
11
  certPem: string;
12
- privateKeyPem: string;
13
- publicKeyPem: string;
14
12
  certDer: Uint8Array;
15
13
  privateKey: CryptoKey;
16
14
  publicKey: CryptoKey;
@@ -18,8 +16,6 @@ export interface CertificateAuthority {
18
16
  }
19
17
  export interface IssuedClientCertificate {
20
18
  certPem: string;
21
- privateKeyPem: string;
22
- publicKeyPem: string;
23
19
  certDer: Uint8Array;
24
20
  privateKey: CryptoKey;
25
21
  publicKey: CryptoKey;
@@ -31,7 +27,7 @@ export interface CreateRootCAOptions {
31
27
  notBefore?: Date;
32
28
  serialNumber?: SerialNumber;
33
29
  pathLenConstraint?: number;
34
- privateKeyPem?: string;
30
+ keyPair?: CryptoKeyPair;
35
31
  }
36
32
  export interface IssueIntermediateCAOptions {
37
33
  ca: CertificateAuthority;
@@ -40,7 +36,7 @@ export interface IssueIntermediateCAOptions {
40
36
  notBefore?: Date;
41
37
  serialNumber?: SerialNumber;
42
38
  pathLenConstraint?: number;
43
- privateKeyPem?: string;
39
+ keyPair?: CryptoKeyPair;
44
40
  }
45
41
  export interface IssueClientCertOptions {
46
42
  ca: CertificateAuthority;
@@ -51,9 +47,24 @@ export interface IssueClientCertOptions {
51
47
  dnsNames?: string[];
52
48
  ipAddresses?: string[];
53
49
  }
50
+ export interface IssueClientCertForPublicKeyOptions {
51
+ ca: CertificateAuthority;
52
+ publicKey: CryptoKey;
53
+ subject: Subject;
54
+ days: number;
55
+ notBefore?: Date;
56
+ serialNumber?: SerialNumber;
57
+ dnsNames?: string[];
58
+ ipAddresses?: string[];
59
+ }
60
+ export interface IssuedClientCertificateForPublicKey {
61
+ certPem: string;
62
+ certDer: Uint8Array;
63
+ certChainPem: string;
64
+ }
54
65
  export interface ImportCertificateAuthorityOptions {
55
66
  certPem: string;
56
- privateKeyPem: string;
67
+ privateKey: CryptoKey;
57
68
  issuerChainPem?: string;
58
69
  }
59
70
  //# sourceMappingURL=types.d.ts.map
@@ -1 +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"}
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,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,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,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;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,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;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,kCAAkC;IACjD,EAAE,EAAE,oBAAoB,CAAC;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,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,mCAAmC;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,UAAU,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iCAAiC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB"}
package/dist/x509.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type SupportedCurve } from "./crypto.js";
1
2
  import type { SerialNumber } from "./types.js";
2
3
  export interface CertificateBuildInput {
3
4
  serialNumber?: SerialNumber | undefined;
@@ -7,6 +8,7 @@ export interface CertificateBuildInput {
7
8
  subjectNameDer: Uint8Array;
8
9
  subjectPublicKeyInfoDer: Uint8Array;
9
10
  extensions: Uint8Array[];
11
+ issuerCurve: SupportedCurve;
10
12
  }
11
13
  export interface TbsCertificateResult {
12
14
  tbsCertificateDer: Uint8Array;
@@ -15,8 +17,8 @@ export interface TbsCertificateResult {
15
17
  notAfter: Date;
16
18
  }
17
19
  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 buildCertificate(tbsCertificateDer: Uint8Array, signatureDer: Uint8Array, issuerCurve: SupportedCurve): Uint8Array;
21
+ export declare function ecdsaSignatureAlgorithmIdentifier(curve: SupportedCurve): Uint8Array;
20
22
  export declare function basicConstraintsCaExtension(pathLenConstraint: number): Uint8Array;
21
23
  export declare function basicConstraintsLeafExtension(): Uint8Array;
22
24
  export declare function keyUsageExtension(usages: readonly KeyUsageBit[]): Uint8Array;
@@ -1 +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"}
1
+ {"version":3,"file":"x509.d.ts","sourceRoot":"","sources":["../src/x509.ts"],"names":[],"mappings":"AACA,OAAO,EAAiC,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAkBjF,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;IACzB,WAAW,EAAE,cAAc,CAAC;CAC7B;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,CAC9B,iBAAiB,EAAE,UAAU,EAC7B,YAAY,EAAE,UAAU,EACxB,WAAW,EAAE,cAAc,GAC1B,UAAU,CAEZ;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,cAAc,GAAG,UAAU,CAEnF;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 CHANGED
@@ -1,11 +1,12 @@
1
1
  import { asciiBytes, concatBytes } from "./bytes.js";
2
+ import { signatureAlgorithmOidForCurve } from "./crypto.js";
2
3
  import { bitString, boolean, contextPrimitive, der, explicit, generalizedTime, integer, octetString, oid, readElement, sequence, TAG, utcTime } from "./der.js";
3
4
  import { encodeIpAddress } from "./ip.js";
4
5
  import { OID } from "./oids.js";
5
6
  export function buildTbsCertificate(input) {
6
7
  const { notBefore, notAfter } = resolveValidity(input.notBefore, input.days);
7
8
  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
+ const tbsCertificateDer = sequence(explicit(0, integer(2)), serialNumberDer, ecdsaSignatureAlgorithmIdentifier(input.issuerCurve), input.issuerNameDer, sequence(encodeTime(notBefore), encodeTime(notAfter)), input.subjectNameDer, input.subjectPublicKeyInfoDer, explicit(3, sequence(...input.extensions)));
9
10
  return {
10
11
  tbsCertificateDer,
11
12
  serialNumberDer,
@@ -13,11 +14,11 @@ export function buildTbsCertificate(input) {
13
14
  notAfter
14
15
  };
15
16
  }
16
- export function buildCertificate(tbsCertificateDer, signatureDer) {
17
- return sequence(tbsCertificateDer, ecdsaWithSha256AlgorithmIdentifier(), bitString(signatureDer));
17
+ export function buildCertificate(tbsCertificateDer, signatureDer, issuerCurve) {
18
+ return sequence(tbsCertificateDer, ecdsaSignatureAlgorithmIdentifier(issuerCurve), bitString(signatureDer));
18
19
  }
19
- export function ecdsaWithSha256AlgorithmIdentifier() {
20
- return sequence(oid(OID.ecdsaWithSha256));
20
+ export function ecdsaSignatureAlgorithmIdentifier(curve) {
21
+ return sequence(oid(signatureAlgorithmOidForCurve(curve)));
21
22
  }
22
23
  export function basicConstraintsCaExtension(pathLenConstraint) {
23
24
  return extension(OID.basicConstraints, true, sequence(boolean(true), integer(pathLenConstraint)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noz-ele/edgca",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A small Cloudflare Workers-friendly toolkit for issuing self-managed CA and mTLS client certificates.",
5
5
  "type": "module",
6
6
  "license": "MIT",