index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. const { X509CertificateGenerator, X509Certificate, cryptoProvider, X509ChainBuilder, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags, ExtendedKeyUsageExtension, ExtendedKeyUsage, SubjectAlternativeNameExtension, GeneralName } = require("@peculiar/x509");
  2. const nodeCrypto = require("crypto");
  3. // Use Node.js native webcrypto
  4. const crypto = nodeCrypto.webcrypto;
  5. // Patch global CryptoProvider to use Node.js crypto
  6. cryptoProvider.set(crypto);
  7. // a hexString is considered negative if it's most significant bit is 1
  8. // because serial numbers use ones' complement notation
  9. // this RFC in section 4.1.2.2 requires serial numbers to be positive
  10. // http://www.ietf.org/rfc/rfc5280.txt
  11. function toPositiveHex(hexString) {
  12. var mostSiginficativeHexAsInt = parseInt(hexString[0], 16);
  13. if (mostSiginficativeHexAsInt < 8) {
  14. return hexString;
  15. }
  16. mostSiginficativeHexAsInt -= 8;
  17. return mostSiginficativeHexAsInt.toString() + hexString.substring(1);
  18. }
  19. function getAlgorithmName(key) {
  20. switch (key) {
  21. case "sha256":
  22. return "SHA-256";
  23. case 'sha384':
  24. return "SHA-384";
  25. case 'sha512':
  26. return "SHA-512";
  27. default:
  28. return "SHA-1";
  29. }
  30. }
  31. function getSigningAlgorithm(hashKey, keyType) {
  32. const hashAlg = getAlgorithmName(hashKey);
  33. if (keyType === 'ec') {
  34. return {
  35. name: "ECDSA",
  36. hash: hashAlg
  37. };
  38. }
  39. return {
  40. name: "RSASSA-PKCS1-v1_5",
  41. hash: hashAlg
  42. };
  43. }
  44. function getKeyAlgorithm(options) {
  45. const keyType = options.keyType || 'rsa';
  46. const hashAlg = getAlgorithmName(options.algorithm || 'sha1');
  47. if (keyType === 'ec') {
  48. const curve = options.curve || 'P-256';
  49. return {
  50. name: "ECDSA",
  51. namedCurve: curve
  52. };
  53. }
  54. return {
  55. name: "RSASSA-PKCS1-v1_5",
  56. modulusLength: options.keySize || 2048,
  57. publicExponent: new Uint8Array([1, 0, 1]),
  58. hash: hashAlg
  59. };
  60. }
  61. // Build extensions array from options or use defaults
  62. // Supports the old node-forge extension format for backwards compatibility
  63. function buildExtensions(userExtensions, commonName) {
  64. if (!userExtensions || userExtensions.length === 0) {
  65. // Default extensions
  66. return [
  67. new BasicConstraintsExtension(false, undefined, true),
  68. new KeyUsagesExtension(KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, true),
  69. new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth, ExtendedKeyUsage.clientAuth], false),
  70. new SubjectAlternativeNameExtension([
  71. { type: 'dns', value: commonName },
  72. ...(commonName === 'localhost' ? [{ type: 'ip', value: '127.0.0.1' }] : [])
  73. ], false)
  74. ];
  75. }
  76. // Convert user extensions from node-forge format to @peculiar/x509 format
  77. const extensions = [];
  78. for (const ext of userExtensions) {
  79. const critical = ext.critical || false;
  80. switch (ext.name) {
  81. case 'basicConstraints':
  82. extensions.push(new BasicConstraintsExtension(
  83. ext.cA || false,
  84. ext.pathLenConstraint,
  85. critical
  86. ));
  87. break;
  88. case 'keyUsage':
  89. let flags = 0;
  90. if (ext.digitalSignature) flags |= KeyUsageFlags.digitalSignature;
  91. if (ext.nonRepudiation || ext.contentCommitment) flags |= KeyUsageFlags.nonRepudiation;
  92. if (ext.keyEncipherment) flags |= KeyUsageFlags.keyEncipherment;
  93. if (ext.dataEncipherment) flags |= KeyUsageFlags.dataEncipherment;
  94. if (ext.keyAgreement) flags |= KeyUsageFlags.keyAgreement;
  95. if (ext.keyCertSign) flags |= KeyUsageFlags.keyCertSign;
  96. if (ext.cRLSign) flags |= KeyUsageFlags.cRLSign;
  97. if (ext.encipherOnly) flags |= KeyUsageFlags.encipherOnly;
  98. if (ext.decipherOnly) flags |= KeyUsageFlags.decipherOnly;
  99. extensions.push(new KeyUsagesExtension(flags, critical));
  100. break;
  101. case 'extKeyUsage':
  102. const usages = [];
  103. if (ext.serverAuth) usages.push(ExtendedKeyUsage.serverAuth);
  104. if (ext.clientAuth) usages.push(ExtendedKeyUsage.clientAuth);
  105. if (ext.codeSigning) usages.push(ExtendedKeyUsage.codeSigning);
  106. if (ext.emailProtection) usages.push(ExtendedKeyUsage.emailProtection);
  107. if (ext.timeStamping) usages.push(ExtendedKeyUsage.timeStamping);
  108. extensions.push(new ExtendedKeyUsageExtension(usages, critical));
  109. break;
  110. case 'subjectAltName':
  111. const altNames = (ext.altNames || []).map(alt => {
  112. // node-forge type values:
  113. // 1 = email (rfc822Name)
  114. // 2 = DNS
  115. // 6 = URI
  116. // 7 = IP
  117. switch (alt.type) {
  118. case 1: // email
  119. return { type: 'email', value: alt.value };
  120. case 2: // DNS
  121. return { type: 'dns', value: alt.value };
  122. case 6: // URI
  123. return { type: 'url', value: alt.value };
  124. case 7: // IP
  125. return { type: 'ip', value: alt.ip || alt.value };
  126. default:
  127. // Try to infer type from properties
  128. if (alt.ip) return { type: 'ip', value: alt.ip };
  129. if (alt.dns) return { type: 'dns', value: alt.dns };
  130. if (alt.email) return { type: 'email', value: alt.email };
  131. if (alt.uri || alt.url) return { type: 'url', value: alt.uri || alt.url };
  132. return { type: 'dns', value: alt.value };
  133. }
  134. });
  135. extensions.push(new SubjectAlternativeNameExtension(altNames, critical));
  136. break;
  137. default:
  138. // Skip unknown extensions with a warning
  139. console.warn(`Unknown extension "${ext.name}" ignored`);
  140. }
  141. }
  142. return extensions;
  143. }
  144. // Convert attributes from node-forge format to X509 name format
  145. function convertAttributes(attrs) {
  146. const nameMap = {
  147. 'commonName': 'CN',
  148. 'countryName': 'C',
  149. 'ST': 'ST',
  150. 'localityName': 'L',
  151. 'organizationName': 'O',
  152. 'OU': 'OU'
  153. };
  154. return attrs.map(attr => {
  155. const key = attr.name || attr.shortName;
  156. const oid = nameMap[key] || key;
  157. return `${oid}=${attr.value}`;
  158. }).join(', ');
  159. }
  160. // Detect key type from PEM key using Node.js crypto
  161. function detectKeyType(pemKey) {
  162. const keyObject = nodeCrypto.createPrivateKey(pemKey);
  163. return keyObject.asymmetricKeyType; // 'rsa' or 'ec'
  164. }
  165. // Map Node.js curve names to Web Crypto curve names
  166. function normalizeECCurve(curveName) {
  167. const curveMap = {
  168. 'prime256v1': 'P-256',
  169. 'secp384r1': 'P-384',
  170. 'secp521r1': 'P-521',
  171. 'P-256': 'P-256',
  172. 'P-384': 'P-384',
  173. 'P-521': 'P-521'
  174. };
  175. return curveMap[curveName] || curveName;
  176. }
  177. // Get EC curve from key object
  178. function getECCurve(keyObject) {
  179. const details = keyObject.asymmetricKeyDetails;
  180. if (details && details.namedCurve) {
  181. return normalizeECCurve(details.namedCurve);
  182. }
  183. return 'P-256'; // default
  184. }
  185. // Convert PEM key to CryptoKey
  186. async function importPrivateKey(pemKey, algorithm, keyType) {
  187. // Auto-detect key type if not provided
  188. const keyObject = nodeCrypto.createPrivateKey(pemKey);
  189. const detectedKeyType = keyObject.asymmetricKeyType;
  190. const actualKeyType = keyType || detectedKeyType;
  191. // Convert to PKCS#8 format
  192. const pkcs8Pem = keyObject.export({ type: 'pkcs8', format: 'pem' });
  193. const pemContents = pkcs8Pem
  194. .replace(/-----BEGIN PRIVATE KEY-----/, '')
  195. .replace(/-----END PRIVATE KEY-----/, '')
  196. .replace(/\s/g, '');
  197. const binaryDer = Buffer.from(pemContents, 'base64');
  198. let importAlgorithm;
  199. if (actualKeyType === 'ec') {
  200. const curve = getECCurve(keyObject);
  201. importAlgorithm = {
  202. name: 'ECDSA',
  203. namedCurve: curve
  204. };
  205. } else {
  206. importAlgorithm = {
  207. name: 'RSASSA-PKCS1-v1_5',
  208. hash: getAlgorithmName(algorithm)
  209. };
  210. }
  211. return await crypto.subtle.importKey(
  212. 'pkcs8',
  213. binaryDer,
  214. importAlgorithm,
  215. true,
  216. ['sign']
  217. );
  218. }
  219. async function importPublicKey(pemKey, algorithm, keyType, curve) {
  220. const pemContents = pemKey
  221. .replace(/-----BEGIN PUBLIC KEY-----/, '')
  222. .replace(/-----END PUBLIC KEY-----/, '')
  223. .replace(/\s/g, '');
  224. const binaryDer = Buffer.from(pemContents, 'base64');
  225. let importAlgorithm;
  226. if (keyType === 'ec') {
  227. importAlgorithm = {
  228. name: 'ECDSA',
  229. namedCurve: curve || 'P-256'
  230. };
  231. } else {
  232. importAlgorithm = {
  233. name: 'RSASSA-PKCS1-v1_5',
  234. hash: getAlgorithmName(algorithm)
  235. };
  236. }
  237. return await crypto.subtle.importKey(
  238. 'spki',
  239. binaryDer,
  240. importAlgorithm,
  241. true,
  242. ['verify']
  243. );
  244. }
  245. async function generatePemAsync(keyPair, attrs, options, ca) {
  246. const { privateKey, publicKey } = keyPair;
  247. // Generate serial number
  248. const serialBytes = crypto.getRandomValues(new Uint8Array(9));
  249. const serialHex = toPositiveHex(Buffer.from(serialBytes).toString('hex'));
  250. // Set up dates
  251. const notBefore = options.notBeforeDate || new Date();
  252. let notAfter;
  253. if (options.notAfterDate) {
  254. notAfter = options.notAfterDate;
  255. } else {
  256. notAfter = new Date(notBefore);
  257. notAfter.setDate(notAfter.getDate() + 365);
  258. }
  259. // Default attributes
  260. attrs = attrs || [
  261. {
  262. name: "commonName",
  263. value: "example.org",
  264. },
  265. {
  266. name: "countryName",
  267. value: "US",
  268. },
  269. {
  270. shortName: "ST",
  271. value: "Virginia",
  272. },
  273. {
  274. name: "localityName",
  275. value: "Blacksburg",
  276. },
  277. {
  278. name: "organizationName",
  279. value: "Test",
  280. },
  281. {
  282. shortName: "OU",
  283. value: "Test",
  284. },
  285. ];
  286. const subjectName = convertAttributes(attrs);
  287. const keyType = options.keyType || 'rsa';
  288. const signingAlg = getSigningAlgorithm(options.algorithm, keyType);
  289. // Extract common name for SAN extension
  290. const commonNameAttr = attrs.find(attr => attr.name === 'commonName' || attr.shortName === 'CN');
  291. const commonName = commonNameAttr ? commonNameAttr.value : 'localhost';
  292. // Build extensions array
  293. const extensions = buildExtensions(options.extensions, commonName);
  294. let cert;
  295. if (ca) {
  296. // Generate certificate signed by CA
  297. const caCert = new X509Certificate(ca.cert);
  298. const caPrivateKey = await importPrivateKey(ca.key, options.algorithm || "sha256", keyType);
  299. cert = await X509CertificateGenerator.create({
  300. serialNumber: serialHex,
  301. subject: subjectName,
  302. issuer: caCert.subject,
  303. notBefore: notBefore,
  304. notAfter: notAfter,
  305. signingAlgorithm: signingAlg,
  306. publicKey: publicKey,
  307. signingKey: caPrivateKey,
  308. extensions: extensions
  309. });
  310. } else {
  311. // Generate self-signed certificate
  312. cert = await X509CertificateGenerator.createSelfSigned({
  313. serialNumber: serialHex,
  314. name: subjectName,
  315. notBefore: notBefore,
  316. notAfter: notAfter,
  317. signingAlgorithm: signingAlg,
  318. keys: {
  319. privateKey: privateKey,
  320. publicKey: publicKey
  321. },
  322. extensions: extensions
  323. });
  324. }
  325. // Calculate fingerprint (SHA-1 hash of the certificate)
  326. const certRaw = cert.rawData;
  327. const fingerprintBuffer = await crypto.subtle.digest('SHA-1', certRaw);
  328. const fingerprint = Buffer.from(fingerprintBuffer)
  329. .toString('hex')
  330. .match(/.{2}/g)
  331. .join(':');
  332. // Export keys to PEM
  333. const privateKeyDer = await crypto.subtle.exportKey('pkcs8', privateKey);
  334. const publicKeyDer = await crypto.subtle.exportKey('spki', publicKey);
  335. let privatePem;
  336. if (options.passphrase) {
  337. // Encrypt the private key with the passphrase using Node.js crypto
  338. const keyObject = nodeCrypto.createPrivateKey({
  339. key: Buffer.from(privateKeyDer),
  340. format: 'der',
  341. type: 'pkcs8'
  342. });
  343. privatePem = keyObject.export({
  344. type: 'pkcs8',
  345. format: 'pem',
  346. cipher: 'aes-256-cbc',
  347. passphrase: options.passphrase
  348. });
  349. } else {
  350. privatePem =
  351. '-----BEGIN PRIVATE KEY-----\n' +
  352. Buffer.from(privateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
  353. '\n-----END PRIVATE KEY-----\n';
  354. }
  355. const publicPem =
  356. '-----BEGIN PUBLIC KEY-----\n' +
  357. Buffer.from(publicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
  358. '\n-----END PUBLIC KEY-----\n';
  359. const certPem = cert.toString('pem');
  360. const pem = {
  361. private: privatePem,
  362. public: publicPem,
  363. cert: certPem,
  364. fingerprint: fingerprint,
  365. };
  366. // Client certificate support
  367. if (options && options.clientCertificate) {
  368. // Parse clientCertificate options - can be boolean or object
  369. const clientOpts = typeof options.clientCertificate === 'object' ? options.clientCertificate : {};
  370. // Resolve client certificate options with fallbacks to deprecated options
  371. const clientKeySize = clientOpts.keySize || options.clientCertificateKeySize || 2048;
  372. const clientAlgorithm = clientOpts.algorithm || options.algorithm || "sha1";
  373. const clientCN = clientOpts.cn || options.clientCertificateCN || "John Doe jdoe123";
  374. // Client cert uses same key type and curve as main cert by default
  375. const clientKeyType = clientOpts.keyType || keyType;
  376. const clientCurve = clientOpts.curve || options.curve || 'P-256';
  377. const clientKeyAlg = getKeyAlgorithm({
  378. keyType: clientKeyType,
  379. keySize: clientKeySize,
  380. algorithm: clientAlgorithm,
  381. curve: clientCurve
  382. });
  383. const clientKeyPair = await crypto.subtle.generateKey(
  384. clientKeyAlg,
  385. true,
  386. ["sign", "verify"]
  387. );
  388. const clientSerialBytes = crypto.getRandomValues(new Uint8Array(9));
  389. const clientSerialHex = toPositiveHex(Buffer.from(clientSerialBytes).toString('hex'));
  390. // Resolve client certificate validity dates
  391. const clientNotBefore = clientOpts.notBeforeDate || new Date();
  392. let clientNotAfter;
  393. if (clientOpts.notAfterDate) {
  394. clientNotAfter = clientOpts.notAfterDate;
  395. } else {
  396. clientNotAfter = new Date(clientNotBefore);
  397. clientNotAfter.setFullYear(clientNotBefore.getFullYear() + 1);
  398. }
  399. const clientAttrs = JSON.parse(JSON.stringify(attrs));
  400. for (let i = 0; i < clientAttrs.length; i++) {
  401. if (clientAttrs[i].name === "commonName") {
  402. clientAttrs[i] = {
  403. name: "commonName",
  404. value: clientCN
  405. };
  406. }
  407. }
  408. const clientSubjectName = convertAttributes(clientAttrs);
  409. const issuerName = convertAttributes(attrs);
  410. // Signing algorithm for client cert - uses main key type since signed by root
  411. const clientSigningAlg = getSigningAlgorithm(clientAlgorithm, keyType);
  412. // Create client cert signed by root key
  413. const clientCertRaw = await X509CertificateGenerator.create({
  414. serialNumber: clientSerialHex,
  415. subject: clientSubjectName,
  416. issuer: issuerName,
  417. notBefore: clientNotBefore,
  418. notAfter: clientNotAfter,
  419. signingAlgorithm: clientSigningAlg,
  420. publicKey: clientKeyPair.publicKey,
  421. signingKey: privateKey // Sign with root private key
  422. });
  423. // Export client keys
  424. const clientPrivateKeyDer = await crypto.subtle.exportKey('pkcs8', clientKeyPair.privateKey);
  425. const clientPublicKeyDer = await crypto.subtle.exportKey('spki', clientKeyPair.publicKey);
  426. pem.clientprivate =
  427. '-----BEGIN PRIVATE KEY-----\n' +
  428. Buffer.from(clientPrivateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
  429. '\n-----END PRIVATE KEY-----\n';
  430. pem.clientpublic =
  431. '-----BEGIN PUBLIC KEY-----\n' +
  432. Buffer.from(clientPublicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
  433. '\n-----END PUBLIC KEY-----\n';
  434. pem.clientcert = clientCertRaw.toString('pem');
  435. }
  436. // Verify certificate chain
  437. const x509Cert = new X509Certificate(cert.rawData);
  438. const certificates = [x509Cert];
  439. // If CA-signed, include CA cert in the chain for verification
  440. if (ca) {
  441. const caCert = new X509Certificate(ca.cert);
  442. certificates.push(caCert);
  443. }
  444. const chainBuilder = new X509ChainBuilder({
  445. certificates: certificates
  446. });
  447. const chain = await chainBuilder.build(x509Cert);
  448. if (chain.length === 0) {
  449. throw new Error("Certificate could not be verified.");
  450. }
  451. return pem;
  452. }
  453. /**
  454. * Generate a certificate (async)
  455. *
  456. * @param {CertificateField[]} attrs Attributes used for subject.
  457. * @param {object} options
  458. * @param {string} [options.keyType="rsa"] Key type: "rsa" or "ec" (elliptic curve)
  459. * @param {number} [options.keySize=2048] the size for the private key in bits (RSA only)
  460. * @param {string} [options.curve="P-256"] The elliptic curve to use: "P-256", "P-384", or "P-521" (EC only)
  461. * @param {object} [options.extensions] additional extensions for the certificate
  462. * @param {string} [options.algorithm="sha1"] The signature algorithm sha256, sha384, sha512 or sha1
  463. * @param {Date} [options.notBeforeDate=new Date()] The date before which the certificate should not be valid
  464. * @param {Date} [options.notAfterDate] The date after which the certificate should not be valid (default: notBeforeDate + 365 days)
  465. * @param {boolean|object} [options.clientCertificate=false] Generate client cert signed by the original key. Can be `true` for defaults or an options object.
  466. * @param {number} [options.clientCertificate.keySize=2048] Key size for the client certificate in bits (RSA only)
  467. * @param {string} [options.clientCertificate.keyType] Key type for client cert (defaults to main keyType)
  468. * @param {string} [options.clientCertificate.curve] Elliptic curve for client cert (EC only)
  469. * @param {string} [options.clientCertificate.algorithm] Signature algorithm for client cert (defaults to options.algorithm or "sha1")
  470. * @param {string} [options.clientCertificate.cn="John Doe jdoe123"] Client certificate's common name
  471. * @param {Date} [options.clientCertificate.notBeforeDate=new Date()] The date before which the client certificate should not be valid
  472. * @param {Date} [options.clientCertificate.notAfterDate] The date after which the client certificate should not be valid (default: notBeforeDate + 1 year)
  473. * @param {string} [options.clientCertificateCN="John Doe jdoe123"] @deprecated Use options.clientCertificate.cn instead
  474. * @param {number} [options.clientCertificateKeySize] @deprecated Use options.clientCertificate.keySize instead
  475. * @param {object} [options.ca] CA certificate and key for signing (if not provided, generates self-signed)
  476. * @param {string} [options.ca.key] CA private key in PEM format
  477. * @param {string} [options.ca.cert] CA certificate in PEM format
  478. * @param {string} [options.passphrase] Passphrase to encrypt the private key (uses AES-256-CBC)
  479. * @returns {Promise<object>} Promise that resolves with certificate data
  480. */
  481. exports.generate = async function generate(attrs, options) {
  482. attrs = attrs || undefined;
  483. options = options || {};
  484. const keyType = options.keyType || 'rsa';
  485. const curve = options.curve || 'P-256';
  486. let keyPair;
  487. if (options.keyPair) {
  488. // Import existing key pair
  489. keyPair = {
  490. privateKey: await importPrivateKey(options.keyPair.privateKey, options.algorithm || "sha1", keyType),
  491. publicKey: await importPublicKey(options.keyPair.publicKey, options.algorithm || "sha1", keyType, curve)
  492. };
  493. } else {
  494. // Generate new key pair using appropriate algorithm
  495. const keyAlg = getKeyAlgorithm(options);
  496. keyPair = await crypto.subtle.generateKey(
  497. keyAlg,
  498. true,
  499. ["sign", "verify"]
  500. );
  501. }
  502. return await generatePemAsync(keyPair, attrs, options, options.ca);
  503. };