@kybernesis/arp-tls 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kybernesis AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # `@kybernesis/arp-tls`
2
+
3
+ Self-signed Ed25519 X.509 certificate generation plus DID-pinned fingerprint
4
+ validation for ARP agents.
5
+
6
+ ## Why not Let's Encrypt?
7
+
8
+ Agent names live on Handshake (`.agent`). Web PKI can't see them. ARP v0 skips
9
+ CAs entirely — agents generate a long-lived self-signed cert, publish its
10
+ SHA-256 fingerprint in their DID document, and peers validate against that
11
+ pin. See `docs/ARP-hns-resolution.md §4`.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @kybernesis/arp-tls
17
+ ```
18
+
19
+ ## Use
20
+
21
+ ```ts
22
+ import {
23
+ generateAgentCert,
24
+ computeFingerprint,
25
+ validatePinnedDer,
26
+ toTlsServerOptions,
27
+ } from '@kybernesis/arp-tls';
28
+ import { createServer, connect } from 'node:tls';
29
+
30
+ const result = await generateAgentCert({ did: 'did:web:samantha.agent' });
31
+ if (!result.ok) throw new Error(result.error.message);
32
+ const { certPem, keyPem, fingerprint } = result.value;
33
+
34
+ // Server side
35
+ const server = createServer(toTlsServerOptions(result.value), (socket) => {
36
+ socket.end('hello');
37
+ });
38
+
39
+ // Client side — pin against the fingerprint found in the peer's DID doc.
40
+ const socket = connect({
41
+ host: 'samantha.agent',
42
+ port: 443,
43
+ rejectUnauthorized: false,
44
+ servername: 'samantha.agent',
45
+ });
46
+ socket.on('secureConnect', () => {
47
+ const peer = socket.getPeerX509Certificate()!;
48
+ if (!validatePinnedDer(peer.raw, expectedFingerprintFromDidDoc)) {
49
+ socket.destroy();
50
+ throw new Error('pin mismatch');
51
+ }
52
+ });
53
+ ```
54
+
55
+ ## API
56
+
57
+ | Function | Notes |
58
+ | --------------------------------------- | --------------------------------------------- |
59
+ | `generateAgentCert(opts)` | Ed25519 self-signed cert, 10-year validity. |
60
+ | `computeFingerprint(pemOrDer)` | SHA-256 of DER, lowercase hex. |
61
+ | `validatePinnedCert(pem, expected)` | Constant-time compare; accepts `sha256:` prefix. |
62
+ | `validatePinnedDer(der, expected)` | Same as above for Node's `peer.raw`. |
63
+ | `toTlsServerOptions(cert)` | Drop into `tls.createServer`/`https.createServer`. |
64
+ | `toTlsClientOptions({host, port})` | `rejectUnauthorized: false` baseline — you own the pin check. |
65
+
66
+ ## Design notes
67
+
68
+ - The generator mints a fresh Ed25519 keypair rather than consuming the
69
+ agent's DID signing key. In v0 the TLS key is independent; Phase 3+ can
70
+ optionally reuse the DID key once Node's Web Crypto supports importing
71
+ raw multibase keys directly.
72
+ - `@peculiar/x509` is the sole extra dependency; Node's built-in
73
+ `X509Certificate` handles parsing.
74
+ - Fingerprint comparison is constant-time to keep timing side channels off
75
+ the table.
package/dist/index.cjs ADDED
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var x509 = require('@peculiar/x509');
5
+
6
+ function _interopNamespace(e) {
7
+ if (e && e.__esModule) return e;
8
+ var n = Object.create(null);
9
+ if (e) {
10
+ Object.keys(e).forEach(function (k) {
11
+ if (k !== 'default') {
12
+ var d = Object.getOwnPropertyDescriptor(e, k);
13
+ Object.defineProperty(n, k, d.get ? d : {
14
+ enumerable: true,
15
+ get: function () { return e[k]; }
16
+ });
17
+ }
18
+ });
19
+ }
20
+ n.default = e;
21
+ return Object.freeze(n);
22
+ }
23
+
24
+ var x509__namespace = /*#__PURE__*/_interopNamespace(x509);
25
+
26
+ // src/cert.ts
27
+
28
+ // src/errors.ts
29
+ function tlsError(code, message, cause) {
30
+ return { code, message, cause };
31
+ }
32
+
33
+ // src/cert.ts
34
+ x509__namespace.cryptoProvider.set(crypto.webcrypto);
35
+ var CERT_VALIDITY_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
36
+ async function generateAgentCert(opts) {
37
+ const host = opts.sanHostname ?? extractHostFromDid(opts.did);
38
+ if (!host) {
39
+ return {
40
+ ok: false,
41
+ error: tlsError("invalid_input", `cannot derive SAN hostname from ${opts.did}`)
42
+ };
43
+ }
44
+ const now = opts.now?.() ?? /* @__PURE__ */ new Date();
45
+ const notAfter = new Date(now.getTime() + (opts.validityMs ?? CERT_VALIDITY_MS));
46
+ const serial = opts.serialNumber ?? randomSerialHex();
47
+ try {
48
+ const keys = await crypto.webcrypto.subtle.generateKey({ name: "Ed25519" }, true, [
49
+ "sign",
50
+ "verify"
51
+ ]);
52
+ const cert = await x509__namespace.X509CertificateGenerator.createSelfSigned({
53
+ name: `CN=${escapeDn(opts.did)}`,
54
+ notBefore: now,
55
+ notAfter,
56
+ serialNumber: serial,
57
+ signingAlgorithm: { name: "Ed25519" },
58
+ keys,
59
+ extensions: [
60
+ new x509__namespace.BasicConstraintsExtension(false, void 0, true),
61
+ new x509__namespace.KeyUsagesExtension(
62
+ x509__namespace.KeyUsageFlags.digitalSignature | x509__namespace.KeyUsageFlags.keyCertSign,
63
+ true
64
+ ),
65
+ new x509__namespace.ExtendedKeyUsageExtension(
66
+ [x509__namespace.ExtendedKeyUsage.serverAuth, x509__namespace.ExtendedKeyUsage.clientAuth],
67
+ true
68
+ ),
69
+ new x509__namespace.SubjectAlternativeNameExtension([{ type: "dns", value: host }])
70
+ ]
71
+ });
72
+ const pkcs8 = await crypto.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
73
+ const certPem = cert.toString("pem");
74
+ const keyPem = derToPem(new Uint8Array(pkcs8), "PRIVATE KEY");
75
+ const fingerprint = sha256HexOfDer(cert.rawData);
76
+ return { ok: true, value: { certPem, keyPem, fingerprint } };
77
+ } catch (err) {
78
+ return {
79
+ ok: false,
80
+ error: tlsError("cert_generation_failed", "failed to generate ed25519 cert", err)
81
+ };
82
+ }
83
+ }
84
+ function computeFingerprint(certPemOrDer) {
85
+ if (typeof certPemOrDer === "string") {
86
+ const der = pemToDer(certPemOrDer);
87
+ return sha256HexOfDer(der);
88
+ }
89
+ return sha256HexOfDer(certPemOrDer);
90
+ }
91
+ function validatePinnedCert(peerCertPem, expectedFingerprint) {
92
+ let actual;
93
+ try {
94
+ actual = computeFingerprint(peerCertPem);
95
+ } catch {
96
+ return false;
97
+ }
98
+ const expected = normalizeFingerprint(expectedFingerprint);
99
+ if (actual.length !== expected.length) return false;
100
+ let mismatch = 0;
101
+ for (let i = 0; i < actual.length; i++) {
102
+ mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
103
+ }
104
+ return mismatch === 0;
105
+ }
106
+ function validatePinnedDer(peerDer, expectedFingerprint) {
107
+ const actual = sha256HexOfDer(peerDer);
108
+ const expected = normalizeFingerprint(expectedFingerprint);
109
+ if (actual.length !== expected.length) return false;
110
+ let mismatch = 0;
111
+ for (let i = 0; i < actual.length; i++) {
112
+ mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
113
+ }
114
+ return mismatch === 0;
115
+ }
116
+ function parseCertificatePem(pem) {
117
+ return new crypto.X509Certificate(pem);
118
+ }
119
+ function extractHostFromDid(did) {
120
+ if (!did.startsWith("did:web:")) return null;
121
+ const body = did.slice("did:web:".length);
122
+ const first = body.split(":")[0];
123
+ return first ? decodeURIComponent(first) : null;
124
+ }
125
+ function sha256HexOfDer(der) {
126
+ const buf = der instanceof Uint8Array ? der : new Uint8Array(der);
127
+ return crypto.createHash("sha256").update(buf).digest("hex");
128
+ }
129
+ function pemToDer(pem) {
130
+ const match = pem.match(/-----BEGIN [^-]+-----([\s\S]+?)-----END [^-]+-----/);
131
+ if (!match || !match[1]) {
132
+ throw new Error("invalid PEM input");
133
+ }
134
+ const b64 = match[1].replace(/\s+/g, "");
135
+ return new Uint8Array(Buffer.from(b64, "base64"));
136
+ }
137
+ function derToPem(der, label) {
138
+ const b64 = Buffer.from(der).toString("base64");
139
+ const wrapped = b64.replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
140
+ return `-----BEGIN ${label}-----
141
+ ${wrapped}
142
+ -----END ${label}-----
143
+ `;
144
+ }
145
+ function normalizeFingerprint(value) {
146
+ return value.trim().toLowerCase().replace(/^sha-?256:/, "").replace(/:/g, "");
147
+ }
148
+ function randomSerialHex() {
149
+ const bytes = new Uint8Array(16);
150
+ crypto.webcrypto.getRandomValues(bytes);
151
+ if (bytes[0] !== void 0) {
152
+ bytes[0] = bytes[0] & 127;
153
+ }
154
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
155
+ }
156
+ function escapeDn(value) {
157
+ return value.replace(/([,+"\\<>;#=])/g, "\\$1");
158
+ }
159
+
160
+ // src/server.ts
161
+ function toTlsServerOptions(cert) {
162
+ return {
163
+ cert: cert.certPem,
164
+ key: cert.keyPem
165
+ };
166
+ }
167
+ function toTlsClientOptions(expected) {
168
+ const sni = expected.servername ?? (isLiteralIp(expected.host) ? void 0 : expected.host);
169
+ return {
170
+ host: expected.host,
171
+ port: expected.port,
172
+ rejectUnauthorized: false,
173
+ ...sni ? { servername: sni } : {}
174
+ };
175
+ }
176
+ function isLiteralIp(host) {
177
+ return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(host) || host.includes(":");
178
+ }
179
+
180
+ exports.CERT_VALIDITY_MS = CERT_VALIDITY_MS;
181
+ exports.computeFingerprint = computeFingerprint;
182
+ exports.extractHostFromDid = extractHostFromDid;
183
+ exports.generateAgentCert = generateAgentCert;
184
+ exports.parseCertificatePem = parseCertificatePem;
185
+ exports.tlsError = tlsError;
186
+ exports.toTlsClientOptions = toTlsClientOptions;
187
+ exports.toTlsServerOptions = toTlsServerOptions;
188
+ exports.validatePinnedCert = validatePinnedCert;
189
+ exports.validatePinnedDer = validatePinnedDer;
190
+ //# sourceMappingURL=index.cjs.map
191
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/cert.ts","../src/server.ts"],"names":["x509","webcrypto","X509Certificate","createHash"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Structured TLS errors. Surfaced via `Result<T, E>` at package boundaries.\n */\n\nexport type TlsErrorCode =\n | 'invalid_input'\n | 'cert_generation_failed'\n | 'fingerprint_mismatch'\n | 'parse_failed';\n\nexport interface TlsError {\n code: TlsErrorCode;\n message: string;\n cause?: unknown;\n}\n\nexport function tlsError(code: TlsErrorCode, message: string, cause?: unknown): TlsError {\n return { code, message, cause };\n}\n","import { createHash, webcrypto, X509Certificate } from 'node:crypto';\nimport * as x509 from '@peculiar/x509';\nimport { tlsError, type TlsError } from './errors.js';\n\n// `@peculiar/x509` accepts any Web Crypto provider; Node's `webcrypto` is\n// structurally compatible but the types diverge. Cast narrows to the shape\n// the library needs.\nx509.cryptoProvider.set(webcrypto as unknown as Parameters<typeof x509.cryptoProvider.set>[0]);\n\n/** 10 years in ms, matching Phase 2 Task 2 §1. */\nexport const CERT_VALIDITY_MS = 10 * 365 * 24 * 60 * 60 * 1000;\n\nexport interface GeneratedCert {\n /** PEM-encoded X.509 certificate. */\n certPem: string;\n /** PEM-encoded PKCS#8 Ed25519 private key. */\n keyPem: string;\n /** SHA-256 of DER bytes, lowercase hex (no `sha256:` prefix). */\n fingerprint: string;\n}\n\nexport interface GenerateAgentCertOptions {\n /** Agent DID used for the cert Common Name, e.g. `did:web:samantha.agent`. */\n did: string;\n /**\n * Multibase-encoded Ed25519 public key. Currently unused in v0 — the cert\n * is generated from a freshly-minted key pair, as Node's Web Crypto does\n * not ingest raw multibase keys directly. Retained on the signature for\n * forward compatibility with `did:web` key-reuse workflows (Phase 3+).\n */\n publicKeyMultibase?: string;\n /** Override SAN. Defaults to the host parsed out of the DID. */\n sanHostname?: string;\n /** Override validity period (ms). Defaults to 10 years. */\n validityMs?: number;\n /** Clock injection for tests. */\n now?: () => Date;\n /** Serial number override (hex). Random when omitted. */\n serialNumber?: string;\n}\n\n/**\n * Generate a self-signed Ed25519 X.509 certificate pinned to an agent DID.\n *\n * - `CN` = the agent DID\n * - `subjectAltName` = the DNS host parsed out of `did:web:<host>` (or\n * `options.sanHostname`)\n * - validity = 10 years by default\n */\nexport async function generateAgentCert(\n opts: GenerateAgentCertOptions,\n): Promise<{ ok: true; value: GeneratedCert } | { ok: false; error: TlsError }> {\n const host = opts.sanHostname ?? extractHostFromDid(opts.did);\n if (!host) {\n return {\n ok: false,\n error: tlsError('invalid_input', `cannot derive SAN hostname from ${opts.did}`),\n };\n }\n\n const now = opts.now?.() ?? new Date();\n const notAfter = new Date(now.getTime() + (opts.validityMs ?? CERT_VALIDITY_MS));\n const serial = opts.serialNumber ?? randomSerialHex();\n\n try {\n const keys = (await webcrypto.subtle.generateKey({ name: 'Ed25519' }, true, [\n 'sign',\n 'verify',\n ])) as { privateKey: webcrypto.CryptoKey; publicKey: webcrypto.CryptoKey };\n const cert = await x509.X509CertificateGenerator.createSelfSigned({\n name: `CN=${escapeDn(opts.did)}`,\n notBefore: now,\n notAfter,\n serialNumber: serial,\n signingAlgorithm: { name: 'Ed25519' },\n keys,\n extensions: [\n new x509.BasicConstraintsExtension(false, undefined, true),\n new x509.KeyUsagesExtension(\n x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyCertSign,\n true,\n ),\n new x509.ExtendedKeyUsageExtension(\n [x509.ExtendedKeyUsage.serverAuth, x509.ExtendedKeyUsage.clientAuth],\n true,\n ),\n new x509.SubjectAlternativeNameExtension([{ type: 'dns', value: host }]),\n ],\n });\n\n const pkcs8 = await webcrypto.subtle.exportKey('pkcs8', keys.privateKey);\n const certPem = cert.toString('pem');\n const keyPem = derToPem(new Uint8Array(pkcs8), 'PRIVATE KEY');\n const fingerprint = sha256HexOfDer(cert.rawData);\n return { ok: true, value: { certPem, keyPem, fingerprint } };\n } catch (err) {\n return {\n ok: false,\n error: tlsError('cert_generation_failed', 'failed to generate ed25519 cert', err),\n };\n }\n}\n\n/**\n * SHA-256 of DER bytes, lowercase hex. Strips PEM headers if a PEM string\n * is passed in.\n */\nexport function computeFingerprint(certPemOrDer: string | ArrayBuffer | Uint8Array): string {\n if (typeof certPemOrDer === 'string') {\n const der = pemToDer(certPemOrDer);\n return sha256HexOfDer(der);\n }\n return sha256HexOfDer(certPemOrDer);\n}\n\n/**\n * Validate a peer certificate (PEM) against an expected fingerprint. Constant-\n * time hex compare.\n */\nexport function validatePinnedCert(\n peerCertPem: string,\n expectedFingerprint: string,\n): boolean {\n let actual: string;\n try {\n actual = computeFingerprint(peerCertPem);\n } catch {\n return false;\n }\n const expected = normalizeFingerprint(expectedFingerprint);\n if (actual.length !== expected.length) return false;\n let mismatch = 0;\n for (let i = 0; i < actual.length; i++) {\n mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\n/**\n * Validate a peer cert in DER form (as returned by Node `tls.TLSSocket`\n * `getPeerX509Certificate().raw`).\n */\nexport function validatePinnedDer(\n peerDer: ArrayBuffer | Uint8Array,\n expectedFingerprint: string,\n): boolean {\n const actual = sha256HexOfDer(peerDer);\n const expected = normalizeFingerprint(expectedFingerprint);\n if (actual.length !== expected.length) return false;\n let mismatch = 0;\n for (let i = 0; i < actual.length; i++) {\n mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\nexport function parseCertificatePem(pem: string): X509Certificate {\n return new X509Certificate(pem);\n}\n\nexport function extractHostFromDid(did: string): string | null {\n if (!did.startsWith('did:web:')) return null;\n const body = did.slice('did:web:'.length);\n const first = body.split(':')[0];\n return first ? decodeURIComponent(first) : null;\n}\n\nfunction sha256HexOfDer(der: ArrayBuffer | Uint8Array): string {\n const buf = der instanceof Uint8Array ? der : new Uint8Array(der);\n return createHash('sha256').update(buf).digest('hex');\n}\n\nfunction pemToDer(pem: string): Uint8Array {\n const match = pem.match(/-----BEGIN [^-]+-----([\\s\\S]+?)-----END [^-]+-----/);\n if (!match || !match[1]) {\n throw new Error('invalid PEM input');\n }\n const b64 = match[1].replace(/\\s+/g, '');\n return new Uint8Array(Buffer.from(b64, 'base64'));\n}\n\nfunction derToPem(der: Uint8Array, label: string): string {\n const b64 = Buffer.from(der).toString('base64');\n const wrapped = b64.replace(/(.{64})/g, '$1\\n').replace(/\\n$/, '');\n return `-----BEGIN ${label}-----\\n${wrapped}\\n-----END ${label}-----\\n`;\n}\n\nfunction normalizeFingerprint(value: string): string {\n return value.trim().toLowerCase().replace(/^sha-?256:/, '').replace(/:/g, '');\n}\n\nfunction randomSerialHex(): string {\n const bytes = new Uint8Array(16);\n webcrypto.getRandomValues(bytes);\n // Serial must be positive. Force MSB=0 to keep it unambiguously positive.\n if (bytes[0] !== undefined) {\n bytes[0] = bytes[0] & 0x7f;\n }\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\nfunction escapeDn(value: string): string {\n // RFC 4514 special chars: , + \" \\ < > ; # (leading) and spaces (leading/trailing)\n return value.replace(/([,+\"\\\\<>;#=])/g, '\\\\$1');\n}\n","import type { SecureContextOptions } from 'node:tls';\nimport type { GeneratedCert } from './cert.js';\n\n/**\n * Build the subset of TLS options Node's `createSecureContext` /\n * `tls.createServer` need to serve a DID-pinned self-signed cert.\n *\n * Usage:\n * const srv = https.createServer(toTlsServerOptions(cert), app);\n * tls.createServer(toTlsServerOptions(cert), ...);\n */\nexport function toTlsServerOptions(cert: GeneratedCert): SecureContextOptions {\n return {\n cert: cert.certPem,\n key: cert.keyPem,\n };\n}\n\n/**\n * Minimum tls.connect options for a pinning-aware client. The caller is\n * responsible for verifying the peer's fingerprint in the `secureConnect`\n * handler (use `validatePinnedDer(sock.getPeerX509Certificate().raw, fp)`).\n * `rejectUnauthorized: false` is required — the ARP PKI is DID-pinned, not\n * CA-chained.\n *\n * `servername` defaults to `host` unless `host` is a literal IP (Node rejects\n * IP-valued SNI). Pass an explicit `servername` when connecting via IP — it\n * should be the DNS name the peer's cert SAN covers.\n */\nexport function toTlsClientOptions(expected: {\n host: string;\n port: number;\n servername?: string;\n}): {\n host: string;\n port: number;\n rejectUnauthorized: false;\n servername?: string;\n} {\n const sni = expected.servername ?? (isLiteralIp(expected.host) ? undefined : expected.host);\n return {\n host: expected.host,\n port: expected.port,\n rejectUnauthorized: false,\n ...(sni ? { servername: sni } : {}),\n };\n}\n\nfunction isLiteralIp(host: string): boolean {\n // Cheap check — good enough for excluding IPv4/IPv6 literals from SNI.\n return /^(?:\\d{1,3}\\.){3}\\d{1,3}$/.test(host) || host.includes(':');\n}\n"]}
@@ -0,0 +1,108 @@
1
+ import { X509Certificate } from 'node:crypto';
2
+ import { SecureContextOptions } from 'node:tls';
3
+
4
+ /**
5
+ * Structured TLS errors. Surfaced via `Result<T, E>` at package boundaries.
6
+ */
7
+ type TlsErrorCode = 'invalid_input' | 'cert_generation_failed' | 'fingerprint_mismatch' | 'parse_failed';
8
+ interface TlsError {
9
+ code: TlsErrorCode;
10
+ message: string;
11
+ cause?: unknown;
12
+ }
13
+ declare function tlsError(code: TlsErrorCode, message: string, cause?: unknown): TlsError;
14
+
15
+ /** 10 years in ms, matching Phase 2 Task 2 §1. */
16
+ declare const CERT_VALIDITY_MS: number;
17
+ interface GeneratedCert {
18
+ /** PEM-encoded X.509 certificate. */
19
+ certPem: string;
20
+ /** PEM-encoded PKCS#8 Ed25519 private key. */
21
+ keyPem: string;
22
+ /** SHA-256 of DER bytes, lowercase hex (no `sha256:` prefix). */
23
+ fingerprint: string;
24
+ }
25
+ interface GenerateAgentCertOptions {
26
+ /** Agent DID used for the cert Common Name, e.g. `did:web:samantha.agent`. */
27
+ did: string;
28
+ /**
29
+ * Multibase-encoded Ed25519 public key. Currently unused in v0 — the cert
30
+ * is generated from a freshly-minted key pair, as Node's Web Crypto does
31
+ * not ingest raw multibase keys directly. Retained on the signature for
32
+ * forward compatibility with `did:web` key-reuse workflows (Phase 3+).
33
+ */
34
+ publicKeyMultibase?: string;
35
+ /** Override SAN. Defaults to the host parsed out of the DID. */
36
+ sanHostname?: string;
37
+ /** Override validity period (ms). Defaults to 10 years. */
38
+ validityMs?: number;
39
+ /** Clock injection for tests. */
40
+ now?: () => Date;
41
+ /** Serial number override (hex). Random when omitted. */
42
+ serialNumber?: string;
43
+ }
44
+ /**
45
+ * Generate a self-signed Ed25519 X.509 certificate pinned to an agent DID.
46
+ *
47
+ * - `CN` = the agent DID
48
+ * - `subjectAltName` = the DNS host parsed out of `did:web:<host>` (or
49
+ * `options.sanHostname`)
50
+ * - validity = 10 years by default
51
+ */
52
+ declare function generateAgentCert(opts: GenerateAgentCertOptions): Promise<{
53
+ ok: true;
54
+ value: GeneratedCert;
55
+ } | {
56
+ ok: false;
57
+ error: TlsError;
58
+ }>;
59
+ /**
60
+ * SHA-256 of DER bytes, lowercase hex. Strips PEM headers if a PEM string
61
+ * is passed in.
62
+ */
63
+ declare function computeFingerprint(certPemOrDer: string | ArrayBuffer | Uint8Array): string;
64
+ /**
65
+ * Validate a peer certificate (PEM) against an expected fingerprint. Constant-
66
+ * time hex compare.
67
+ */
68
+ declare function validatePinnedCert(peerCertPem: string, expectedFingerprint: string): boolean;
69
+ /**
70
+ * Validate a peer cert in DER form (as returned by Node `tls.TLSSocket`
71
+ * `getPeerX509Certificate().raw`).
72
+ */
73
+ declare function validatePinnedDer(peerDer: ArrayBuffer | Uint8Array, expectedFingerprint: string): boolean;
74
+ declare function parseCertificatePem(pem: string): X509Certificate;
75
+ declare function extractHostFromDid(did: string): string | null;
76
+
77
+ /**
78
+ * Build the subset of TLS options Node's `createSecureContext` /
79
+ * `tls.createServer` need to serve a DID-pinned self-signed cert.
80
+ *
81
+ * Usage:
82
+ * const srv = https.createServer(toTlsServerOptions(cert), app);
83
+ * tls.createServer(toTlsServerOptions(cert), ...);
84
+ */
85
+ declare function toTlsServerOptions(cert: GeneratedCert): SecureContextOptions;
86
+ /**
87
+ * Minimum tls.connect options for a pinning-aware client. The caller is
88
+ * responsible for verifying the peer's fingerprint in the `secureConnect`
89
+ * handler (use `validatePinnedDer(sock.getPeerX509Certificate().raw, fp)`).
90
+ * `rejectUnauthorized: false` is required — the ARP PKI is DID-pinned, not
91
+ * CA-chained.
92
+ *
93
+ * `servername` defaults to `host` unless `host` is a literal IP (Node rejects
94
+ * IP-valued SNI). Pass an explicit `servername` when connecting via IP — it
95
+ * should be the DNS name the peer's cert SAN covers.
96
+ */
97
+ declare function toTlsClientOptions(expected: {
98
+ host: string;
99
+ port: number;
100
+ servername?: string;
101
+ }): {
102
+ host: string;
103
+ port: number;
104
+ rejectUnauthorized: false;
105
+ servername?: string;
106
+ };
107
+
108
+ export { CERT_VALIDITY_MS, type GenerateAgentCertOptions, type GeneratedCert, type TlsError, type TlsErrorCode, computeFingerprint, extractHostFromDid, generateAgentCert, parseCertificatePem, tlsError, toTlsClientOptions, toTlsServerOptions, validatePinnedCert, validatePinnedDer };
@@ -0,0 +1,108 @@
1
+ import { X509Certificate } from 'node:crypto';
2
+ import { SecureContextOptions } from 'node:tls';
3
+
4
+ /**
5
+ * Structured TLS errors. Surfaced via `Result<T, E>` at package boundaries.
6
+ */
7
+ type TlsErrorCode = 'invalid_input' | 'cert_generation_failed' | 'fingerprint_mismatch' | 'parse_failed';
8
+ interface TlsError {
9
+ code: TlsErrorCode;
10
+ message: string;
11
+ cause?: unknown;
12
+ }
13
+ declare function tlsError(code: TlsErrorCode, message: string, cause?: unknown): TlsError;
14
+
15
+ /** 10 years in ms, matching Phase 2 Task 2 §1. */
16
+ declare const CERT_VALIDITY_MS: number;
17
+ interface GeneratedCert {
18
+ /** PEM-encoded X.509 certificate. */
19
+ certPem: string;
20
+ /** PEM-encoded PKCS#8 Ed25519 private key. */
21
+ keyPem: string;
22
+ /** SHA-256 of DER bytes, lowercase hex (no `sha256:` prefix). */
23
+ fingerprint: string;
24
+ }
25
+ interface GenerateAgentCertOptions {
26
+ /** Agent DID used for the cert Common Name, e.g. `did:web:samantha.agent`. */
27
+ did: string;
28
+ /**
29
+ * Multibase-encoded Ed25519 public key. Currently unused in v0 — the cert
30
+ * is generated from a freshly-minted key pair, as Node's Web Crypto does
31
+ * not ingest raw multibase keys directly. Retained on the signature for
32
+ * forward compatibility with `did:web` key-reuse workflows (Phase 3+).
33
+ */
34
+ publicKeyMultibase?: string;
35
+ /** Override SAN. Defaults to the host parsed out of the DID. */
36
+ sanHostname?: string;
37
+ /** Override validity period (ms). Defaults to 10 years. */
38
+ validityMs?: number;
39
+ /** Clock injection for tests. */
40
+ now?: () => Date;
41
+ /** Serial number override (hex). Random when omitted. */
42
+ serialNumber?: string;
43
+ }
44
+ /**
45
+ * Generate a self-signed Ed25519 X.509 certificate pinned to an agent DID.
46
+ *
47
+ * - `CN` = the agent DID
48
+ * - `subjectAltName` = the DNS host parsed out of `did:web:<host>` (or
49
+ * `options.sanHostname`)
50
+ * - validity = 10 years by default
51
+ */
52
+ declare function generateAgentCert(opts: GenerateAgentCertOptions): Promise<{
53
+ ok: true;
54
+ value: GeneratedCert;
55
+ } | {
56
+ ok: false;
57
+ error: TlsError;
58
+ }>;
59
+ /**
60
+ * SHA-256 of DER bytes, lowercase hex. Strips PEM headers if a PEM string
61
+ * is passed in.
62
+ */
63
+ declare function computeFingerprint(certPemOrDer: string | ArrayBuffer | Uint8Array): string;
64
+ /**
65
+ * Validate a peer certificate (PEM) against an expected fingerprint. Constant-
66
+ * time hex compare.
67
+ */
68
+ declare function validatePinnedCert(peerCertPem: string, expectedFingerprint: string): boolean;
69
+ /**
70
+ * Validate a peer cert in DER form (as returned by Node `tls.TLSSocket`
71
+ * `getPeerX509Certificate().raw`).
72
+ */
73
+ declare function validatePinnedDer(peerDer: ArrayBuffer | Uint8Array, expectedFingerprint: string): boolean;
74
+ declare function parseCertificatePem(pem: string): X509Certificate;
75
+ declare function extractHostFromDid(did: string): string | null;
76
+
77
+ /**
78
+ * Build the subset of TLS options Node's `createSecureContext` /
79
+ * `tls.createServer` need to serve a DID-pinned self-signed cert.
80
+ *
81
+ * Usage:
82
+ * const srv = https.createServer(toTlsServerOptions(cert), app);
83
+ * tls.createServer(toTlsServerOptions(cert), ...);
84
+ */
85
+ declare function toTlsServerOptions(cert: GeneratedCert): SecureContextOptions;
86
+ /**
87
+ * Minimum tls.connect options for a pinning-aware client. The caller is
88
+ * responsible for verifying the peer's fingerprint in the `secureConnect`
89
+ * handler (use `validatePinnedDer(sock.getPeerX509Certificate().raw, fp)`).
90
+ * `rejectUnauthorized: false` is required — the ARP PKI is DID-pinned, not
91
+ * CA-chained.
92
+ *
93
+ * `servername` defaults to `host` unless `host` is a literal IP (Node rejects
94
+ * IP-valued SNI). Pass an explicit `servername` when connecting via IP — it
95
+ * should be the DNS name the peer's cert SAN covers.
96
+ */
97
+ declare function toTlsClientOptions(expected: {
98
+ host: string;
99
+ port: number;
100
+ servername?: string;
101
+ }): {
102
+ host: string;
103
+ port: number;
104
+ rejectUnauthorized: false;
105
+ servername?: string;
106
+ };
107
+
108
+ export { CERT_VALIDITY_MS, type GenerateAgentCertOptions, type GeneratedCert, type TlsError, type TlsErrorCode, computeFingerprint, extractHostFromDid, generateAgentCert, parseCertificatePem, tlsError, toTlsClientOptions, toTlsServerOptions, validatePinnedCert, validatePinnedDer };
package/dist/index.js ADDED
@@ -0,0 +1,160 @@
1
+ import { webcrypto, X509Certificate, createHash } from 'crypto';
2
+ import * as x509 from '@peculiar/x509';
3
+
4
+ // src/cert.ts
5
+
6
+ // src/errors.ts
7
+ function tlsError(code, message, cause) {
8
+ return { code, message, cause };
9
+ }
10
+
11
+ // src/cert.ts
12
+ x509.cryptoProvider.set(webcrypto);
13
+ var CERT_VALIDITY_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
14
+ async function generateAgentCert(opts) {
15
+ const host = opts.sanHostname ?? extractHostFromDid(opts.did);
16
+ if (!host) {
17
+ return {
18
+ ok: false,
19
+ error: tlsError("invalid_input", `cannot derive SAN hostname from ${opts.did}`)
20
+ };
21
+ }
22
+ const now = opts.now?.() ?? /* @__PURE__ */ new Date();
23
+ const notAfter = new Date(now.getTime() + (opts.validityMs ?? CERT_VALIDITY_MS));
24
+ const serial = opts.serialNumber ?? randomSerialHex();
25
+ try {
26
+ const keys = await webcrypto.subtle.generateKey({ name: "Ed25519" }, true, [
27
+ "sign",
28
+ "verify"
29
+ ]);
30
+ const cert = await x509.X509CertificateGenerator.createSelfSigned({
31
+ name: `CN=${escapeDn(opts.did)}`,
32
+ notBefore: now,
33
+ notAfter,
34
+ serialNumber: serial,
35
+ signingAlgorithm: { name: "Ed25519" },
36
+ keys,
37
+ extensions: [
38
+ new x509.BasicConstraintsExtension(false, void 0, true),
39
+ new x509.KeyUsagesExtension(
40
+ x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyCertSign,
41
+ true
42
+ ),
43
+ new x509.ExtendedKeyUsageExtension(
44
+ [x509.ExtendedKeyUsage.serverAuth, x509.ExtendedKeyUsage.clientAuth],
45
+ true
46
+ ),
47
+ new x509.SubjectAlternativeNameExtension([{ type: "dns", value: host }])
48
+ ]
49
+ });
50
+ const pkcs8 = await webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
51
+ const certPem = cert.toString("pem");
52
+ const keyPem = derToPem(new Uint8Array(pkcs8), "PRIVATE KEY");
53
+ const fingerprint = sha256HexOfDer(cert.rawData);
54
+ return { ok: true, value: { certPem, keyPem, fingerprint } };
55
+ } catch (err) {
56
+ return {
57
+ ok: false,
58
+ error: tlsError("cert_generation_failed", "failed to generate ed25519 cert", err)
59
+ };
60
+ }
61
+ }
62
+ function computeFingerprint(certPemOrDer) {
63
+ if (typeof certPemOrDer === "string") {
64
+ const der = pemToDer(certPemOrDer);
65
+ return sha256HexOfDer(der);
66
+ }
67
+ return sha256HexOfDer(certPemOrDer);
68
+ }
69
+ function validatePinnedCert(peerCertPem, expectedFingerprint) {
70
+ let actual;
71
+ try {
72
+ actual = computeFingerprint(peerCertPem);
73
+ } catch {
74
+ return false;
75
+ }
76
+ const expected = normalizeFingerprint(expectedFingerprint);
77
+ if (actual.length !== expected.length) return false;
78
+ let mismatch = 0;
79
+ for (let i = 0; i < actual.length; i++) {
80
+ mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
81
+ }
82
+ return mismatch === 0;
83
+ }
84
+ function validatePinnedDer(peerDer, expectedFingerprint) {
85
+ const actual = sha256HexOfDer(peerDer);
86
+ const expected = normalizeFingerprint(expectedFingerprint);
87
+ if (actual.length !== expected.length) return false;
88
+ let mismatch = 0;
89
+ for (let i = 0; i < actual.length; i++) {
90
+ mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
91
+ }
92
+ return mismatch === 0;
93
+ }
94
+ function parseCertificatePem(pem) {
95
+ return new X509Certificate(pem);
96
+ }
97
+ function extractHostFromDid(did) {
98
+ if (!did.startsWith("did:web:")) return null;
99
+ const body = did.slice("did:web:".length);
100
+ const first = body.split(":")[0];
101
+ return first ? decodeURIComponent(first) : null;
102
+ }
103
+ function sha256HexOfDer(der) {
104
+ const buf = der instanceof Uint8Array ? der : new Uint8Array(der);
105
+ return createHash("sha256").update(buf).digest("hex");
106
+ }
107
+ function pemToDer(pem) {
108
+ const match = pem.match(/-----BEGIN [^-]+-----([\s\S]+?)-----END [^-]+-----/);
109
+ if (!match || !match[1]) {
110
+ throw new Error("invalid PEM input");
111
+ }
112
+ const b64 = match[1].replace(/\s+/g, "");
113
+ return new Uint8Array(Buffer.from(b64, "base64"));
114
+ }
115
+ function derToPem(der, label) {
116
+ const b64 = Buffer.from(der).toString("base64");
117
+ const wrapped = b64.replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
118
+ return `-----BEGIN ${label}-----
119
+ ${wrapped}
120
+ -----END ${label}-----
121
+ `;
122
+ }
123
+ function normalizeFingerprint(value) {
124
+ return value.trim().toLowerCase().replace(/^sha-?256:/, "").replace(/:/g, "");
125
+ }
126
+ function randomSerialHex() {
127
+ const bytes = new Uint8Array(16);
128
+ webcrypto.getRandomValues(bytes);
129
+ if (bytes[0] !== void 0) {
130
+ bytes[0] = bytes[0] & 127;
131
+ }
132
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
133
+ }
134
+ function escapeDn(value) {
135
+ return value.replace(/([,+"\\<>;#=])/g, "\\$1");
136
+ }
137
+
138
+ // src/server.ts
139
+ function toTlsServerOptions(cert) {
140
+ return {
141
+ cert: cert.certPem,
142
+ key: cert.keyPem
143
+ };
144
+ }
145
+ function toTlsClientOptions(expected) {
146
+ const sni = expected.servername ?? (isLiteralIp(expected.host) ? void 0 : expected.host);
147
+ return {
148
+ host: expected.host,
149
+ port: expected.port,
150
+ rejectUnauthorized: false,
151
+ ...sni ? { servername: sni } : {}
152
+ };
153
+ }
154
+ function isLiteralIp(host) {
155
+ return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(host) || host.includes(":");
156
+ }
157
+
158
+ export { CERT_VALIDITY_MS, computeFingerprint, extractHostFromDid, generateAgentCert, parseCertificatePem, tlsError, toTlsClientOptions, toTlsServerOptions, validatePinnedCert, validatePinnedDer };
159
+ //# sourceMappingURL=index.js.map
160
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/cert.ts","../src/server.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Structured TLS errors. Surfaced via `Result<T, E>` at package boundaries.\n */\n\nexport type TlsErrorCode =\n | 'invalid_input'\n | 'cert_generation_failed'\n | 'fingerprint_mismatch'\n | 'parse_failed';\n\nexport interface TlsError {\n code: TlsErrorCode;\n message: string;\n cause?: unknown;\n}\n\nexport function tlsError(code: TlsErrorCode, message: string, cause?: unknown): TlsError {\n return { code, message, cause };\n}\n","import { createHash, webcrypto, X509Certificate } from 'node:crypto';\nimport * as x509 from '@peculiar/x509';\nimport { tlsError, type TlsError } from './errors.js';\n\n// `@peculiar/x509` accepts any Web Crypto provider; Node's `webcrypto` is\n// structurally compatible but the types diverge. Cast narrows to the shape\n// the library needs.\nx509.cryptoProvider.set(webcrypto as unknown as Parameters<typeof x509.cryptoProvider.set>[0]);\n\n/** 10 years in ms, matching Phase 2 Task 2 §1. */\nexport const CERT_VALIDITY_MS = 10 * 365 * 24 * 60 * 60 * 1000;\n\nexport interface GeneratedCert {\n /** PEM-encoded X.509 certificate. */\n certPem: string;\n /** PEM-encoded PKCS#8 Ed25519 private key. */\n keyPem: string;\n /** SHA-256 of DER bytes, lowercase hex (no `sha256:` prefix). */\n fingerprint: string;\n}\n\nexport interface GenerateAgentCertOptions {\n /** Agent DID used for the cert Common Name, e.g. `did:web:samantha.agent`. */\n did: string;\n /**\n * Multibase-encoded Ed25519 public key. Currently unused in v0 — the cert\n * is generated from a freshly-minted key pair, as Node's Web Crypto does\n * not ingest raw multibase keys directly. Retained on the signature for\n * forward compatibility with `did:web` key-reuse workflows (Phase 3+).\n */\n publicKeyMultibase?: string;\n /** Override SAN. Defaults to the host parsed out of the DID. */\n sanHostname?: string;\n /** Override validity period (ms). Defaults to 10 years. */\n validityMs?: number;\n /** Clock injection for tests. */\n now?: () => Date;\n /** Serial number override (hex). Random when omitted. */\n serialNumber?: string;\n}\n\n/**\n * Generate a self-signed Ed25519 X.509 certificate pinned to an agent DID.\n *\n * - `CN` = the agent DID\n * - `subjectAltName` = the DNS host parsed out of `did:web:<host>` (or\n * `options.sanHostname`)\n * - validity = 10 years by default\n */\nexport async function generateAgentCert(\n opts: GenerateAgentCertOptions,\n): Promise<{ ok: true; value: GeneratedCert } | { ok: false; error: TlsError }> {\n const host = opts.sanHostname ?? extractHostFromDid(opts.did);\n if (!host) {\n return {\n ok: false,\n error: tlsError('invalid_input', `cannot derive SAN hostname from ${opts.did}`),\n };\n }\n\n const now = opts.now?.() ?? new Date();\n const notAfter = new Date(now.getTime() + (opts.validityMs ?? CERT_VALIDITY_MS));\n const serial = opts.serialNumber ?? randomSerialHex();\n\n try {\n const keys = (await webcrypto.subtle.generateKey({ name: 'Ed25519' }, true, [\n 'sign',\n 'verify',\n ])) as { privateKey: webcrypto.CryptoKey; publicKey: webcrypto.CryptoKey };\n const cert = await x509.X509CertificateGenerator.createSelfSigned({\n name: `CN=${escapeDn(opts.did)}`,\n notBefore: now,\n notAfter,\n serialNumber: serial,\n signingAlgorithm: { name: 'Ed25519' },\n keys,\n extensions: [\n new x509.BasicConstraintsExtension(false, undefined, true),\n new x509.KeyUsagesExtension(\n x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyCertSign,\n true,\n ),\n new x509.ExtendedKeyUsageExtension(\n [x509.ExtendedKeyUsage.serverAuth, x509.ExtendedKeyUsage.clientAuth],\n true,\n ),\n new x509.SubjectAlternativeNameExtension([{ type: 'dns', value: host }]),\n ],\n });\n\n const pkcs8 = await webcrypto.subtle.exportKey('pkcs8', keys.privateKey);\n const certPem = cert.toString('pem');\n const keyPem = derToPem(new Uint8Array(pkcs8), 'PRIVATE KEY');\n const fingerprint = sha256HexOfDer(cert.rawData);\n return { ok: true, value: { certPem, keyPem, fingerprint } };\n } catch (err) {\n return {\n ok: false,\n error: tlsError('cert_generation_failed', 'failed to generate ed25519 cert', err),\n };\n }\n}\n\n/**\n * SHA-256 of DER bytes, lowercase hex. Strips PEM headers if a PEM string\n * is passed in.\n */\nexport function computeFingerprint(certPemOrDer: string | ArrayBuffer | Uint8Array): string {\n if (typeof certPemOrDer === 'string') {\n const der = pemToDer(certPemOrDer);\n return sha256HexOfDer(der);\n }\n return sha256HexOfDer(certPemOrDer);\n}\n\n/**\n * Validate a peer certificate (PEM) against an expected fingerprint. Constant-\n * time hex compare.\n */\nexport function validatePinnedCert(\n peerCertPem: string,\n expectedFingerprint: string,\n): boolean {\n let actual: string;\n try {\n actual = computeFingerprint(peerCertPem);\n } catch {\n return false;\n }\n const expected = normalizeFingerprint(expectedFingerprint);\n if (actual.length !== expected.length) return false;\n let mismatch = 0;\n for (let i = 0; i < actual.length; i++) {\n mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\n/**\n * Validate a peer cert in DER form (as returned by Node `tls.TLSSocket`\n * `getPeerX509Certificate().raw`).\n */\nexport function validatePinnedDer(\n peerDer: ArrayBuffer | Uint8Array,\n expectedFingerprint: string,\n): boolean {\n const actual = sha256HexOfDer(peerDer);\n const expected = normalizeFingerprint(expectedFingerprint);\n if (actual.length !== expected.length) return false;\n let mismatch = 0;\n for (let i = 0; i < actual.length; i++) {\n mismatch |= actual.charCodeAt(i) ^ expected.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\nexport function parseCertificatePem(pem: string): X509Certificate {\n return new X509Certificate(pem);\n}\n\nexport function extractHostFromDid(did: string): string | null {\n if (!did.startsWith('did:web:')) return null;\n const body = did.slice('did:web:'.length);\n const first = body.split(':')[0];\n return first ? decodeURIComponent(first) : null;\n}\n\nfunction sha256HexOfDer(der: ArrayBuffer | Uint8Array): string {\n const buf = der instanceof Uint8Array ? der : new Uint8Array(der);\n return createHash('sha256').update(buf).digest('hex');\n}\n\nfunction pemToDer(pem: string): Uint8Array {\n const match = pem.match(/-----BEGIN [^-]+-----([\\s\\S]+?)-----END [^-]+-----/);\n if (!match || !match[1]) {\n throw new Error('invalid PEM input');\n }\n const b64 = match[1].replace(/\\s+/g, '');\n return new Uint8Array(Buffer.from(b64, 'base64'));\n}\n\nfunction derToPem(der: Uint8Array, label: string): string {\n const b64 = Buffer.from(der).toString('base64');\n const wrapped = b64.replace(/(.{64})/g, '$1\\n').replace(/\\n$/, '');\n return `-----BEGIN ${label}-----\\n${wrapped}\\n-----END ${label}-----\\n`;\n}\n\nfunction normalizeFingerprint(value: string): string {\n return value.trim().toLowerCase().replace(/^sha-?256:/, '').replace(/:/g, '');\n}\n\nfunction randomSerialHex(): string {\n const bytes = new Uint8Array(16);\n webcrypto.getRandomValues(bytes);\n // Serial must be positive. Force MSB=0 to keep it unambiguously positive.\n if (bytes[0] !== undefined) {\n bytes[0] = bytes[0] & 0x7f;\n }\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\nfunction escapeDn(value: string): string {\n // RFC 4514 special chars: , + \" \\ < > ; # (leading) and spaces (leading/trailing)\n return value.replace(/([,+\"\\\\<>;#=])/g, '\\\\$1');\n}\n","import type { SecureContextOptions } from 'node:tls';\nimport type { GeneratedCert } from './cert.js';\n\n/**\n * Build the subset of TLS options Node's `createSecureContext` /\n * `tls.createServer` need to serve a DID-pinned self-signed cert.\n *\n * Usage:\n * const srv = https.createServer(toTlsServerOptions(cert), app);\n * tls.createServer(toTlsServerOptions(cert), ...);\n */\nexport function toTlsServerOptions(cert: GeneratedCert): SecureContextOptions {\n return {\n cert: cert.certPem,\n key: cert.keyPem,\n };\n}\n\n/**\n * Minimum tls.connect options for a pinning-aware client. The caller is\n * responsible for verifying the peer's fingerprint in the `secureConnect`\n * handler (use `validatePinnedDer(sock.getPeerX509Certificate().raw, fp)`).\n * `rejectUnauthorized: false` is required — the ARP PKI is DID-pinned, not\n * CA-chained.\n *\n * `servername` defaults to `host` unless `host` is a literal IP (Node rejects\n * IP-valued SNI). Pass an explicit `servername` when connecting via IP — it\n * should be the DNS name the peer's cert SAN covers.\n */\nexport function toTlsClientOptions(expected: {\n host: string;\n port: number;\n servername?: string;\n}): {\n host: string;\n port: number;\n rejectUnauthorized: false;\n servername?: string;\n} {\n const sni = expected.servername ?? (isLiteralIp(expected.host) ? undefined : expected.host);\n return {\n host: expected.host,\n port: expected.port,\n rejectUnauthorized: false,\n ...(sni ? { servername: sni } : {}),\n };\n}\n\nfunction isLiteralIp(host: string): boolean {\n // Cheap check — good enough for excluding IPv4/IPv6 literals from SNI.\n return /^(?:\\d{1,3}\\.){3}\\d{1,3}$/.test(host) || host.includes(':');\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@kybernesis/arp-tls",
3
+ "version": "0.3.0",
4
+ "description": "ARP TLS — self-signed Ed25519 X.509 cert generation + DID-doc pinning + fingerprint validation.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/KybernesisAI/arp.git",
9
+ "directory": "packages/tls"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "dependencies": {
30
+ "@peculiar/x509": "^1.12.3"
31
+ },
32
+ "devDependencies": {},
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "test": "vitest run",
36
+ "typecheck": "tsc --noEmit",
37
+ "lint": "eslint src tests"
38
+ }
39
+ }