@noz-ele/edgca 0.4.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.
package/README.md CHANGED
@@ -23,6 +23,7 @@ ECDSA on **NIST P-256, P-384, and P-521** is supported throughout (signing, veri
23
23
 
24
24
  ## Contents
25
25
 
26
+ - [CLI](#cli) — `npx edgca …` one-liners for the four most common tasks
26
27
  - [Quick Start](#quick-start) — root → intermediate → client cert (incl. PFX bundling)
27
28
  - [Issue a document-signing certificate](#issue-a-document-signing-certificate) — RFC 9336 `id-kp-documentSigning` leaf
28
29
  - [Verify on Cloudflare Worker](#verify-cloudflare-worker) — confirm a cert was issued by your CA
@@ -41,6 +42,66 @@ npm install @noz-ele/edgca
41
42
 
42
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.
43
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
+
44
105
  ## Quick Start
45
106
 
46
107
  ```ts
@@ -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,6 +1,8 @@
1
1
  export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueDocumentSigningCert, issueIntermediateCA } from "./ca.js";
2
- export { parseCertificateSigningRequest, verifyCertificateSigningRequestSignature, type ParsedCertificateSigningRequest, type CertificateSigningRequestExtension, type CertificateSigningRequestAttribute } from "./csr.js";
3
- export { pemToDer, certificateToPem } from "./pem.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
8
  export type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueDocumentSigningCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey, IssuedDocumentSigningCertificate, SerialNumber, ShortSubjectAttributeType, Subject, SubjectAttribute, SubjectAttributeType } from "./types.js";
@@ -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,wBAAwB,EACxB,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,+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"}
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
1
  export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueDocumentSigningCert, issueIntermediateCA } from "./ca.js";
2
- export { parseCertificateSigningRequest, verifyCertificateSigningRequestSignature } from "./csr.js";
3
- export { pemToDer, certificateToPem } from "./pem.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/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/x509.d.ts CHANGED
@@ -27,6 +27,7 @@ export declare function extendedKeyUsageDocumentSigningExtension(): Uint8Array;
27
27
  export declare function subjectKeyIdentifierExtension(keyIdentifier: Uint8Array): Uint8Array;
28
28
  export declare function authorityKeyIdentifierExtension(keyIdentifier: Uint8Array): Uint8Array;
29
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;
30
31
  declare const KEY_USAGE_BITS: {
31
32
  readonly digitalSignature: 0;
32
33
  readonly contentCommitment: 1;
@@ -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,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;AAuID,QAAA,MAAM,cAAc;;;;;CAKV,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
@@ -93,7 +93,7 @@ function assertOptionalStringArray(name, values) {
93
93
  throw new Error(`${name} must be an array`);
94
94
  }
95
95
  }
96
- function extension(extensionOid, critical, valueDer) {
96
+ export function extension(extensionOid, critical, valueDer) {
97
97
  return sequence(oid(extensionOid), ...(critical ? [boolean(true)] : []), octetString(valueDer));
98
98
  }
99
99
  function encodeSerialNumber(serialNumber) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noz-ele/edgca",
3
- "version": "0.4.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",