@bhooai/nexus-crypto 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @bhooai/nexus-crypto
2
+
3
+ RSA/ECDSA keypairs, self-signed X.509 certs, CSRs, and HTTPS/mTLS helpers.
4
+
5
+ ## Exports
6
+
7
+ - **keypair** — `generateKeyPair(type, { modulusLength, namedCurve })` →
8
+ `{ publicKey, privateKey, pem }`.
9
+ - **x509** — `createSelfSignedCertificate({ keyPair, commonName, organization, country })`.
10
+ - **csr** — `createCsr({ keyPair, commonName, organization, country })` via a
11
+ **hand-rolled DER/ASN.1 encoder** (`der.ts`). `node:crypto` can't create CSRs, so
12
+ this is built from scratch (no `node-forge`).
13
+ - **der** — the minimal ASN.1/DER encoder underpinning the CSR.
14
+ - **https** — helpers for HTTPS servers and mTLS.
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { generateKeyPair, createSelfSignedCertificate, createCsr } from '@bhooai/nexus-crypto';
20
+ const kp = generateKeyPair('rsa', { modulusLength: 2048 });
21
+ const cert = createSelfSignedCertificate({ keyPair: kp, commonName: 'localhost' });
22
+ const csr = createCsr({ keyPair: kp, commonName: 'localhost' });
23
+ ```
24
+
25
+ The backend exposes this at `POST /certs/self-signed` (with optional `csr: true`).
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@bhooai/nexus-crypto",
3
+ "version": "0.1.0",
4
+ "publishConfig": { "access": "public" },
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "test": "vitest run"
11
+ },
12
+ "dependencies": {},
13
+ "devDependencies": {
14
+ "@types/node": "^22.5.0",
15
+ "typescript": "^5.6.2",
16
+ "vitest": "^2.1.1"
17
+ }
18
+ }
package/src/csr.ts ADDED
@@ -0,0 +1,165 @@
1
+ import crypto from 'node:crypto';
2
+ import {
3
+ SEQUENCE,
4
+ SET,
5
+ INTEGER,
6
+ BIT_STRING,
7
+ OID,
8
+ OCTET_STRING,
9
+ UTF8String,
10
+ BOOLEAN,
11
+ COMMON_NAME,
12
+ ORG,
13
+ COUNTRY,
14
+ CHALLENGE_PASSWORD,
15
+ EXTENSION_REQUEST,
16
+ PrintableString,
17
+ sha256WithRsaAlgId,
18
+ ecdsaP256AlgId,
19
+ oidFromString,
20
+ toBuffer,
21
+ type Byte,
22
+ } from './der.js';
23
+ import { pemEncode } from './x509.js';
24
+
25
+ export interface CsrExtension {
26
+ type: string;
27
+ value: Buffer;
28
+ critical?: boolean;
29
+ }
30
+
31
+ export interface CsrOptions {
32
+ keyPair: { publicKey: Buffer; privateKey: Buffer };
33
+ commonName: string;
34
+ organization?: string;
35
+ country?: string;
36
+ challengePassword?: string;
37
+ extensions?: CsrExtension[];
38
+ }
39
+
40
+ export interface CsrResult {
41
+ csr: Buffer;
42
+ pem: string;
43
+ }
44
+
45
+ /** Build a Name (RDNSequence) from subject components. */
46
+ function buildName(
47
+ commonName: string,
48
+ organization?: string,
49
+ country?: string,
50
+ ): Byte {
51
+ const rdns: Byte[] = [];
52
+ if (country) {
53
+ rdns.push(SET(SEQUENCE(COUNTRY, PrintableString(country))));
54
+ }
55
+ if (organization) {
56
+ rdns.push(SET(SEQUENCE(ORG, PrintableString(organization))));
57
+ }
58
+ rdns.push(SET(SEQUENCE(COMMON_NAME, PrintableString(commonName))));
59
+ return SEQUENCE(...rdns);
60
+ }
61
+
62
+ /** Build the attributes [0] IMPLICIT SET OF Attribute for the CRI. */
63
+ function buildAttributes(
64
+ challengePassword?: string,
65
+ extensions?: CsrExtension[],
66
+ ): Byte {
67
+ const attrs: Byte[] = [];
68
+
69
+ if (challengePassword !== undefined) {
70
+ // challengePassword attribute: SEQUENCE { OID, SET OF DirectoryString }
71
+ attrs.push(
72
+ SEQUENCE(
73
+ CHALLENGE_PASSWORD,
74
+ SET(UTF8String(challengePassword)),
75
+ ),
76
+ );
77
+ }
78
+
79
+ if (extensions && extensions.length > 0) {
80
+ const exts: Byte[] = extensions.map((ext) => {
81
+ const extOid = OID(oidFromString(ext.type));
82
+ const value = OCTET_STRING(Array.from(ext.value));
83
+ if (ext.critical) {
84
+ return SEQUENCE(extOid, BOOLEAN(true), value);
85
+ }
86
+ return SEQUENCE(extOid, value);
87
+ });
88
+ // extensionRequest attribute: SEQUENCE { OID(1.2.840.113549.1.9.14), SET OF Extensions }
89
+ // Extensions ::= SEQUENCE OF Extension
90
+ attrs.push(
91
+ SEQUENCE(
92
+ EXTENSION_REQUEST,
93
+ SET(SEQUENCE(...exts)),
94
+ ),
95
+ );
96
+ }
97
+
98
+ // [0] IMPLICIT SET OF Attribute — context tag 0xa0 (constructed), content is
99
+ // the concatenation of the Attribute SEQUENCEs (no extra SET wrapper, because
100
+ // the [0] IMPLICIT replaces the SET tag).
101
+ return tagConstructed(0xa0, ...attrs);
102
+ }
103
+
104
+ /** Construct a context-class constructed tag wrapping concatenated items. */
105
+ function tagConstructed(tagByte: number, ...items: Byte[]): Byte {
106
+ const flat: number[] = [];
107
+ for (const it of items) flat.push(...it);
108
+ const len = encodeLenLocal(flat.length);
109
+ return [tagByte, ...len, ...flat];
110
+ }
111
+
112
+ function encodeLenLocal(n: number): number[] {
113
+ if (n < 0x80) return [n];
114
+ const bytes: number[] = [];
115
+ let v = n;
116
+ while (v > 0) {
117
+ bytes.unshift(v & 0xff);
118
+ v = v >>> 8;
119
+ }
120
+ if (bytes.length === 0) bytes.push(0);
121
+ return [0x80 | bytes.length, ...bytes];
122
+ }
123
+
124
+ /**
125
+ * Create a PKCS#10 Certificate Signing Request.
126
+ *
127
+ * Builds CertificationRequestInfo DER by hand, signs it with the private key
128
+ * (SHA-256), and wraps it as SEQUENCE { CRI, signatureAlgorithm, signature }.
129
+ * No node-forge — only der.ts + node:crypto signing.
130
+ */
131
+ export function createCsr(opts: CsrOptions): CsrResult {
132
+ const privateKey = crypto.createPrivateKey({
133
+ key: opts.keyPair.privateKey,
134
+ format: 'der',
135
+ type: 'pkcs8',
136
+ });
137
+ const keyType = privateKey.asymmetricKeyType;
138
+ const isEc = keyType === 'ec';
139
+ const sigAlgId = isEc ? ecdsaP256AlgId() : sha256WithRsaAlgId();
140
+
141
+ const subject = buildName(opts.commonName, opts.organization, opts.country);
142
+ // publicKey buffer is already SPKI DER — embed verbatim.
143
+ const spki = Array.from(opts.keyPair.publicKey);
144
+ const attributes = buildAttributes(opts.challengePassword, opts.extensions);
145
+
146
+ const cri = SEQUENCE(
147
+ INTEGER(0), // version
148
+ subject,
149
+ spki,
150
+ attributes,
151
+ );
152
+
153
+ const criBuf = toBuffer(cri);
154
+ const signature = crypto.sign('sha256', criBuf, privateKey);
155
+
156
+ const csr = SEQUENCE(
157
+ cri,
158
+ sigAlgId,
159
+ BIT_STRING(Array.from(signature)),
160
+ );
161
+
162
+ const csrBuf = toBuffer(csr);
163
+ const pem = pemEncode(csrBuf, 'CERTIFICATE REQUEST');
164
+ return { csr: csrBuf, pem };
165
+ }
package/src/der.ts ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Minimal DER/ASN.1 encoder (hand-rolled, no external deps).
3
+ *
4
+ * Works in arrays of byte values (0-255) and flattens to a Buffer at the end.
5
+ * This is sufficient to encode X.509 TBSCertificates and PKCS#10 CSRs.
6
+ */
7
+
8
+ /** A working byte array. */
9
+ export type Byte = number[];
10
+
11
+ // --- Tag bytes for common ASN.1 types ---
12
+ const TAG_INTEGER = 0x02;
13
+ const TAG_BIT_STRING = 0x03;
14
+ const TAG_OCTET_STRING = 0x04;
15
+ const TAG_NULL = 0x05;
16
+ const TAG_BOOLEAN = 0x01;
17
+ const TAG_OID = 0x06;
18
+ const TAG_UTF8_STRING = 0x0c;
19
+ const TAG_SEQUENCE = 0x30;
20
+ const TAG_SET = 0x31;
21
+ const TAG_PRINTABLE_STRING = 0x13;
22
+ const TAG_IA5_STRING = 0x16;
23
+ const TAG_UTC_TIME = 0x17;
24
+ const TAG_GENERALIZED_TIME = 0x18;
25
+
26
+ /** DER length encoding: short form for <128, long form otherwise. */
27
+ export function encodeLen(n: number): number[] {
28
+ if (n < 0) {
29
+ throw new Error('DER length cannot be negative');
30
+ }
31
+ if (n < 0x80) {
32
+ return [n];
33
+ }
34
+ // Long form: 0x80 | number of length bytes, then big-endian length bytes.
35
+ const bytes: number[] = [];
36
+ let v = n;
37
+ while (v > 0) {
38
+ bytes.unshift(v & 0xff);
39
+ v = v >>> 8;
40
+ }
41
+ // Ensure we never produce a non-minimal long-form encoding (no leading 0).
42
+ if (bytes.length === 0) bytes.push(0);
43
+ if (bytes.length > 0x7f) {
44
+ throw new Error('DER length too large');
45
+ }
46
+ return [0x80 | bytes.length, ...bytes];
47
+ }
48
+
49
+ /** Wrap content with a tag byte and DER length. */
50
+ export function tag(tagByte: number, content: number[]): number[] {
51
+ return [tagByte, ...encodeLen(content.length), ...content];
52
+ }
53
+
54
+ /** INTEGER from a non-negative number or bigint (two's complement for negatives). */
55
+ export function INTEGER(n: number | bigint): number[] {
56
+ if (typeof n === 'bigint') {
57
+ return INTEGER_bigint(n);
58
+ }
59
+ if (!Number.isInteger(n)) {
60
+ throw new Error('INTEGER requires an integer');
61
+ }
62
+ return INTEGER_bigint(BigInt(n));
63
+ }
64
+
65
+ function INTEGER_bigint(n: bigint): number[] {
66
+ if (n === 0n) {
67
+ return tag(TAG_INTEGER, [0x00]);
68
+ }
69
+ const bytes: number[] = [];
70
+ const negative = n < 0n;
71
+ if (negative) {
72
+ // Two's complement of a negative bigint: we use a wide representation.
73
+ // Find the smallest byte width that holds the magnitude, then invert.
74
+ const magnitude = -n;
75
+ let bits = 0n;
76
+ let m = magnitude;
77
+ while (m > 0n) {
78
+ bits += 1n;
79
+ m >>= 1n;
80
+ }
81
+ const width = Number((bits + 7n) / 8n) + 1; // extra byte to hold sign bit
82
+ const mask = (1n << BigInt(width * 8)) - 1n;
83
+ const twos = ((~magnitude) + 1n) & mask;
84
+ let t = twos;
85
+ for (let i = 0; i < width; i++) {
86
+ bytes.unshift(Number(t & 0xffn));
87
+ t >>= 8n;
88
+ }
89
+ // Trim leading 0xff only if next bit is set, etc. Keep minimal but signed.
90
+ while (bytes.length > 1 && bytes[0] === 0xff && (bytes[1]! & 0x80) !== 0) {
91
+ bytes.shift();
92
+ }
93
+ return tag(TAG_INTEGER, bytes);
94
+ }
95
+ // Positive: encode big-endian, prepend 0x00 if high bit set (so it stays positive).
96
+ let v = n;
97
+ while (v > 0n) {
98
+ bytes.unshift(Number(v & 0xffn));
99
+ v >>= 8n;
100
+ }
101
+ if ((bytes[0]! & 0x80) !== 0) {
102
+ bytes.unshift(0x00);
103
+ }
104
+ return tag(TAG_INTEGER, bytes);
105
+ }
106
+
107
+ /** BIT STRING with a leading unused-bits byte (default 0). */
108
+ export function BIT_STRING(content: number[], unusedBits = 0): number[] {
109
+ return tag(TAG_BIT_STRING, [unusedBits, ...content]);
110
+ }
111
+
112
+ export function OCTET_STRING(content: number[]): number[] {
113
+ return tag(TAG_OCTET_STRING, content);
114
+ }
115
+
116
+ export function NULL(): number[] {
117
+ return tag(TAG_NULL, []);
118
+ }
119
+
120
+ /** BOOLEAN: 0x00 for false, 0xff for true. */
121
+ export function BOOLEAN(value: boolean): number[] {
122
+ return tag(TAG_BOOLEAN, [value ? 0xff : 0x00]);
123
+ }
124
+
125
+ /** Parse a dotted OID string (e.g. "1.2.840.113549.1.1.11") into an arc array. */
126
+ export function oidFromString(s: string): number[] {
127
+ return s.split('.').map((part) => {
128
+ const n = Number(part);
129
+ if (!Number.isInteger(n) || n < 0) {
130
+ throw new Error(`Invalid OID arc: ${part}`);
131
+ }
132
+ return n;
133
+ });
134
+ }
135
+
136
+ /** OID encoder: first two arcs packed as 40*a+b, rest base-128 with continuation. */
137
+ export function OID(arcs: number[]): number[] {
138
+ if (arcs.length < 2) {
139
+ throw new Error('OID needs at least 2 arcs');
140
+ }
141
+ const content: number[] = [];
142
+ content.push(40 * arcs[0]! + arcs[1]!);
143
+ for (let i = 2; i < arcs.length; i++) {
144
+ const arc = arcs[i]!;
145
+ if (arc < 0) throw new Error('OID arc cannot be negative');
146
+ if (arc === 0) {
147
+ content.push(0x00);
148
+ continue;
149
+ }
150
+ const buf: number[] = [];
151
+ let v = arc;
152
+ while (v > 0) {
153
+ buf.unshift(v & 0x7f);
154
+ v = v >>> 7;
155
+ }
156
+ for (let j = 0; j < buf.length; j++) {
157
+ if (j < buf.length - 1) {
158
+ content.push(buf[j]! | 0x80);
159
+ } else {
160
+ content.push(buf[j]!);
161
+ }
162
+ }
163
+ }
164
+ return tag(TAG_OID, content);
165
+ }
166
+
167
+ export function SEQUENCE(...items: Byte[]): Byte {
168
+ const flat: number[] = [];
169
+ for (const it of items) flat.push(...it);
170
+ return tag(TAG_SEQUENCE, flat);
171
+ }
172
+
173
+ export function SET(...items: Byte[]): Byte {
174
+ const flat: number[] = [];
175
+ for (const it of items) flat.push(...it);
176
+ return tag(TAG_SET, flat);
177
+ }
178
+
179
+ function asciiBytes(s: string): number[] {
180
+ const out: number[] = [];
181
+ for (let i = 0; i < s.length; i++) {
182
+ out.push(s.charCodeAt(i) & 0xff);
183
+ }
184
+ return out;
185
+ }
186
+
187
+ function pad2(n: number): string {
188
+ return n < 10 ? '0' + n : String(n);
189
+ }
190
+
191
+ /** UTCTime: YYMMDDHHMMSSZ (used for years 1950-2049). */
192
+ export function UTCTime(date: Date): number[] {
193
+ const yy = date.getUTCFullYear() % 100;
194
+ const s =
195
+ pad2(yy) +
196
+ pad2(date.getUTCMonth() + 1) +
197
+ pad2(date.getUTCDate()) +
198
+ pad2(date.getUTCHours()) +
199
+ pad2(date.getUTCMinutes()) +
200
+ pad2(date.getUTCSeconds()) +
201
+ 'Z';
202
+ return tag(TAG_UTC_TIME, asciiBytes(s));
203
+ }
204
+
205
+ /** GeneralizedTime: YYYYMMDDHHMMSSZ (used for years >= 2050). */
206
+ export function GeneralizedTime(date: Date): number[] {
207
+ const s =
208
+ String(date.getUTCFullYear()).padStart(4, '0') +
209
+ pad2(date.getUTCMonth() + 1) +
210
+ pad2(date.getUTCDate()) +
211
+ pad2(date.getUTCHours()) +
212
+ pad2(date.getUTCMinutes()) +
213
+ pad2(date.getUTCSeconds()) +
214
+ 'Z';
215
+ return tag(TAG_GENERALIZED_TIME, asciiBytes(s));
216
+ }
217
+
218
+ export function PrintableString(s: string): number[] {
219
+ return tag(TAG_PRINTABLE_STRING, asciiBytes(s));
220
+ }
221
+
222
+ export function UTF8String(s: string): number[] {
223
+ return tag(TAG_UTF8_STRING, Array.from(Buffer.from(s, 'utf8')));
224
+ }
225
+
226
+ export function IA5String(s: string): number[] {
227
+ return tag(TAG_IA5_STRING, asciiBytes(s));
228
+ }
229
+
230
+ // --- Common OID constants (already tagged, ready to embed) ---
231
+ export const RSA_ENCRYPTION = OID([1, 2, 840, 113549, 1, 1, 1]);
232
+ export const SHA256_WITH_RSA = OID([1, 2, 840, 113549, 1, 1, 11]);
233
+ export const EC_PUBLIC_KEY = OID([1, 2, 840, 10045, 2, 1]);
234
+ export const ECDSA_P256 = OID([1, 2, 840, 10045, 4, 3, 2]);
235
+ export const PRIME256V1 = OID([1, 2, 840, 10045, 3, 1, 7]);
236
+ export const COMMON_NAME = OID([2, 5, 4, 3]);
237
+ export const ORG = OID([2, 5, 4, 10]);
238
+ export const COUNTRY = OID([2, 5, 4, 6]);
239
+ export const EXTENSION_REQUEST = OID([1, 2, 840, 113549, 1, 9, 14]);
240
+ export const CHALLENGE_PASSWORD = OID([1, 2, 840, 113549, 1, 9, 7]);
241
+
242
+ /** AlgorithmIdentifier SEQUENCE { OID, NULL } for RSA-SHA256. */
243
+ export function sha256WithRsaAlgId(): number[] {
244
+ return SEQUENCE(SHA256_WITH_RSA, NULL());
245
+ }
246
+
247
+ /** AlgorithmIdentifier SEQUENCE { OID, curveName OID } for ECDSA P-256. */
248
+ export function ecdsaP256AlgId(): number[] {
249
+ return SEQUENCE(ECDSA_P256);
250
+ }
251
+
252
+ /** Flatten a byte array into a Buffer. */
253
+ export function toBuffer(bytes: number[]): Buffer {
254
+ return Buffer.from(bytes);
255
+ }
256
+
257
+ /** Debug helper: render a byte array as hex. */
258
+ export function hex(bytes: number[]): string {
259
+ return bytes
260
+ .map((b) => b.toString(16).padStart(2, '0'))
261
+ .join(' ');
262
+ }
package/src/https.ts ADDED
@@ -0,0 +1,36 @@
1
+ import https from 'node:https';
2
+
3
+ export interface HttpsAgentOptions {
4
+ cert: Buffer;
5
+ key: Buffer;
6
+ ca?: Buffer;
7
+ rejectUnauthorized?: boolean;
8
+ }
9
+
10
+ /** Create an https.Agent suitable for mutual TLS (client-side). */
11
+ export function createHttpsAgent(opts: HttpsAgentOptions): https.Agent {
12
+ return new https.Agent({
13
+ cert: opts.cert,
14
+ key: opts.key,
15
+ ca: opts.ca,
16
+ rejectUnauthorized: opts.rejectUnauthorized ?? true,
17
+ });
18
+ }
19
+
20
+ export interface TlsConfigOptions {
21
+ cert: Buffer;
22
+ key: Buffer;
23
+ ca?: Buffer;
24
+ }
25
+
26
+ /** Return the { cert, key, ca } object for use with https.createServer. */
27
+ export function createTlsConfig(opts: TlsConfigOptions): {
28
+ cert: Buffer;
29
+ key: Buffer;
30
+ ca?: Buffer;
31
+ } {
32
+ if (opts.ca !== undefined) {
33
+ return { cert: opts.cert, key: opts.key, ca: opts.ca };
34
+ }
35
+ return { cert: opts.cert, key: opts.key };
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './der.js';
2
+ export * from './keypair.js';
3
+ export * from './x509.js';
4
+ export * from './csr.js';
5
+ export * from './https.js';
package/src/keypair.ts ADDED
@@ -0,0 +1,59 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export interface KeyPairResult {
4
+ publicKey: Buffer;
5
+ privateKey: Buffer;
6
+ pem: { public: string; private: string };
7
+ }
8
+
9
+ export interface KeyPairOptions {
10
+ modulusLength?: number;
11
+ namedCurve?: string;
12
+ }
13
+
14
+ /**
15
+ * Generate an RSA or EC keypair.
16
+ *
17
+ * Returns DER buffers (spki public, pkcs8 private) and PEM strings.
18
+ * RSA defaults to 2048-bit; EC defaults to prime256v1 (P-256).
19
+ */
20
+ export function generateKeyPair(
21
+ type: 'rsa' | 'ec',
22
+ opts: KeyPairOptions = {},
23
+ ): KeyPairResult {
24
+ let publicKey: crypto.KeyObject;
25
+ let privateKey: crypto.KeyObject;
26
+ if (type === 'rsa') {
27
+ ({ publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
28
+ modulusLength: opts.modulusLength ?? 2048,
29
+ }));
30
+ } else if (type === 'ec') {
31
+ ({ publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
32
+ namedCurve: opts.namedCurve ?? 'prime256v1',
33
+ }));
34
+ } else {
35
+ throw new Error(`Unsupported key type: ${type}`);
36
+ }
37
+
38
+ const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' }) as Buffer;
39
+ const privateKeyDer = privateKey.export({ type: 'pkcs8', format: 'der' }) as Buffer;
40
+
41
+ return {
42
+ publicKey: publicKeyDer,
43
+ privateKey: privateKeyDer,
44
+ pem: {
45
+ public: exportPublicKeyPem(publicKey),
46
+ private: exportPrivateKeyPem(privateKey),
47
+ },
48
+ };
49
+ }
50
+
51
+ /** Export a KeyObject as a SPKI PEM string. */
52
+ export function exportPublicKeyPem(key: crypto.KeyObject): string {
53
+ return key.export({ type: 'spki', format: 'pem' }).toString('utf8');
54
+ }
55
+
56
+ /** Export a KeyObject as a PKCS#8 PEM string. */
57
+ export function exportPrivateKeyPem(key: crypto.KeyObject): string {
58
+ return key.export({ type: 'pkcs8', format: 'pem' }).toString('utf8');
59
+ }
package/src/x509.ts ADDED
@@ -0,0 +1,156 @@
1
+ import crypto from 'node:crypto';
2
+ import {
3
+ SEQUENCE,
4
+ INTEGER,
5
+ BIT_STRING,
6
+ UTCTime,
7
+ GeneralizedTime,
8
+ OID,
9
+ COMMON_NAME,
10
+ ORG,
11
+ COUNTRY,
12
+ PrintableString,
13
+ NULL,
14
+ sha256WithRsaAlgId,
15
+ ecdsaP256AlgId,
16
+ toBuffer,
17
+ SET,
18
+ type Byte,
19
+ } from './der.js';
20
+
21
+ export interface X509Options {
22
+ keyPair: { publicKey: Buffer; privateKey: Buffer };
23
+ commonName: string;
24
+ organization?: string;
25
+ country?: string;
26
+ notBefore?: Date;
27
+ notAfter?: Date;
28
+ serial?: number;
29
+ }
30
+
31
+ export interface X509Result {
32
+ cert: Buffer;
33
+ pem: string;
34
+ }
35
+
36
+ /** Build a Name (RDNSequence) from subject components. */
37
+ function buildName(
38
+ commonName: string,
39
+ organization?: string,
40
+ country?: string,
41
+ ): Byte {
42
+ const rdns: Byte[] = [];
43
+ if (country) {
44
+ rdns.push(SET(SEQUENCE(COUNTRY, PrintableString(country))));
45
+ }
46
+ if (organization) {
47
+ rdns.push(SET(SEQUENCE(ORG, PrintableString(organization))));
48
+ }
49
+ rdns.push(SET(SEQUENCE(COMMON_NAME, PrintableString(commonName))));
50
+ return SEQUENCE(...rdns);
51
+ }
52
+
53
+ /** Pick Time encoding: UTCTime for 1950-2049, GeneralizedTime otherwise. */
54
+ function timeEncode(date: Date): Byte {
55
+ const year = date.getUTCFullYear();
56
+ if (year >= 1950 && year <= 2049) {
57
+ return UTCTime(date);
58
+ }
59
+ return GeneralizedTime(date);
60
+ }
61
+
62
+ /**
63
+ * Create a self-signed X.509 v3 certificate.
64
+ *
65
+ * Builds TBSCertificate DER by hand, signs it with the private key (SHA-256),
66
+ * and wraps it as SEQUENCE { tbs, signatureAlgorithm, signature }.
67
+ * No extensions are added (a v3 cert with no extensions is still valid DER).
68
+ */
69
+ export function createSelfSignedCertificate(opts: X509Options): X509Result {
70
+ const privateKey = crypto.createPrivateKey({
71
+ key: opts.keyPair.privateKey,
72
+ format: 'der',
73
+ type: 'pkcs8',
74
+ });
75
+ const keyType = privateKey.asymmetricKeyType;
76
+ const isEc = keyType === 'ec';
77
+
78
+ const sigAlgId = isEc ? ecdsaP256AlgId() : sha256WithRsaAlgId();
79
+ const signAlg = isEc ? 'sha256' : 'sha256';
80
+
81
+ const notBefore = opts.notBefore ?? new Date();
82
+ const notAfter =
83
+ opts.notAfter ?? new Date(notBefore.getTime() + 365 * 24 * 60 * 60 * 1000);
84
+ const serial = opts.serial ?? 1;
85
+
86
+ // version [0] EXPLICIT INTEGER(2) → v3
87
+ const version = tagExplicit(0, INTEGER(2));
88
+
89
+ const issuer = buildName(opts.commonName, opts.organization, opts.country);
90
+ const subject = buildName(opts.commonName, opts.organization, opts.country);
91
+ const validity = SEQUENCE(timeEncode(notBefore), timeEncode(notAfter));
92
+
93
+ // The publicKey buffer is already a SPKI DER (from generateKeyPairSync with
94
+ // type 'spki', format 'der') — embed it verbatim.
95
+ const spki = Array.from(opts.keyPair.publicKey);
96
+
97
+ const tbs = SEQUENCE(
98
+ version,
99
+ INTEGER(serial),
100
+ sigAlgId,
101
+ issuer,
102
+ validity,
103
+ subject,
104
+ spki,
105
+ );
106
+
107
+ const tbsBuf = toBuffer(tbs);
108
+ const signature = crypto.sign(signAlg, tbsBuf, privateKey);
109
+
110
+ const cert = SEQUENCE(
111
+ tbs,
112
+ sigAlgId,
113
+ BIT_STRING(Array.from(signature)),
114
+ );
115
+
116
+ const certBuf = toBuffer(cert);
117
+ const pem = pemEncode(certBuf, 'CERTIFICATE');
118
+ return { cert: certBuf, pem };
119
+ }
120
+
121
+ /** IMPLICIT/EXPLICIT context tag helper. EXPLICIT wraps content in another TLV. */
122
+ function tagExplicit(tagNumber: number, content: Byte): Byte {
123
+ const tagByte = 0xa0 | (tagNumber & 0x1f);
124
+ return tag(tagByte, content);
125
+ }
126
+
127
+ function tag(tagByte: number, content: Byte): Byte {
128
+ // Local copy to avoid exporting a private helper; matches der.tag semantics.
129
+ const len = encodeLenLocal(content.length);
130
+ return [tagByte, ...len, ...content];
131
+ }
132
+
133
+ function encodeLenLocal(n: number): number[] {
134
+ if (n < 0x80) return [n];
135
+ const bytes: number[] = [];
136
+ let v = n;
137
+ while (v > 0) {
138
+ bytes.unshift(v & 0xff);
139
+ v = v >>> 8;
140
+ }
141
+ if (bytes.length === 0) bytes.push(0);
142
+ return [0x80 | bytes.length, ...bytes];
143
+ }
144
+
145
+ /** Wrap a DER buffer as a PEM string. */
146
+ export function pemEncode(der: Buffer, label: string): string {
147
+ const b64 = der.toString('base64');
148
+ const lines: string[] = [];
149
+ for (let i = 0; i < b64.length; i += 64) {
150
+ lines.push(b64.slice(i, i + 64));
151
+ }
152
+ return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`;
153
+ }
154
+
155
+ // Re-export for index convenience.
156
+ export { OID, NULL };
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import crypto from 'node:crypto';
3
+ import {
4
+ generateKeyPair,
5
+ createSelfSignedCertificate,
6
+ createCsr,
7
+ createTlsConfig,
8
+ encodeLen,
9
+ OID,
10
+ SEQUENCE,
11
+ NULL,
12
+ toBuffer,
13
+ } from '../src/index.js';
14
+
15
+ /** Node's X509Certificate has no `subjectCN` accessor; derive it from subject. */
16
+ function subjectCN(x509: crypto.X509Certificate): string | undefined {
17
+ const subject = x509.subject;
18
+ const match = subject.match(/(?:^|\n)CN=([^\n]+)/);
19
+ return match ? match[1] : undefined;
20
+ }
21
+
22
+ // --- Tiny DER reader helpers for re-parsing our own CSR DER in the test ---
23
+ function readLen(buf: Buffer, offset: number): { len: number; next: number } {
24
+ const first = buf[offset]!;
25
+ if (first < 0x80) {
26
+ return { len: first, next: offset + 1 };
27
+ }
28
+ const numBytes = first & 0x7f;
29
+ let len = 0;
30
+ let o = offset + 1;
31
+ for (let i = 0; i < numBytes; i++) {
32
+ len = (len << 8) | buf[o]!;
33
+ o++;
34
+ }
35
+ return { len, next: o };
36
+ }
37
+
38
+ /** Read one TLV; returns { tag, content, rest } where content is the V bytes. */
39
+ function readTlv(buf: Buffer, offset: number): {
40
+ tag: number;
41
+ content: Buffer;
42
+ rest: Buffer;
43
+ } {
44
+ const tag = buf[offset]!;
45
+ const { len, next } = readLen(buf, offset + 1);
46
+ const content = buf.subarray(next, next + len);
47
+ const rest = buf.subarray(next + len);
48
+ return { tag, content, rest };
49
+ }
50
+
51
+ /** Split a constructed SEQUENCE's content into its child TLV byte slices. */
52
+ function splitChildren(content: Buffer): Buffer[] {
53
+ const children: Buffer[] = [];
54
+ let o = 0;
55
+ while (o < content.length) {
56
+ const { len, next } = readLen(content, o + 1);
57
+ const fullLen = next + len - o;
58
+ children.push(content.subarray(o, o + fullLen));
59
+ o += fullLen;
60
+ }
61
+ return children;
62
+ }
63
+
64
+ describe('nexus-crypto', () => {
65
+ it('generateKeyPair rsa produces valid PEM and DER buffers', () => {
66
+ const kp = generateKeyPair('rsa');
67
+ expect(kp.publicKey.length).toBeGreaterThan(0);
68
+ expect(kp.privateKey.length).toBeGreaterThan(0);
69
+ expect(kp.pem.public.startsWith('-----BEGIN PUBLIC KEY-----')).toBe(true);
70
+ expect(kp.pem.private.startsWith('-----BEGIN PRIVATE KEY-----')).toBe(true);
71
+ // createPublicKey accepts the PEM.
72
+ const pub = crypto.createPublicKey(kp.pem.public);
73
+ expect(pub.asymmetricKeyType).toBe('rsa');
74
+ });
75
+
76
+ it('generateKeyPair ec (prime256v1) produces valid PEM and DER buffers', () => {
77
+ const kp = generateKeyPair('ec');
78
+ expect(kp.publicKey.length).toBeGreaterThan(0);
79
+ expect(kp.privateKey.length).toBeGreaterThan(0);
80
+ expect(kp.pem.public.startsWith('-----BEGIN PUBLIC KEY-----')).toBe(true);
81
+ expect(kp.pem.private.startsWith('-----BEGIN PRIVATE KEY-----')).toBe(true);
82
+ const pub = crypto.createPublicKey(kp.pem.public);
83
+ expect(pub.asymmetricKeyType).toBe('ec');
84
+ });
85
+
86
+ it('createSelfSignedCertificate rsa parses and self-verifies', () => {
87
+ const kp = generateKeyPair('rsa');
88
+ const { pem } = createSelfSignedCertificate({
89
+ keyPair: kp,
90
+ commonName: 'test.rsa.example',
91
+ organization: 'BhooAI',
92
+ country: 'IN',
93
+ });
94
+ expect(pem.startsWith('-----BEGIN CERTIFICATE-----')).toBe(true);
95
+ const x509 = new crypto.X509Certificate(pem);
96
+ expect(subjectCN(x509)).toBe('test.rsa.example');
97
+ expect(x509.validFrom).toBeDefined();
98
+ expect(x509.validTo).toBeDefined();
99
+ // Self-signed: verifies against its own public key.
100
+ const pub = crypto.createPublicKey(kp.pem.public);
101
+ expect(x509.verify(pub)).toBe(true);
102
+ });
103
+
104
+ it('createSelfSignedCertificate ec parses and self-verifies', () => {
105
+ const kp = generateKeyPair('ec');
106
+ const { pem } = createSelfSignedCertificate({
107
+ keyPair: kp,
108
+ commonName: 'test.ec.example',
109
+ });
110
+ expect(pem.startsWith('-----BEGIN CERTIFICATE-----')).toBe(true);
111
+ const x509 = new crypto.X509Certificate(pem);
112
+ expect(subjectCN(x509)).toBe('test.ec.example');
113
+ const pub = crypto.createPublicKey(kp.pem.public);
114
+ expect(x509.verify(pub)).toBe(true);
115
+ });
116
+
117
+ it('createCsr rsa produces a verifiable CSR', () => {
118
+ const kp = generateKeyPair('rsa');
119
+ const { csr, pem } = createCsr({
120
+ keyPair: kp,
121
+ commonName: 'csr.rsa.example',
122
+ });
123
+ expect(pem.startsWith('-----BEGIN CERTIFICATE REQUEST-----')).toBe(true);
124
+ // Outer SEQUENCE: tag 0x30.
125
+ const outer = readTlv(csr, 0);
126
+ expect(outer.tag).toBe(0x30);
127
+ // Children: CRI SEQUENCE, sigAlg SEQUENCE, signature BIT STRING.
128
+ const kids = splitChildren(outer.content);
129
+ expect(kids.length).toBe(3);
130
+ expect(kids[0]![0]).toBe(0x30); // CRI
131
+ expect(kids[1]![0]).toBe(0x30); // sigAlg
132
+ expect(kids[2]![0]).toBe(0x03); // BIT STRING
133
+
134
+ const criBytes = kids[0]!;
135
+ const sigTlv = readTlv(kids[2]!, 0);
136
+ // BIT STRING content: first byte is unused-bits count (0), rest is signature.
137
+ const signature = sigTlv.content.subarray(1);
138
+
139
+ const verify = crypto.createVerify('SHA256').update(criBytes);
140
+ expect(verify.verify(kp.pem.public, signature)).toBe(true);
141
+ });
142
+
143
+ it('createCsr with challengePassword + extension still verifies', () => {
144
+ const kp = generateKeyPair('rsa');
145
+ const extValue = Buffer.from('hello-extension');
146
+ const { csr, pem } = createCsr({
147
+ keyPair: kp,
148
+ commonName: 'csr.ext.example',
149
+ challengePassword: 'secret-pass',
150
+ extensions: [{ type: '1.2.3.4.5.6.7', value: extValue }],
151
+ });
152
+ expect(pem.startsWith('-----BEGIN CERTIFICATE REQUEST-----')).toBe(true);
153
+ const outer = readTlv(csr, 0);
154
+ const kids = splitChildren(outer.content);
155
+ expect(kids.length).toBe(3);
156
+ const verify = crypto
157
+ .createVerify('SHA256')
158
+ .update(kids[0]!)
159
+ .verify(kp.pem.public, readTlv(kids[2]!, 0).content.subarray(1));
160
+ expect(verify).toBe(true);
161
+ });
162
+
163
+ it('createCsr ec verifies with EC key', () => {
164
+ const kp = generateKeyPair('ec');
165
+ const { csr, pem } = createCsr({
166
+ keyPair: kp,
167
+ commonName: 'csr.ec.example',
168
+ });
169
+ expect(pem.startsWith('-----BEGIN CERTIFICATE REQUEST-----')).toBe(true);
170
+ const outer = readTlv(csr, 0);
171
+ const kids = splitChildren(outer.content);
172
+ expect(kids.length).toBe(3);
173
+ const verify = crypto
174
+ .createVerify('SHA256')
175
+ .update(kids[0]!)
176
+ .verify(kp.pem.public, readTlv(kids[2]!, 0).content.subarray(1));
177
+ expect(verify).toBe(true);
178
+ });
179
+
180
+ it('DER unit tests: encodeLen, OID, SEQUENCE(NULL())', () => {
181
+ expect(encodeLen(0)).toEqual([0x00]);
182
+ expect(encodeLen(127)).toEqual([0x7f]);
183
+ expect(encodeLen(128)).toEqual([0x81, 0x80]);
184
+
185
+ const oid = OID([1, 2, 840, 113549, 1, 1, 11]);
186
+ // 0x06 (OID tag), length 0x09, then the standard sha256WithRSAEncryption arcs.
187
+ expect(oid.slice(0, 2)).toEqual([0x06, 0x09]);
188
+ expect(oid.slice(2)).toEqual([
189
+ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b,
190
+ ]);
191
+
192
+ const seq = SEQUENCE(NULL());
193
+ expect(seq[0]).toBe(0x30);
194
+ // SEQUENCE length 2 (NULL is 0x05 0x00).
195
+ expect(seq[1]).toBe(0x02);
196
+ expect(seq.slice(2)).toEqual([0x05, 0x00]);
197
+ // Round-trip through a Buffer.
198
+ expect(toBuffer(seq)).toEqual(Buffer.from([0x30, 0x02, 0x05, 0x00]));
199
+ });
200
+
201
+ it('createTlsConfig returns the cert/key object', () => {
202
+ const cert = Buffer.from('cert');
203
+ const key = Buffer.from('key');
204
+ const cfg = createTlsConfig({ cert, key });
205
+ expect(cfg.cert).toBe(cert);
206
+ expect(cfg.key).toBe(key);
207
+ expect('ca' in cfg).toBe(false);
208
+
209
+ const ca = Buffer.from('ca');
210
+ const cfg2 = createTlsConfig({ cert, key, ca });
211
+ expect(cfg2.ca).toBe(ca);
212
+ });
213
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "references": []
9
+ }
@@ -0,0 +1,10 @@
1
+ import { defineProject } from 'vitest/config';
2
+
3
+ export default defineProject({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['tests/**/*.test.ts'],
7
+ globals: false,
8
+ testTimeout: 15_000,
9
+ },
10
+ });
@@ -0,0 +1,14 @@
1
+ // packages/nexus-crypto/vitest.config.ts
2
+ import { defineProject } from "file:///C:/server/BhooAI/BhooAI-Nexus/BhooAI-Nexus/bhooai-nexus/node_modules/vitest/dist/config.js";
3
+ var vitest_config_default = defineProject({
4
+ test: {
5
+ environment: "node",
6
+ include: ["tests/**/*.test.ts"],
7
+ globals: false,
8
+ testTimeout: 15e3
9
+ }
10
+ });
11
+ export {
12
+ vitest_config_default as default
13
+ };
14
+ //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvbmV4dXMtY3J5cHRvL3ZpdGVzdC5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxzZXJ2ZXJcXFxcQmhvb0FJXFxcXEJob29BSS1OZXh1c1xcXFxCaG9vQUktTmV4dXNcXFxcYmhvb2FpLW5leHVzXFxcXHBhY2thZ2VzXFxcXG5leHVzLWNyeXB0b1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxcc2VydmVyXFxcXEJob29BSVxcXFxCaG9vQUktTmV4dXNcXFxcQmhvb0FJLU5leHVzXFxcXGJob29haS1uZXh1c1xcXFxwYWNrYWdlc1xcXFxuZXh1cy1jcnlwdG9cXFxcdml0ZXN0LmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vQzovc2VydmVyL0Job29BSS9CaG9vQUktTmV4dXMvQmhvb0FJLU5leHVzL2Job29haS1uZXh1cy9wYWNrYWdlcy9uZXh1cy1jcnlwdG8vdml0ZXN0LmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZVByb2plY3QgfSBmcm9tICd2aXRlc3QvY29uZmlnJztcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lUHJvamVjdCh7XG4gIHRlc3Q6IHtcbiAgICBlbnZpcm9ubWVudDogJ25vZGUnLFxuICAgIGluY2x1ZGU6IFsndGVzdHMvKiovKi50ZXN0LnRzJ10sXG4gICAgZ2xvYmFsczogZmFsc2UsXG4gICAgdGVzdFRpbWVvdXQ6IDE1XzAwMCxcbiAgfSxcbn0pOyJdLAogICJtYXBwaW5ncyI6ICI7QUFBNmEsU0FBUyxxQkFBcUI7QUFFM2MsSUFBTyx3QkFBUSxjQUFjO0FBQUEsRUFDM0IsTUFBTTtBQUFBLElBQ0osYUFBYTtBQUFBLElBQ2IsU0FBUyxDQUFDLG9CQUFvQjtBQUFBLElBQzlCLFNBQVM7QUFBQSxJQUNULGFBQWE7QUFBQSxFQUNmO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K