@noz-ele/edgca 0.3.0 → 0.5.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.
Files changed (46) hide show
  1. package/README.md +109 -2
  2. package/dist/ca.d.ts +2 -1
  3. package/dist/ca.d.ts.map +1 -1
  4. package/dist/ca.js +46 -22
  5. package/dist/cli/commands/create-root-ca.d.ts +2 -0
  6. package/dist/cli/commands/create-root-ca.d.ts.map +1 -0
  7. package/dist/cli/commands/create-root-ca.js +26 -0
  8. package/dist/cli/commands/issue-client.d.ts +2 -0
  9. package/dist/cli/commands/issue-client.d.ts.map +1 -0
  10. package/dist/cli/commands/issue-client.js +46 -0
  11. package/dist/cli/commands/issue-intermediate-ca.d.ts +2 -0
  12. package/dist/cli/commands/issue-intermediate-ca.d.ts.map +1 -0
  13. package/dist/cli/commands/issue-intermediate-ca.js +33 -0
  14. package/dist/cli/commands/pem-to-pfx.d.ts +2 -0
  15. package/dist/cli/commands/pem-to-pfx.d.ts.map +1 -0
  16. package/dist/cli/commands/pem-to-pfx.js +47 -0
  17. package/dist/cli/dn.d.ts +3 -0
  18. package/dist/cli/dn.d.ts.map +1 -0
  19. package/dist/cli/dn.js +65 -0
  20. package/dist/cli/flags.d.ts +8 -0
  21. package/dist/cli/flags.d.ts.map +1 -0
  22. package/dist/cli/flags.js +32 -0
  23. package/dist/cli/io.d.ts +19 -0
  24. package/dist/cli/io.d.ts.map +1 -0
  25. package/dist/cli/io.js +92 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +52 -0
  29. package/dist/csr.d.ts +12 -0
  30. package/dist/csr.d.ts.map +1 -1
  31. package/dist/csr.js +52 -8
  32. package/dist/index.d.ts +6 -4
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -3
  35. package/dist/oids.d.ts +1 -0
  36. package/dist/oids.d.ts.map +1 -1
  37. package/dist/oids.js +1 -0
  38. package/dist/pem.d.ts +2 -0
  39. package/dist/pem.d.ts.map +1 -1
  40. package/dist/pem.js +4 -1
  41. package/dist/types.d.ts +14 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/x509.d.ts +3 -0
  44. package/dist/x509.d.ts.map +1 -1
  45. package/dist/x509.js +5 -1
  46. package/package.json +5 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > [日本語](https://github.com/noz-ele/EdgCA/blob/main/docs/jp/README.md) | English
4
4
 
5
- EdgCA is a small TypeScript library that issues mTLS client certificates from a self-managed CA on Cloudflare Workers-compatible runtimes. It supports internal keygen, CSR-based enrollment (PKCS#10 + proof-of-possession), and PFX (PKCS#12) export for OS keystore import.
5
+ EdgCA is a small TypeScript library that issues mTLS client certificates and document-signing certificates from a self-managed CA on Cloudflare Workers-compatible runtimes. It supports internal keygen, CSR-based enrollment for mTLS leaves (PKCS#10 + proof-of-possession), and PFX (PKCS#12) export for OS keystore import.
6
6
 
7
7
  The scope is intentionally narrow:
8
8
 
@@ -10,6 +10,7 @@ The scope is intentionally narrow:
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
12
  - Issue an mTLS client certificate from a caller-provided public key (no private key returned). Pairs with the CSR helpers below.
13
+ - Issue a document-signing certificate (RFC 9336 `id-kp-documentSigning`) and private key from a CA. Used as the signer cert for CAdES / CMS / ASiC-style document signatures, which are produced by separate tooling.
13
14
  - Parse a PKCS#10 CSR (subject, requested SAN, public key, raw extensions/attributes) and verify its proof-of-possession signature.
14
15
  - Decide whether a received client certificate was issued by your own CA.
15
16
  - 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.
@@ -22,7 +23,9 @@ ECDSA on **NIST P-256, P-384, and P-521** is supported throughout (signing, veri
22
23
 
23
24
  ## Contents
24
25
 
26
+ - [CLI](#cli) — `npx edgca …` one-liners for the four most common tasks
25
27
  - [Quick Start](#quick-start) — root → intermediate → client cert (incl. PFX bundling)
28
+ - [Issue a document-signing certificate](#issue-a-document-signing-certificate) — RFC 9336 `id-kp-documentSigning` leaf
26
29
  - [Verify on Cloudflare Worker](#verify-cloudflare-worker) — confirm a cert was issued by your CA
27
30
  - [Issue from a CSR](#issue-from-a-csr) — accept a caller-managed key via PKCS#10 + POP
28
31
  - [Subject](#subject) · [Scope](#scope) · [Key Handling](#key-handling) · [Development](#development) · [API Documentation](#api-documentation)
@@ -39,6 +42,66 @@ npm install @noz-ele/edgca
39
42
 
40
43
  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.
41
44
 
45
+ ## CLI
46
+
47
+ EdgCA ships a small zero-dependency CLI (`bin: edgca`) for the four most common one-shot tasks. It is a thin wrapper over the library API and uses `node:util.parseArgs` — no transitive dependencies are pulled in for non-CLI consumers.
48
+
49
+ > `npx` runs the CLI **without installing** it — it fetches the package into npm's local cache, executes the `bin`, and leaves your project's `node_modules` / `package.json` untouched. Use this for one-shot tasks (creating a local dev CA, building a PFX). For repeated use, install globally with `npm install -g @noz-ele/edgca` and then call `edgca …` directly.
50
+
51
+ All commands write outputs to the **current working directory**. Filenames are derived from `--name` (default: `root` / `intermediate` / `client`). Cert files are `<name>.crt.pem`, private keys are PKCS#8 PEM at `<name>.key.pem`, and full chains (when relevant) are `<name>.chain.pem`.
52
+
53
+ ```sh
54
+ # 1. Create a root CA (default: P-256, 3650 days)
55
+ npx @noz-ele/edgca create-root-ca --subject "CN=My Test Root,O=Acme,C=JP"
56
+ # → ./root.crt.pem, ./root.key.pem
57
+
58
+ # 2. Issue an intermediate CA from that root
59
+ npx @noz-ele/edgca issue-intermediate-ca \
60
+ --ca-cert root.crt.pem --ca-key root.key.pem \
61
+ --subject "CN=My Intermediate,O=Acme"
62
+ # → ./intermediate.crt.pem, ./intermediate.key.pem, ./intermediate.chain.pem
63
+
64
+ # 3. Issue an mTLS client cert from the intermediate
65
+ npx @noz-ele/edgca issue-client \
66
+ --ca-cert intermediate.crt.pem --ca-key intermediate.key.pem \
67
+ --ca-chain intermediate.chain.pem \
68
+ --subject "CN=alice" \
69
+ --dns-name alice.example.test --ip 10.0.0.1 \
70
+ --days 365
71
+ # → ./client.crt.pem, ./client.key.pem, ./client.chain.pem
72
+
73
+ # 4. Bundle a cert + key (+ optional chain) into a password-protected PFX
74
+ npx @noz-ele/edgca pem-to-pfx \
75
+ --cert client.crt.pem --key client.key.pem --chain client.chain.pem \
76
+ --password "hunter2"
77
+ # → ./client.pfx (defaults to <cert-basename>.pfx next to --cert)
78
+ ```
79
+
80
+ Flags summary (run `npx @noz-ele/edgca --help` to see this in your terminal):
81
+
82
+ ```
83
+ edgca create-root-ca --subject <dn>
84
+ [--days 3650] [--curve P-256|P-384|P-521]
85
+ [--name root]
86
+
87
+ edgca issue-intermediate-ca --ca-cert <pem> --ca-key <pem>
88
+ --subject <dn>
89
+ [--days 1825] [--curve P-256|P-384|P-521]
90
+ [--name intermediate]
91
+
92
+ edgca issue-client --ca-cert <pem> --ca-key <pem> [--ca-chain <pem>]
93
+ --subject <dn>
94
+ [--dns-name <name>]... [--ip <addr>]...
95
+ [--days 365] [--name client]
96
+
97
+ edgca pem-to-pfx --cert <pem> --key <pem> --password <pw>
98
+ [--chain <pem>] [--out <pfx>]
99
+ ```
100
+
101
+ `--subject` accepts an OpenSSL-style DN string (`"CN=foo,O=bar,C=JP"`); short names are case-insensitive (`CN`/`O`/`OU`/`C`/`ST`/`L`/`E`/`DC`/`SERIALNUMBER`/`STREET`/`POSTALCODE`/`TITLE`/`GIVENNAME`/`SURNAME`/`UID`), and dotted OID attributes (`1.2.840.113549.1.9.1=...`) are accepted as-is. Private keys are read/written as PKCS#8 PEM (`-----BEGIN PRIVATE KEY-----`); SEC1 (`EC PRIVATE KEY`) is not supported.
102
+
103
+ > ⚠ The CLI is a convenience wrapper for local testing and one-shot operational tasks. For programmatic use in a server, Worker, or browser, call the library API directly — that way private keys stay in `CryptoKey` form and never touch disk. See [Key Handling](#key-handling) for the rationale.
104
+
42
105
  ## Quick Start
43
106
 
44
107
  ```ts
@@ -119,6 +182,46 @@ The implementation is environment-agnostic (WebCrypto only, no Node-specific API
119
182
 
120
183
  The `@noz-ele/edgca/pkcs12` subpath is provided so consumers that only need PFX assembly can import it without pulling in the CA / CSR / verify modules.
121
184
 
185
+ ## Issue a document-signing certificate
186
+
187
+ `issueDocumentSigningCert` issues a leaf intended for signing arbitrary documents (CAdES detached, CMS, ASiC-E containers, etc., which are produced by separate tooling — not by EdgCA). It is **not** an mTLS client certificate: the EKU is `id-kp-documentSigning` (RFC 9336), and `keyUsage` carries `digitalSignature, contentCommitment` instead of just `digitalSignature`. SAN is intentionally not accepted; the signer is identified by Subject DN.
188
+
189
+ ```ts
190
+ import {
191
+ createRootCA,
192
+ issueIntermediateCA,
193
+ issueDocumentSigningCert
194
+ } from "@noz-ele/edgca";
195
+
196
+ const root = await createRootCA({
197
+ subject: [{ type: "CN", value: "dev-root" }],
198
+ days: 3650
199
+ });
200
+
201
+ const intermediate = await issueIntermediateCA({
202
+ ca: root,
203
+ subject: [{ type: "CN", value: "dev-intermediate" }],
204
+ days: 365
205
+ });
206
+
207
+ const signer = await issueDocumentSigningCert({
208
+ ca: intermediate,
209
+ subject: [
210
+ { type: "CN", value: "Alice (Document Signer)" },
211
+ { type: "O", value: "Example" }
212
+ ],
213
+ days: 365
214
+ });
215
+
216
+ // signer.certPem — the signing certificate
217
+ // signer.certChainPem — full chain to embed alongside the signature (signer + intermediate + root)
218
+ // signer.privateKey — secret CryptoKey, used by the document-signing tool to produce the signature
219
+ ```
220
+
221
+ The returned shape is `IssuedDocumentSigningCertificate` (structurally identical to `IssuedClientCertificate`; the distinct name flags the EKU profile). The same `exportPkcs12` flow used for mTLS leaves works here too if you need to bundle the signer cert + key into a PFX for tools that consume PKCS#12.
222
+
223
+ There is no `issueDocumentSigningCertForPublicKey` (CSR variant) in v1, and EdgCA does not produce CAdES / CMS / ASiC containers itself — see [docs/en/NON_GOALS.md](https://github.com/noz-ele/EdgCA/blob/main/docs/en/NON_GOALS.md) for the rationale.
224
+
122
225
  ## Verify (Cloudflare Worker)
123
226
 
124
227
  > ⚠ **What this is — and is not**
@@ -283,6 +386,7 @@ In scope:
283
386
  - Root CA creation.
284
387
  - Intermediate CA issuance.
285
388
  - mTLS client certificate issuance (with internal key generation, or from a caller-provided public key).
389
+ - Document-signing certificate issuance with EKU `id-kp-documentSigning` (RFC 9336) and `keyUsage digitalSignature, contentCommitment` — internal key generation only, no SAN.
286
390
  - CSR (PKCS#10) parsing and proof-of-possession signature verification.
287
391
  - Identity check that a cert was issued by your own CA (`verifyClientCertificateIssuedBy`, with optional time-validity check).
288
392
  - PEM/DER helpers (certificates only — keys are exchanged as `CryptoKey`).
@@ -291,7 +395,10 @@ In scope:
291
395
 
292
396
  Intentionally out of scope:
293
397
 
294
- - Server certificate issuance.
398
+ - Server certificate issuance. Leaf scope is mTLS client certs and document-signing certs only.
399
+ - Document-signing certificate issuance from a caller-provided public key (`issueDocumentSigningCertForPublicKey`) — not in v1.
400
+ - SAN (`dnsNames` / `ipAddresses` / `emailAddresses`) on document-signing leaves.
401
+ - CAdES / CMS / PAdES / XAdES / ASiC document signing or container building. EdgCA only issues the signing certificate; producing a signed document or container is a separate concern.
295
402
  - Public chain-validation APIs.
296
403
  - 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`.
297
404
  - CRL, OCSP, revocation databases, revocation checks.
package/dist/ca.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey } from "./types.js";
1
+ import type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueDocumentSigningCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey, IssuedDocumentSigningCertificate } 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
5
  export declare function issueClientCertForPublicKey(options: IssueClientCertForPublicKeyOptions): Promise<IssuedClientCertificateForPublicKey>;
6
+ export declare function issueDocumentSigningCert(options: IssueDocumentSigningCertOptions): Promise<IssuedDocumentSigningCertificate>;
6
7
  export declare function importCertificateAuthority(options: ImportCertificateAuthorityOptions): Promise<CertificateAuthority>;
7
8
  //# 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":"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"}
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,+BAA+B,EAC/B,0BAA0B,EAC1B,uBAAuB,EACvB,mCAAmC,EACnC,gCAAgC,EAEjC,MAAM,YAAY,CAAC;AAcpB,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,CAoBvG;AAED,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,kCAAkC,GAC1C,OAAO,CAAC,mCAAmC,CAAC,CAiB9C;AAuED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,gCAAgC,CAAC,CAoB3C;AAED,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAe1H"}
package/dist/ca.js CHANGED
@@ -3,7 +3,7 @@ import { assertKeyPairMatches, curveOf, exportSpki, generateKeyPair, keyIdentifi
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
- import { authorityKeyIdentifierExtension, basicConstraintsCaExtension, basicConstraintsLeafExtension, buildCertificate, buildTbsCertificate, extendedKeyUsageClientAuthExtension, keyUsageExtension, subjectAltNameExtension, subjectKeyIdentifierExtension } from "./x509.js";
6
+ import { authorityKeyIdentifierExtension, basicConstraintsCaExtension, basicConstraintsLeafExtension, buildCertificate, buildTbsCertificate, extendedKeyUsageClientAuthExtension, extendedKeyUsageDocumentSigningExtension, keyUsageExtension, subjectAltNameExtension, subjectKeyIdentifierExtension } from "./x509.js";
7
7
  export async function createRootCA(options) {
8
8
  const keyPair = await resolveKeyPair(options.keyPair);
9
9
  const issuerCurve = curveOf(keyPair.privateKey);
@@ -65,14 +65,12 @@ export async function issueIntermediateCA(options) {
65
65
  }
66
66
  export async function issueClientCert(options) {
67
67
  const keyPair = await generateKeyPair();
68
- const built = await buildClientCertificate(options.ca, keyPair.publicKey, {
68
+ const built = await buildLeafCertificate(options.ca, keyPair.publicKey, {
69
69
  subject: options.subject,
70
70
  days: options.days,
71
71
  notBefore: options.notBefore,
72
- serialNumber: options.serialNumber,
73
- dnsNames: options.dnsNames,
74
- ipAddresses: options.ipAddresses
75
- });
72
+ serialNumber: options.serialNumber
73
+ }, clientCertProfileExtensions(options.dnsNames, options.ipAddresses));
76
74
  return {
77
75
  certPem: built.certPem,
78
76
  certDer: built.certDer,
@@ -82,42 +80,36 @@ export async function issueClientCert(options) {
82
80
  };
83
81
  }
84
82
  export async function issueClientCertForPublicKey(options) {
85
- const built = await buildClientCertificate(options.ca, options.publicKey, {
83
+ const built = await buildLeafCertificate(options.ca, options.publicKey, {
86
84
  subject: options.subject,
87
85
  days: options.days,
88
86
  notBefore: options.notBefore,
89
- serialNumber: options.serialNumber,
90
- dnsNames: options.dnsNames,
91
- ipAddresses: options.ipAddresses
92
- });
87
+ serialNumber: options.serialNumber
88
+ }, clientCertProfileExtensions(options.dnsNames, options.ipAddresses));
93
89
  return {
94
90
  certPem: built.certPem,
95
91
  certDer: built.certDer,
96
92
  certChainPem: built.certChainPem
97
93
  };
98
94
  }
99
- async function buildClientCertificate(ca, subjectPublicKey, content) {
95
+ async function buildLeafCertificate(ca, subjectPublicKey, identity, profileExtensions) {
100
96
  const issuer = await parseIssuer(ca);
101
97
  const issuerChainPem = ca.issuerChainPem;
102
98
  assertCanIssueCertificate(issuer);
103
99
  const issuerCurve = curveOf(ca.privateKey);
104
- const subjectNameDer = encodeName(content.subject);
100
+ const subjectNameDer = encodeName(identity.subject);
105
101
  const spki = await exportSpki(subjectPublicKey);
106
102
  const subjectKeyIdentifier = await keyIdentifierFromSpki(spki);
107
103
  const authorityKeyIdentifier = issuer.subjectKeyIdentifier ?? await keyIdentifierFromSpki(issuer.subjectPublicKeyInfoDer);
108
- const san = subjectAltNameExtension(content.dnsNames, content.ipAddresses);
109
104
  const extensions = [
110
- basicConstraintsLeafExtension(),
111
- keyUsageExtension(["digitalSignature"]),
112
- extendedKeyUsageClientAuthExtension(),
105
+ ...profileExtensions,
113
106
  subjectKeyIdentifierExtension(subjectKeyIdentifier),
114
- authorityKeyIdentifierExtension(authorityKeyIdentifier),
115
- ...(san ? [san] : [])
107
+ authorityKeyIdentifierExtension(authorityKeyIdentifier)
116
108
  ];
117
109
  const { tbsCertificateDer } = buildTbsCertificate({
118
- serialNumber: content.serialNumber,
119
- notBefore: content.notBefore,
120
- days: content.days,
110
+ serialNumber: identity.serialNumber,
111
+ notBefore: identity.notBefore,
112
+ days: identity.days,
121
113
  issuerNameDer: issuer.subjectNameDer,
122
114
  subjectNameDer,
123
115
  subjectPublicKeyInfoDer: spki,
@@ -133,6 +125,38 @@ async function buildClientCertificate(ca, subjectPublicKey, content) {
133
125
  certChainPem: joinPemChain([certPem, ca.certPem, issuerChainPem])
134
126
  };
135
127
  }
128
+ function clientCertProfileExtensions(dnsNames, ipAddresses) {
129
+ const san = subjectAltNameExtension(dnsNames, ipAddresses);
130
+ return [
131
+ basicConstraintsLeafExtension(),
132
+ keyUsageExtension(["digitalSignature"]),
133
+ extendedKeyUsageClientAuthExtension(),
134
+ ...(san ? [san] : [])
135
+ ];
136
+ }
137
+ function documentSigningProfileExtensions() {
138
+ return [
139
+ basicConstraintsLeafExtension(),
140
+ keyUsageExtension(["digitalSignature", "contentCommitment"]),
141
+ extendedKeyUsageDocumentSigningExtension()
142
+ ];
143
+ }
144
+ export async function issueDocumentSigningCert(options) {
145
+ const keyPair = await generateKeyPair();
146
+ const built = await buildLeafCertificate(options.ca, keyPair.publicKey, {
147
+ subject: options.subject,
148
+ days: options.days,
149
+ notBefore: options.notBefore,
150
+ serialNumber: options.serialNumber
151
+ }, documentSigningProfileExtensions());
152
+ return {
153
+ certPem: built.certPem,
154
+ certDer: built.certDer,
155
+ privateKey: keyPair.privateKey,
156
+ publicKey: keyPair.publicKey,
157
+ certChainPem: built.certChainPem
158
+ };
159
+ }
136
160
  export async function importCertificateAuthority(options) {
137
161
  const issuerChainPem = options.issuerChainPem ?? "";
138
162
  assertIssuerChainPem(issuerChainPem);
@@ -0,0 +1,2 @@
1
+ export declare function createRootCaCommand(args: string[]): Promise<void>;
2
+ //# sourceMappingURL=create-root-ca.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-root-ca.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/create-root-ca.ts"],"names":[],"mappings":"AAMA,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBvE"}
@@ -0,0 +1,26 @@
1
+ import { parseArgs } from "node:util";
2
+ import { createRootCA, generateKeyPair } from "../../index.js";
3
+ import { parseDnString } from "../dn.js";
4
+ import { parseCurveFlag, parseDaysFlag, requireString } from "../flags.js";
5
+ import { writeCaTriplet } from "../io.js";
6
+ export async function createRootCaCommand(args) {
7
+ const { values } = parseArgs({
8
+ args,
9
+ options: {
10
+ subject: { type: "string" },
11
+ days: { type: "string", default: "3650" },
12
+ curve: { type: "string", default: "P-256" },
13
+ name: { type: "string", default: "root" }
14
+ },
15
+ strict: true
16
+ });
17
+ const subject = parseDnString(requireString(values.subject, "--subject"));
18
+ const days = parseDaysFlag(values.days);
19
+ const curve = parseCurveFlag(values.curve);
20
+ const basename = values.name;
21
+ const keyPair = await generateKeyPair(curve);
22
+ const ca = await createRootCA({ subject, days, keyPair });
23
+ const written = await writeCaTriplet(ca, ".", basename);
24
+ console.log(`wrote ${written.certPath}`);
25
+ console.log(`wrote ${written.keyPath}`);
26
+ }
@@ -0,0 +1,2 @@
1
+ export declare function issueClientCommand(args: string[]): Promise<void>;
2
+ //# sourceMappingURL=issue-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-client.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/issue-client.ts"],"names":[],"mappings":"AAMA,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2CtE"}
@@ -0,0 +1,46 @@
1
+ import { parseArgs } from "node:util";
2
+ import { issueClientCert } from "../../index.js";
3
+ import { parseDnString } from "../dn.js";
4
+ import { parseDaysFlag, requireString } from "../flags.js";
5
+ import { loadCaFromDisk, writeIssuedLeafTriplet } from "../io.js";
6
+ export async function issueClientCommand(args) {
7
+ const { values } = parseArgs({
8
+ args,
9
+ options: {
10
+ "ca-cert": { type: "string" },
11
+ "ca-key": { type: "string" },
12
+ "ca-chain": { type: "string" },
13
+ subject: { type: "string" },
14
+ "dns-name": { type: "string", multiple: true, default: [] },
15
+ ip: { type: "string", multiple: true, default: [] },
16
+ days: { type: "string", default: "365" },
17
+ name: { type: "string", default: "client" }
18
+ },
19
+ strict: true
20
+ });
21
+ const caCertPath = requireString(values["ca-cert"], "--ca-cert");
22
+ const caKeyPath = requireString(values["ca-key"], "--ca-key");
23
+ const caChainPath = values["ca-chain"];
24
+ const subject = parseDnString(requireString(values.subject, "--subject"));
25
+ const days = parseDaysFlag(values.days);
26
+ const basename = values.name;
27
+ const dnsNames = values["dns-name"];
28
+ const ipAddresses = values.ip;
29
+ const ca = await loadCaFromDisk({
30
+ certPath: caCertPath,
31
+ keyPath: caKeyPath,
32
+ ...(caChainPath !== undefined ? { chainPath: caChainPath } : {})
33
+ });
34
+ const issued = await issueClientCert({
35
+ ca,
36
+ subject,
37
+ days,
38
+ ...(dnsNames.length > 0 ? { dnsNames } : {}),
39
+ ...(ipAddresses.length > 0 ? { ipAddresses } : {})
40
+ });
41
+ const written = await writeIssuedLeafTriplet(issued, ".", basename);
42
+ console.log(`wrote ${written.certPath}`);
43
+ console.log(`wrote ${written.keyPath}`);
44
+ if (written.chainPath)
45
+ console.log(`wrote ${written.chainPath}`);
46
+ }
@@ -0,0 +1,2 @@
1
+ export declare function issueIntermediateCaCommand(args: string[]): Promise<void>;
2
+ //# sourceMappingURL=issue-intermediate-ca.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-intermediate-ca.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/issue-intermediate-ca.ts"],"names":[],"mappings":"AAMA,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B9E"}
@@ -0,0 +1,33 @@
1
+ import { parseArgs } from "node:util";
2
+ import { generateKeyPair, issueIntermediateCA } from "../../index.js";
3
+ import { parseDnString } from "../dn.js";
4
+ import { parseCurveFlag, parseDaysFlag, requireString } from "../flags.js";
5
+ import { loadCaFromDisk, writeCaTriplet } from "../io.js";
6
+ export async function issueIntermediateCaCommand(args) {
7
+ const { values } = parseArgs({
8
+ args,
9
+ options: {
10
+ "ca-cert": { type: "string" },
11
+ "ca-key": { type: "string" },
12
+ subject: { type: "string" },
13
+ days: { type: "string", default: "1825" },
14
+ curve: { type: "string", default: "P-256" },
15
+ name: { type: "string", default: "intermediate" }
16
+ },
17
+ strict: true
18
+ });
19
+ const caCertPath = requireString(values["ca-cert"], "--ca-cert");
20
+ const caKeyPath = requireString(values["ca-key"], "--ca-key");
21
+ const subject = parseDnString(requireString(values.subject, "--subject"));
22
+ const days = parseDaysFlag(values.days);
23
+ const curve = parseCurveFlag(values.curve);
24
+ const basename = values.name;
25
+ const root = await loadCaFromDisk({ certPath: caCertPath, keyPath: caKeyPath });
26
+ const keyPair = await generateKeyPair(curve);
27
+ const intermediate = await issueIntermediateCA({ ca: root, subject, days, keyPair });
28
+ const written = await writeCaTriplet(intermediate, ".", basename);
29
+ console.log(`wrote ${written.certPath}`);
30
+ console.log(`wrote ${written.keyPath}`);
31
+ if (written.chainPath)
32
+ console.log(`wrote ${written.chainPath}`);
33
+ }
@@ -0,0 +1,2 @@
1
+ export declare function pemToPfxCommand(args: string[]): Promise<void>;
2
+ //# sourceMappingURL=pem-to-pfx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pem-to-pfx.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pem-to-pfx.ts"],"names":[],"mappings":"AAYA,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CnE"}
@@ -0,0 +1,47 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { parseArgs } from "node:util";
3
+ import { exportPkcs12, importCertificateAuthority, pemToDer, pemToDerWithLabel, splitPemBlocks } from "../../index.js";
4
+ import { requireString } from "../flags.js";
5
+ import { defaultPfxPath, importPkcs8PrivateKeyFromPem } from "../io.js";
6
+ export async function pemToPfxCommand(args) {
7
+ const { values } = parseArgs({
8
+ args,
9
+ options: {
10
+ cert: { type: "string" },
11
+ key: { type: "string" },
12
+ chain: { type: "string" },
13
+ out: { type: "string" },
14
+ password: { type: "string" }
15
+ },
16
+ strict: true
17
+ });
18
+ const certPath = requireString(values.cert, "--cert");
19
+ const keyPath = requireString(values.key, "--key");
20
+ const password = requireString(values.password, "--password");
21
+ const chainPath = values.chain;
22
+ const outPath = values.out ?? defaultPfxPath(certPath);
23
+ const [certPem, keyPem, chainPem] = await Promise.all([
24
+ readFile(certPath, "utf8"),
25
+ readFile(keyPath, "utf8"),
26
+ chainPath ? readFile(chainPath, "utf8") : Promise.resolve("")
27
+ ]);
28
+ const certDer = pemToDer(certPem);
29
+ const chainDer = chainPem
30
+ ? splitPemBlocks(chainPem).map((block) => pemToDerWithLabel(block, "CERTIFICATE"))
31
+ : [];
32
+ const privateKey = await importPkcs8PrivateKeyFromPem(keyPem);
33
+ // Refuse to emit a PFX whose key does not match the leaf cert. The library's
34
+ // exportPkcs12 does not check this, so without the guard a mismatched pair
35
+ // silently produces a structurally valid but unusable .pfx file. We discard
36
+ // the returned CertificateAuthority — only the sign/verify round-trip
37
+ // performed inside importCertificateAuthority matters here.
38
+ await importCertificateAuthority({ certPem, privateKey });
39
+ const pfx = await exportPkcs12({
40
+ certDer,
41
+ ...(chainDer.length > 0 ? { chainDer } : {}),
42
+ privateKey,
43
+ password: new TextEncoder().encode(password)
44
+ });
45
+ await writeFile(outPath, pfx);
46
+ console.log(`wrote ${outPath}`);
47
+ }
@@ -0,0 +1,3 @@
1
+ import type { Subject } from "../types.js";
2
+ export declare function parseDnString(input: string): Subject;
3
+ //# sourceMappingURL=dn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dn.d.ts","sourceRoot":"","sources":["../../src/cli/dn.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,OAAO,EAGR,MAAM,aAAa,CAAC;AAQrB,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBpD"}
package/dist/cli/dn.js ADDED
@@ -0,0 +1,65 @@
1
+ import { SUBJECT_ATTRIBUTE_OIDS } from "../oids.js";
2
+ const SHORT_NAMES = new Set(Object.keys(SUBJECT_ATTRIBUTE_OIDS));
3
+ const DOTTED_OID = /^\d+(?:\.\d+)+$/;
4
+ export function parseDnString(input) {
5
+ const parts = [];
6
+ for (const raw of splitTopLevel(input, ",")) {
7
+ const part = raw.trim();
8
+ if (part === "")
9
+ continue;
10
+ const eq = part.indexOf("=");
11
+ if (eq < 0) {
12
+ throw new Error(`Invalid DN component (missing '='): ${part}`);
13
+ }
14
+ const rawKey = part.slice(0, eq).trim();
15
+ const value = unescape(part.slice(eq + 1).trim());
16
+ if (rawKey === "")
17
+ throw new Error(`Invalid DN component (empty key): ${part}`);
18
+ if (value === "")
19
+ throw new Error(`Invalid DN component (empty value): ${part}`);
20
+ const type = resolveAttributeType(rawKey);
21
+ parts.push({ type, value });
22
+ }
23
+ if (parts.length === 0) {
24
+ throw new Error("Subject is empty");
25
+ }
26
+ return parts;
27
+ }
28
+ function resolveAttributeType(raw) {
29
+ const upper = raw.toUpperCase();
30
+ if (SHORT_NAMES.has(upper)) {
31
+ return upper;
32
+ }
33
+ if (DOTTED_OID.test(raw)) {
34
+ return raw;
35
+ }
36
+ throw new Error(`Unsupported DN attribute: ${raw}`);
37
+ }
38
+ function splitTopLevel(input, sep) {
39
+ const out = [];
40
+ let current = "";
41
+ let escaped = false;
42
+ for (const ch of input) {
43
+ if (escaped) {
44
+ current += ch;
45
+ escaped = false;
46
+ continue;
47
+ }
48
+ if (ch === "\\") {
49
+ current += ch;
50
+ escaped = true;
51
+ continue;
52
+ }
53
+ if (ch === sep) {
54
+ out.push(current);
55
+ current = "";
56
+ continue;
57
+ }
58
+ current += ch;
59
+ }
60
+ out.push(current);
61
+ return out;
62
+ }
63
+ function unescape(value) {
64
+ return value.replace(/\\(.)/g, "$1");
65
+ }
@@ -0,0 +1,8 @@
1
+ import type { SupportedCurve } from "../index.js";
2
+ export declare class UsageError extends Error {
3
+ name: string;
4
+ }
5
+ export declare function parseDaysFlag(value: string): number;
6
+ export declare function parseCurveFlag(value: string): SupportedCurve;
7
+ export declare function requireString(value: string | undefined, flagName: string): string;
8
+ //# sourceMappingURL=flags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../src/cli/flags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,qBAAa,UAAW,SAAQ,KAAK;IAC1B,IAAI,SAAgB;CAC9B;AASD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASnD;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAK5D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKjF"}
@@ -0,0 +1,32 @@
1
+ export class UsageError extends Error {
2
+ name = "UsageError";
3
+ }
4
+ // Accept only positive decimal integers with no leading zero, surrounding
5
+ // whitespace, sign, scientific notation, or fractional part. Number() is too
6
+ // permissive (it accepts " 365 ", "1e3", "0x10", "01", "1.0", and silently
7
+ // rounds beyond MAX_SAFE_INTEGER), which would let surprising values slip
8
+ // past CLI validation into the issuance API.
9
+ const POSITIVE_DECIMAL_INTEGER = /^[1-9]\d*$/;
10
+ export function parseDaysFlag(value) {
11
+ if (!POSITIVE_DECIMAL_INTEGER.test(value)) {
12
+ throw new UsageError(`--days must be a positive decimal integer, got: ${JSON.stringify(value)}`);
13
+ }
14
+ const n = Number(value);
15
+ if (!Number.isSafeInteger(n)) {
16
+ throw new UsageError(`--days exceeds the safe integer range: ${value}`);
17
+ }
18
+ return n;
19
+ }
20
+ const SUPPORTED_CURVES = ["P-256", "P-384", "P-521"];
21
+ export function parseCurveFlag(value) {
22
+ if (SUPPORTED_CURVES.includes(value)) {
23
+ return value;
24
+ }
25
+ throw new UsageError(`--curve must be one of P-256, P-384, P-521 (got: ${value})`);
26
+ }
27
+ export function requireString(value, flagName) {
28
+ if (value === undefined || value === "") {
29
+ throw new UsageError(`${flagName} is required`);
30
+ }
31
+ return value;
32
+ }
@@ -0,0 +1,19 @@
1
+ import { type CertificateAuthority, type IssuedClientCertificate } from "../index.js";
2
+ export declare function importPkcs8PrivateKeyFromPem(pem: string): Promise<CryptoKey>;
3
+ export declare function cryptoKeyToPkcs8Pem(key: CryptoKey): Promise<string>;
4
+ export interface WriteCaResult {
5
+ certPath: string;
6
+ keyPath: string;
7
+ chainPath?: string;
8
+ }
9
+ export declare function writeCaTriplet(ca: CertificateAuthority, outDir: string, basename: string): Promise<WriteCaResult>;
10
+ export declare function writeIssuedLeafTriplet(issued: IssuedClientCertificate, outDir: string, basename: string): Promise<WriteCaResult>;
11
+ export declare function stripLeafFromChain(leafPem: string, chainPem: string): string;
12
+ export interface LoadCaInput {
13
+ certPath: string;
14
+ keyPath: string;
15
+ chainPath?: string;
16
+ }
17
+ export declare function loadCaFromDisk(input: LoadCaInput): Promise<CertificateAuthority>;
18
+ export declare function defaultPfxPath(certPath: string): string;
19
+ //# sourceMappingURL=io.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/cli/io.ts"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC7B,MAAM,aAAa,CAAC;AAKrB,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAmBlF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAGzE;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAoBxB;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,uBAAuB,EAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAoBxB;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ5E;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAWtF;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOvD"}
package/dist/cli/io.js ADDED
@@ -0,0 +1,92 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { arrayBufferFromBytes, bytesEqual, encodePem, importCertificateAuthority, pemToDer, pemToDerWithLabel, splitPemBlocks } from "../index.js";
4
+ const EC_CURVES = ["P-256", "P-384", "P-521"];
5
+ export async function importPkcs8PrivateKeyFromPem(pem) {
6
+ const der = pemToDerWithLabel(pem, "PRIVATE KEY");
7
+ let lastError;
8
+ for (const namedCurve of EC_CURVES) {
9
+ try {
10
+ return await crypto.subtle.importKey("pkcs8", arrayBufferFromBytes(der), { name: "ECDSA", namedCurve }, true, ["sign"]);
11
+ }
12
+ catch (err) {
13
+ lastError = err;
14
+ }
15
+ }
16
+ throw new Error(`Unable to import PRIVATE KEY as ECDSA P-256/P-384/P-521 PKCS#8: ${describe(lastError)}`);
17
+ }
18
+ export async function cryptoKeyToPkcs8Pem(key) {
19
+ const der = new Uint8Array(await crypto.subtle.exportKey("pkcs8", key));
20
+ return encodePem("PRIVATE KEY", der);
21
+ }
22
+ export async function writeCaTriplet(ca, outDir, basename) {
23
+ const certPath = path.join(outDir, `${basename}.crt.pem`);
24
+ const keyPath = path.join(outDir, `${basename}.key.pem`);
25
+ const keyPem = await cryptoKeyToPkcs8Pem(ca.privateKey);
26
+ const writes = [
27
+ writeFile(certPath, ca.certPem, "utf8"),
28
+ writeFile(keyPath, keyPem, "utf8")
29
+ ];
30
+ const hasChain = ca.issuerChainPem.trim().length > 0;
31
+ const chainPath = hasChain ? path.join(outDir, `${basename}.chain.pem`) : undefined;
32
+ if (hasChain && chainPath) {
33
+ writes.push(writeFile(chainPath, ca.issuerChainPem, "utf8"));
34
+ }
35
+ await Promise.all(writes);
36
+ return chainPath !== undefined
37
+ ? { certPath, keyPath, chainPath }
38
+ : { certPath, keyPath };
39
+ }
40
+ export async function writeIssuedLeafTriplet(issued, outDir, basename) {
41
+ const certPath = path.join(outDir, `${basename}.crt.pem`);
42
+ const keyPath = path.join(outDir, `${basename}.key.pem`);
43
+ const chainPath = path.join(outDir, `${basename}.chain.pem`);
44
+ const keyPem = await cryptoKeyToPkcs8Pem(issued.privateKey);
45
+ // The library's `certChainPem` is a fullchain (leaf + issuers) suitable for
46
+ // nginx-style server config. The CLI convention for `*.chain.pem` is the
47
+ // issuer-only chain (no leaf), matching what `pem-to-pfx --chain` expects.
48
+ // Strip the leaf out by DER match so the file faithfully represents an
49
+ // issuer-only chain regardless of where the leaf sits in certChainPem.
50
+ const issuerOnlyChainPem = stripLeafFromChain(issued.certPem, issued.certChainPem);
51
+ await Promise.all([
52
+ writeFile(certPath, issued.certPem, "utf8"),
53
+ writeFile(keyPath, keyPem, "utf8"),
54
+ writeFile(chainPath, issuerOnlyChainPem, "utf8")
55
+ ]);
56
+ return { certPath, keyPath, chainPath };
57
+ }
58
+ export function stripLeafFromChain(leafPem, chainPem) {
59
+ const leafDer = pemToDer(leafPem);
60
+ const issuerBlocks = splitPemBlocks(chainPem).filter((block) => {
61
+ const blockDer = pemToDerWithLabel(block, "CERTIFICATE");
62
+ return !bytesEqual(blockDer, leafDer);
63
+ });
64
+ if (issuerBlocks.length === 0)
65
+ return "";
66
+ return issuerBlocks.join("\n") + "\n";
67
+ }
68
+ export async function loadCaFromDisk(input) {
69
+ const [certPem, keyPem, chainPem] = await Promise.all([
70
+ readFile(input.certPath, "utf8"),
71
+ readFile(input.keyPath, "utf8"),
72
+ input.chainPath ? readFile(input.chainPath, "utf8") : Promise.resolve("")
73
+ ]);
74
+ return importCertificateAuthority({
75
+ certPem,
76
+ privateKey: await importPkcs8PrivateKeyFromPem(keyPem),
77
+ issuerChainPem: chainPem
78
+ });
79
+ }
80
+ export function defaultPfxPath(certPath) {
81
+ const dir = path.dirname(certPath);
82
+ const base = path
83
+ .basename(certPath)
84
+ .replace(/\.crt\.pem$/i, "")
85
+ .replace(/\.pem$/i, "");
86
+ return path.join(dir, `${base}.pfx`);
87
+ }
88
+ function describe(err) {
89
+ if (err instanceof Error)
90
+ return err.message;
91
+ return String(err);
92
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { createRootCaCommand } from "./cli/commands/create-root-ca.js";
3
+ import { issueClientCommand } from "./cli/commands/issue-client.js";
4
+ import { issueIntermediateCaCommand } from "./cli/commands/issue-intermediate-ca.js";
5
+ import { pemToPfxCommand } from "./cli/commands/pem-to-pfx.js";
6
+ import { UsageError } from "./cli/flags.js";
7
+ const USAGE = `Usage:
8
+ edgca create-root-ca --subject <dn> [--days 3650] [--curve P-256] [--name root]
9
+
10
+ edgca issue-intermediate-ca --ca-cert <pem> --ca-key <pem>
11
+ --subject <dn>
12
+ [--days 1825] [--curve P-256] [--name intermediate]
13
+
14
+ edgca issue-client --ca-cert <pem> --ca-key <pem> [--ca-chain <pem>]
15
+ --subject <dn>
16
+ [--dns-name <name>]... [--ip <addr>]...
17
+ [--days 365] [--name client]
18
+
19
+ edgca pem-to-pfx --cert <pem> --key <pem> --password <pw>
20
+ [--chain <pem>] [--out <pfx>]
21
+ `;
22
+ async function main() {
23
+ const [sub, ...rest] = process.argv.slice(2);
24
+ switch (sub) {
25
+ case "create-root-ca":
26
+ await createRootCaCommand(rest);
27
+ return;
28
+ case "issue-intermediate-ca":
29
+ await issueIntermediateCaCommand(rest);
30
+ return;
31
+ case "issue-client":
32
+ await issueClientCommand(rest);
33
+ return;
34
+ case "pem-to-pfx":
35
+ await pemToPfxCommand(rest);
36
+ return;
37
+ case undefined:
38
+ case "-h":
39
+ case "--help":
40
+ process.stdout.write(USAGE);
41
+ return;
42
+ default:
43
+ process.stderr.write(`unknown subcommand: ${sub}\n\n${USAGE}`);
44
+ process.exitCode = 2;
45
+ return;
46
+ }
47
+ }
48
+ main().catch((err) => {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ process.stderr.write(`${message}\n`);
51
+ process.exitCode = err instanceof UsageError ? 2 : 1;
52
+ });
package/dist/csr.d.ts CHANGED
@@ -22,4 +22,16 @@ export interface ParsedCertificateSigningRequest {
22
22
  }
23
23
  export declare function parseCertificateSigningRequest(input: string | Uint8Array): Promise<ParsedCertificateSigningRequest>;
24
24
  export declare function verifyCertificateSigningRequestSignature(csr: ParsedCertificateSigningRequest): Promise<boolean>;
25
+ export interface CreateCertificateSigningRequestInput {
26
+ subject: Subject;
27
+ keyPair: CryptoKeyPair;
28
+ dnsNames?: readonly string[];
29
+ ipAddresses?: readonly string[];
30
+ extensions?: readonly CertificateSigningRequestExtension[];
31
+ }
32
+ export interface CreatedCertificateSigningRequest {
33
+ der: Uint8Array;
34
+ pem: string;
35
+ }
36
+ export declare function createCertificateSigningRequest(input: CreateCertificateSigningRequestInput): Promise<CreatedCertificateSigningRequest>;
25
37
  //# sourceMappingURL=csr.d.ts.map
package/dist/csr.d.ts.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"csr.d.ts","sourceRoot":"","sources":["../src/csr.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,OAAO,EAAwB,MAAM,YAAY,CAAC;AAmBhE,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;AAED,MAAM,WAAW,oCAAoC;IACnD,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,SAAS,kCAAkC,EAAE,CAAC;CAC5D;AAED,MAAM,WAAW,gCAAgC;IAC/C,GAAG,EAAE,UAAU,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,oCAAoC,GAC1C,OAAO,CAAC,gCAAgC,CAAC,CAiD3C"}
package/dist/csr.js CHANGED
@@ -1,8 +1,10 @@
1
- import { cloneBytes } from "./bytes.js";
2
- import { importPublicKeySpki, verifyDer } from "./crypto.js";
3
- import { decodeInteger, decodeOid, readChildren, readElement, readSequenceChildren, TAG } from "./der.js";
1
+ import { cloneBytes, concatBytes } from "./bytes.js";
2
+ import { curveOf, exportSpki, importPublicKeySpki, signDer, signatureAlgorithmOidForCurve, verifyDer } from "./crypto.js";
3
+ import { bitString, decodeInteger, decodeOid, der, integer, oid, readChildren, readElement, readSequenceChildren, sequence, set, TAG } from "./der.js";
4
+ import { encodeName } from "./name.js";
4
5
  import { OID, SUBJECT_ATTRIBUTE_OIDS } from "./oids.js";
5
- import { pemToDerWithLabel, splitPemBlocks } from "./pem.js";
6
+ import { csrToPem, pemToDerWithLabel, splitPemBlocks } from "./pem.js";
7
+ import { extension, subjectAltNameExtension } from "./x509.js";
6
8
  const SHORT_NAME_BY_OID = Object.fromEntries(Object.entries(SUBJECT_ATTRIBUTE_OIDS).map(([shortName, oid]) => [oid, shortName]));
7
9
  const VALUE_DECODERS = new Map([
8
10
  [TAG.UTF8_STRING, (b) => new TextDecoder("utf-8", { fatal: true }).decode(b)],
@@ -15,9 +17,9 @@ const SUPPORTED_SIGNATURE_OIDS = new Set([
15
17
  OID.ecdsaWithSha512
16
18
  ]);
17
19
  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) {
20
+ const derBytes = typeof input === "string" ? csrPemToDer(input) : input;
21
+ const root = readElement(derBytes);
22
+ if (root.tag !== TAG.SEQUENCE || root.end !== derBytes.length) {
21
23
  throw new Error("Invalid CSR DER");
22
24
  }
23
25
  const [requestInfo, signatureAlgorithm, signatureValue] = readSequenceChildren(root);
@@ -62,7 +64,7 @@ export async function parseCertificateSigningRequest(input) {
62
64
  const requestedExtensions = extensionRequest
63
65
  ? decodeRequestedExtensions(extensionRequest.valuesDer)
64
66
  : [];
65
- const sanExtension = requestedExtensions.find((extension) => extension.oid === OID.subjectAltName);
67
+ const sanExtension = requestedExtensions.find((ext) => ext.oid === OID.subjectAltName);
66
68
  const { dnsNames, ipAddresses } = sanExtension
67
69
  ? decodeSubjectAltName(sanExtension.valueDer)
68
70
  : { dnsNames: [], ipAddresses: [] };
@@ -82,6 +84,48 @@ export async function parseCertificateSigningRequest(input) {
82
84
  export async function verifyCertificateSigningRequestSignature(csr) {
83
85
  return verifyDer(csr.publicKey, csr.signatureDer, csr.certificationRequestInfoDer);
84
86
  }
87
+ export async function createCertificateSigningRequest(input) {
88
+ const privateCurve = curveOf(input.keyPair.privateKey);
89
+ const publicCurve = curveOf(input.keyPair.publicKey);
90
+ if (privateCurve !== publicCurve) {
91
+ throw new Error("CSR keyPair private/public curve mismatch");
92
+ }
93
+ const subjectNameDer = encodeName(input.subject);
94
+ const spki = await exportSpki(input.keyPair.publicKey);
95
+ const requestedExtensions = collectRequestedExtensions(input.dnsNames, input.ipAddresses, input.extensions);
96
+ const attributes = [];
97
+ if (requestedExtensions.length > 0) {
98
+ attributes.push(sequence(oid(OID.extensionRequest), set(sequence(...requestedExtensions))));
99
+ }
100
+ // CertificationRequestInfo.attributes is IMPLICIT [0] SET OF Attribute.
101
+ // Wire form: tag 0xa0 wrapping the concatenated Attribute SEQUENCEs.
102
+ const attributesField = der(0xa0, concatBytes(attributes));
103
+ const certificationRequestInfoDer = sequence(integer(0), subjectNameDer, spki, attributesField);
104
+ const signatureDer = await signDer(input.keyPair.privateKey, certificationRequestInfoDer);
105
+ const signatureAlgorithmOid = signatureAlgorithmOidForCurve(privateCurve);
106
+ const csrDer = sequence(certificationRequestInfoDer, sequence(oid(signatureAlgorithmOid)), bitString(signatureDer));
107
+ return {
108
+ der: csrDer,
109
+ pem: csrToPem(csrDer)
110
+ };
111
+ }
112
+ function collectRequestedExtensions(dnsNames, ipAddresses, extraExtensions) {
113
+ const seenOids = new Set();
114
+ const out = [];
115
+ const san = subjectAltNameExtension(dnsNames, ipAddresses);
116
+ if (san) {
117
+ seenOids.add(OID.subjectAltName);
118
+ out.push(san);
119
+ }
120
+ for (const ext of extraExtensions ?? []) {
121
+ if (seenOids.has(ext.oid)) {
122
+ throw new Error(`Duplicate CSR extension OID: ${ext.oid}`);
123
+ }
124
+ seenOids.add(ext.oid);
125
+ out.push(extension(ext.oid, ext.critical, ext.valueDer));
126
+ }
127
+ return out;
128
+ }
85
129
  function csrPemToDer(pem) {
86
130
  // RFC 7468 §7 uses "CERTIFICATE REQUEST"; some legacy tools emit "NEW CERTIFICATE REQUEST".
87
131
  const blocks = splitPemBlocks(pem);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueIntermediateCA } from "./ca.js";
2
- export { parseCertificateSigningRequest, verifyCertificateSigningRequestSignature, type ParsedCertificateSigningRequest, type CertificateSigningRequestExtension, type CertificateSigningRequestAttribute } from "./csr.js";
3
- export { pemToDer, certificateToPem } from "./pem.js";
1
+ export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueDocumentSigningCert, issueIntermediateCA } from "./ca.js";
2
+ export { createCertificateSigningRequest, parseCertificateSigningRequest, verifyCertificateSigningRequestSignature, type CertificateSigningRequestAttribute, type CertificateSigningRequestExtension, type CreateCertificateSigningRequestInput, type CreatedCertificateSigningRequest, type ParsedCertificateSigningRequest } from "./csr.js";
3
+ export { certificateToPem, csrToPem, encodePem, pemToDer, pemToDerWithLabel, splitPemBlocks } from "./pem.js";
4
+ export { generateKeyPair, type SupportedCurve } from "./crypto.js";
5
+ export { arrayBufferFromBytes, bytesEqual } from "./bytes.js";
4
6
  export { exportPkcs12, type ExportPkcs12Input } from "./pkcs12.js";
5
7
  export { verifyClientCertificateIssuedBy, type VerifyClientCertificateIssuedByOptions, type VerifyClientCertificateValidity } from "./verify.js";
6
- export type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey, SerialNumber, ShortSubjectAttributeType, Subject, SubjectAttribute, SubjectAttributeType } from "./types.js";
8
+ export type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueDocumentSigningCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey, IssuedDocumentSigningCertificate, SerialNumber, ShortSubjectAttributeType, Subject, SubjectAttribute, SubjectAttributeType } from "./types.js";
7
9
  //# 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,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,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,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"}
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,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,EAC9B,wCAAwC,EACxC,KAAK,kCAAkC,EACvC,KAAK,kCAAkC,EACvC,KAAK,oCAAoC,EACzC,KAAK,gCAAgC,EACrC,KAAK,+BAA+B,EACrC,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACf,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,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,+BAA+B,EAC/B,0BAA0B,EAC1B,uBAAuB,EACvB,mCAAmC,EACnC,gCAAgC,EAChC,YAAY,EACZ,yBAAyB,EACzB,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueIntermediateCA } from "./ca.js";
2
- export { parseCertificateSigningRequest, verifyCertificateSigningRequestSignature } from "./csr.js";
3
- export { pemToDer, certificateToPem } from "./pem.js";
1
+ export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueDocumentSigningCert, issueIntermediateCA } from "./ca.js";
2
+ export { createCertificateSigningRequest, parseCertificateSigningRequest, verifyCertificateSigningRequestSignature } from "./csr.js";
3
+ export { certificateToPem, csrToPem, encodePem, pemToDer, pemToDerWithLabel, splitPemBlocks } from "./pem.js";
4
+ export { generateKeyPair } from "./crypto.js";
5
+ export { arrayBufferFromBytes, bytesEqual } from "./bytes.js";
4
6
  export { exportPkcs12 } from "./pkcs12.js";
5
7
  export { verifyClientCertificateIssuedBy } from "./verify.js";
package/dist/oids.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare const OID: {
14
14
  readonly subjectKeyIdentifier: "2.5.29.14";
15
15
  readonly authorityKeyIdentifier: "2.5.29.35";
16
16
  readonly clientAuth: "1.3.6.1.5.5.7.3.2";
17
+ readonly documentSigning: "1.3.6.1.5.5.7.3.36";
17
18
  readonly extensionRequest: "1.2.840.113549.1.9.14";
18
19
  readonly data: "1.2.840.113549.1.7.1";
19
20
  readonly encryptedData: "1.2.840.113549.1.7.6";
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BN,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BN,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
@@ -13,6 +13,7 @@ export const OID = {
13
13
  subjectKeyIdentifier: "2.5.29.14",
14
14
  authorityKeyIdentifier: "2.5.29.35",
15
15
  clientAuth: "1.3.6.1.5.5.7.3.2",
16
+ documentSigning: "1.3.6.1.5.5.7.3.36",
16
17
  extensionRequest: "1.2.840.113549.1.9.14",
17
18
  data: "1.2.840.113549.1.7.1",
18
19
  encryptedData: "1.2.840.113549.1.7.6",
package/dist/pem.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export declare function certificateToPem(der: Uint8Array): string;
2
+ export declare function csrToPem(der: Uint8Array): string;
2
3
  export declare function pemToDer(pem: string): Uint8Array;
3
4
  export declare function pemToDerWithLabel(pem: string, label: string): Uint8Array;
4
5
  export declare function splitPemBlocks(pem: string): string[];
6
+ export declare function encodePem(label: string, der: Uint8Array): string;
5
7
  //# sourceMappingURL=pem.d.ts.map
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,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,UAAU,GAAG,MAAM,CAEhD;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;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,CAShE"}
package/dist/pem.js CHANGED
@@ -3,6 +3,9 @@ const PEM_LINE_LENGTH = 64;
3
3
  export function certificateToPem(der) {
4
4
  return encodePem("CERTIFICATE", der);
5
5
  }
6
+ export function csrToPem(der) {
7
+ return encodePem("CERTIFICATE REQUEST", der);
8
+ }
6
9
  export function pemToDer(pem) {
7
10
  const match = /-----BEGIN (.+?)-----([\s\S]*?)-----END \1-----/.exec(pem);
8
11
  if (!match || !match[2]) {
@@ -36,7 +39,7 @@ export function splitPemBlocks(pem) {
36
39
  }
37
40
  return blocks;
38
41
  }
39
- function encodePem(label, der) {
42
+ export function encodePem(label, der) {
40
43
  const base64 = btoa(bytesToBinary(der));
41
44
  const lines = [];
42
45
  for (let i = 0; i < base64.length; i += PEM_LINE_LENGTH) {
package/dist/types.d.ts CHANGED
@@ -62,6 +62,20 @@ export interface IssuedClientCertificateForPublicKey {
62
62
  certDer: Uint8Array;
63
63
  certChainPem: string;
64
64
  }
65
+ export interface IssueDocumentSigningCertOptions {
66
+ ca: CertificateAuthority;
67
+ subject: Subject;
68
+ days: number;
69
+ notBefore?: Date;
70
+ serialNumber?: SerialNumber;
71
+ }
72
+ export interface IssuedDocumentSigningCertificate {
73
+ certPem: string;
74
+ certDer: Uint8Array;
75
+ privateKey: CryptoKey;
76
+ publicKey: CryptoKey;
77
+ certChainPem: string;
78
+ }
65
79
  export interface ImportCertificateAuthorityOptions {
66
80
  certPem: string;
67
81
  privateKey: CryptoKey;
@@ -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,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"}
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,+BAA+B;IAC9C,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;CAC7B;AAED,MAAM,WAAW,gCAAgC;IAC/C,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,iCAAiC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB"}
package/dist/x509.d.ts CHANGED
@@ -23,11 +23,14 @@ export declare function basicConstraintsCaExtension(pathLenConstraint: number):
23
23
  export declare function basicConstraintsLeafExtension(): Uint8Array;
24
24
  export declare function keyUsageExtension(usages: readonly KeyUsageBit[]): Uint8Array;
25
25
  export declare function extendedKeyUsageClientAuthExtension(): Uint8Array;
26
+ export declare function extendedKeyUsageDocumentSigningExtension(): Uint8Array;
26
27
  export declare function subjectKeyIdentifierExtension(keyIdentifier: Uint8Array): Uint8Array;
27
28
  export declare function authorityKeyIdentifierExtension(keyIdentifier: Uint8Array): Uint8Array;
28
29
  export declare function subjectAltNameExtension(dnsNames?: readonly string[], ipAddresses?: readonly string[]): Uint8Array | undefined;
30
+ export declare function extension(extensionOid: string, critical: boolean, valueDer: Uint8Array): Uint8Array;
29
31
  declare const KEY_USAGE_BITS: {
30
32
  readonly digitalSignature: 0;
33
+ readonly contentCommitment: 1;
31
34
  readonly keyCertSign: 5;
32
35
  readonly cRLSign: 6;
33
36
  };
@@ -1 +1 @@
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;AAuID,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,wCAAwC,IAAI,UAAU,CAErE;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;AAaD,wBAAgB,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,GAAG,UAAU,CAMnG;AAoHD,QAAA,MAAM,cAAc;;;;;CAKV,CAAC;AAEX,KAAK,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC"}
package/dist/x509.js CHANGED
@@ -43,6 +43,9 @@ export function keyUsageExtension(usages) {
43
43
  export function extendedKeyUsageClientAuthExtension() {
44
44
  return extension(OID.extendedKeyUsage, false, sequence(oid(OID.clientAuth)));
45
45
  }
46
+ export function extendedKeyUsageDocumentSigningExtension() {
47
+ return extension(OID.extendedKeyUsage, false, sequence(oid(OID.documentSigning)));
48
+ }
46
49
  export function subjectKeyIdentifierExtension(keyIdentifier) {
47
50
  return extension(OID.subjectKeyIdentifier, false, octetString(keyIdentifier));
48
51
  }
@@ -90,7 +93,7 @@ function assertOptionalStringArray(name, values) {
90
93
  throw new Error(`${name} must be an array`);
91
94
  }
92
95
  }
93
- function extension(extensionOid, critical, valueDer) {
96
+ export function extension(extensionOid, critical, valueDer) {
94
97
  return sequence(oid(extensionOid), ...(critical ? [boolean(true)] : []), octetString(valueDer));
95
98
  }
96
99
  function encodeSerialNumber(serialNumber) {
@@ -192,6 +195,7 @@ function assertYearInRange(year, fieldName) {
192
195
  }
193
196
  const KEY_USAGE_BITS = {
194
197
  digitalSignature: 0,
198
+ contentCommitment: 1,
195
199
  keyCertSign: 5,
196
200
  cRLSign: 6
197
201
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noz-ele/edgca",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
@@ -49,6 +49,9 @@
49
49
  }
50
50
  },
51
51
  "types": "./dist/index.d.ts",
52
+ "bin": {
53
+ "edgca": "./dist/cli.js"
54
+ },
52
55
  "scripts": {
53
56
  "build": "tsc -p tsconfig.json",
54
57
  "typecheck": "tsc -p tsconfig.json --noEmit",
@@ -61,6 +64,7 @@
61
64
  },
62
65
  "devDependencies": {
63
66
  "@cloudflare/vitest-pool-workers": "^0.15.2",
67
+ "@types/node": "^25.7.0",
64
68
  "@vitest/coverage-istanbul": "^4.1.5",
65
69
  "fast-check": "^4.7.0",
66
70
  "typescript": "^5.9.3",