first commit

This commit is contained in:
Stefan Hacker
2026-04-03 09:38:48 +02:00
commit 37ad745546
47450 changed files with 3120798 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
name: PR Tests
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18.x, 20.x, 22.x, 24.x]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
+39
View File
@@ -0,0 +1,39 @@
{
"camelcase": false,
"curly": false,
"node": true,
"esnext": true,
"bitwise": true,
"eqeqeq": true,
"immed": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"regexp": true,
"undef": true,
"strict": false,
"smarttabs": true,
"expr": true,
"evil": true,
"browser": true,
"regexdash": true,
"wsh": true,
"trailing": true,
"sub": true,
"unused": true,
"laxcomma": true,
"globals": {
"after": false,
"before": false,
"afterEach": false,
"beforeEach": false,
"describe": false,
"it": false,
"DOMParser": true,
"XMLSerializer": true
}
}
+1
View File
@@ -0,0 +1 @@
22
+97
View File
@@ -0,0 +1,97 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.0.0] - 2025-11-26
### 🚀 Major Rewrite
Complete rewrite replacing `node-forge` with modern `@peculiar/x509` and `pkijs` libraries.
### ✨ Added
- Native WebCrypto API support for better performance and security
- TypeScript examples in documentation
- Async/await support as the primary API
- Support for `keyPair` option to use existing keys
- Updated to use Node.js native crypto for all operations
- Separate `selfsigned/pkcs7` module for tree-shakeable PKCS#7 support
### 💥 BREAKING CHANGES
1. **Async-only API**: The `generate()` function now returns a Promise. Synchronous generation has been removed.
```js
// Old (v4.x)
const pems = selfsigned.generate(attrs, options);
// New (v5.x)
const pems = await selfsigned.generate(attrs, options);
```
2. **No callback support**: Callbacks have been completely removed in favor of Promises.
```js
// Old (v4.x)
selfsigned.generate(attrs, options, function(err, pems) { ... });
// New (v5.x)
const pems = await selfsigned.generate(attrs, options);
```
3. **Minimum Node.js version**: Now requires Node.js >= 15.6.0 (was >= 10)
- Required for native WebCrypto support
4. **Dependencies changed**:
- ❌ Removed: `node-forge` (1.64 MB)
- ✅ Added: `@peculiar/x509` (551 KB) - 66% smaller!
- ✅ Added: `pkijs` (1.94 MB, only for PKCS#7 support)
- Bundle size reduced by 66% when not using PKCS#7
5. **PKCS#7 API changed**:
- Old: `const pems = await generate(attrs, { pkcs7: true }); pems.pkcs7`
- New: `const { createPkcs7 } = require('selfsigned/pkcs7'); const pkcs7 = createPkcs7(pems.cert);`
- PKCS#7 is now a separate module for better tree-shaking
### 🔧 Changed
- Default key size remains 2048 bits (was incorrectly documented as 1024)
- PEM output uses `\n` line endings (was `\r\n`)
- Private keys now use PKCS#8 format (`BEGIN PRIVATE KEY` instead of `BEGIN RSA PRIVATE KEY`)
- Certificate generation is now fully async using native WebCrypto
- **PKCS#7 is now tree-shakeable**: Moved to separate `selfsigned/pkcs7` module so bundlers can exclude it when not used
### 🐛 Fixed
- Default key size documentation corrected from 1024 to 2048 bits
- Improved error handling for certificate generation failures
### 📦 Dependencies
**Removed:**
- `node-forge@^1.3.1`
- `@types/node-forge@^1.3.0`
**Added:**
- `@peculiar/x509@^1.14.2` (required)
- `pkijs@^3.3.3` (required, but tree-shakeable via separate `selfsigned/pkcs7` module)
### 🔒 Security
- Now uses Node.js native WebCrypto API instead of JavaScript implementation
- Better integration with platform security features
- More secure random number generation
### 📚 Documentation
- Complete README rewrite with async/await examples
- Added migration guide from v4.x to v5.x
- Updated all code examples to use async/await
- Added requirements section highlighting Node.js version requirement
---
## [4.0.0] - Previous Release
See git history for changes in 4.x and earlier versions.
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2013 José F. Romaniello
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+474
View File
@@ -0,0 +1,474 @@
# selfsigned
Generate self-signed X.509 certificates using Node.js native crypto.
## Install
```bash
npm install selfsigned
```
## Requirements
- **Node.js >= 15.6.0** (for native WebCrypto support)
## Usage
**Version 5.0 is async-only.** The `generate()` function now returns a Promise.
```js
const selfsigned = require('selfsigned');
const attrs = [{ name: 'commonName', value: 'contoso.com' }];
const pems = await selfsigned.generate(attrs);
console.log(pems);
```
### Output
```js
{
private: '-----BEGIN PRIVATE KEY-----\n...',
public: '-----BEGIN PUBLIC KEY-----\n...',
cert: '-----BEGIN CERTIFICATE-----\n...',
fingerprint: 'XX:XX:XX:...'
}
```
## Options
```js
const pems = await selfsigned.generate(null, {
keyType: 'rsa', // key type: 'rsa' or 'ec' (default: 'rsa')
keySize: 2048, // the size for the private key in bits (default: 2048, RSA only)
curve: 'P-256', // elliptic curve: 'P-256', 'P-384', or 'P-521' (default: 'P-256', EC only)
notBeforeDate: new Date(), // start of certificate validity (default: now)
notAfterDate: new Date('2026-01-01'), // end of certificate validity (default: notBeforeDate + 365 days)
algorithm: 'sha256', // sign the certificate with specified algorithm (default: 'sha1')
extensions: [{ name: 'basicConstraints', cA: true }], // certificate extensions array
clientCertificate: true, // generate client cert (default: false) - can also be an options object
ca: { key: '...', cert: '...' }, // CA key and cert for signing (default: self-signed)
passphrase: 'secret' // encrypt the private key with a passphrase (default: none)
});
```
### Setting Custom Validity Period
Use `notBeforeDate` and `notAfterDate` to control certificate validity:
```js
// Using date-fns
const { addDays, addYears } = require('date-fns');
const pems = await selfsigned.generate(null, {
notBeforeDate: new Date(),
notAfterDate: addDays(new Date(), 30) // Valid for 30 days
});
// Or with vanilla JS
const notBefore = new Date();
const notAfter = new Date(notBefore);
notAfter.setFullYear(notAfter.getFullYear() + 2); // Valid for 2 years
const pems = await selfsigned.generate(null, {
notBeforeDate: notBefore,
notAfterDate: notAfter
});
```
### Supported Algorithms
- `sha1` (default)
- `sha256`
- `sha384`
- `sha512`
### Custom Extensions
You can customize certificate extensions using the `extensions` option. This is useful for adding Subject Alternative Names (SANs) with IPv6 addresses, custom key usage, and more.
```js
const pems = await selfsigned.generate(
[{ name: 'commonName', value: 'localhost' }],
{
extensions: [
{
name: 'basicConstraints',
cA: false
},
{
name: 'keyUsage',
digitalSignature: true,
keyEncipherment: true
},
{
name: 'subjectAltName',
altNames: [
{ type: 2, value: 'localhost' }, // DNS
{ type: 7, ip: '127.0.0.1' }, // IPv4
{ type: 7, ip: '::1' } // IPv6
]
}
]
}
);
```
#### Supported Extensions
**basicConstraints**
```js
{
name: 'basicConstraints',
cA: true, // is this a CA certificate?
pathLenConstraint: 0, // max depth of valid cert chain (optional)
critical: true // mark as critical extension
}
```
**keyUsage**
```js
{
name: 'keyUsage',
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true,
keyAgreement: true,
keyCertSign: true, // for CA certificates
cRLSign: true, // for CA certificates
encipherOnly: true,
decipherOnly: true,
critical: true
}
```
**extKeyUsage** (Extended Key Usage)
```js
{
name: 'extKeyUsage',
serverAuth: true, // TLS server authentication
clientAuth: true, // TLS client authentication
codeSigning: true,
emailProtection: true,
timeStamping: true
}
```
**subjectAltName** (Subject Alternative Name)
```js
{
name: 'subjectAltName',
altNames: [
{ type: 1, value: 'user@example.com' }, // email (rfc822Name)
{ type: 2, value: 'example.com' }, // DNS name
{ type: 2, value: '*.example.com' }, // wildcard DNS
{ type: 6, value: 'http://example.com/webid' }, // URI
{ type: 7, ip: '127.0.0.1' }, // IPv4 address
{ type: 7, ip: '::1' } // IPv6 address
]
}
```
#### Default Extensions
When no `extensions` option is provided (or an empty array), the following defaults are used:
```js
[
{ name: 'basicConstraints', cA: false, critical: true },
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true, critical: true },
{ name: 'extKeyUsage', serverAuth: true, clientAuth: true },
{ name: 'subjectAltName', altNames: [
{ type: 2, value: commonName },
// For localhost, also includes: { type: 7, ip: '127.0.0.1' }
]}
]
```
### Elliptic Curve (EC) Keys
By default, selfsigned generates RSA keys. You can generate certificates using elliptic curve cryptography instead, which provides equivalent security with smaller key sizes and faster operations.
```js
// Generate EC certificate with P-256 curve (default)
const pems = await selfsigned.generate(null, { keyType: 'ec' });
// Generate EC certificate with P-384 curve
const pems = await selfsigned.generate(null, { keyType: 'ec', curve: 'P-384' });
// Generate EC certificate with P-521 curve and SHA-512
const pems = await selfsigned.generate(null, {
keyType: 'ec',
curve: 'P-521',
algorithm: 'sha512'
});
```
**Supported curves:**
- `P-256` (default) - 128-bit security, fastest
- `P-384` - 192-bit security
- `P-521` - 256-bit security, strongest
EC keys work with all other options including `clientCertificate`, `passphrase`, `ca`, and `keyPair`:
```js
// EC certificate with encrypted private key
const pems = await selfsigned.generate(null, {
keyType: 'ec',
passphrase: 'secret'
});
// EC certificate with client certificate
const pems = await selfsigned.generate(null, {
keyType: 'ec',
clientCertificate: true
});
// Reuse existing EC key pair
const pems = await selfsigned.generate(null, {
keyType: 'ec',
curve: 'P-256',
keyPair: {
publicKey: existingPublicKey,
privateKey: existingPrivateKey
}
});
```
### Using Your Own Keys
You can avoid key pair generation by specifying your own keys:
```js
const pems = await selfsigned.generate(null, {
keyPair: {
publicKey: '-----BEGIN PUBLIC KEY-----...',
privateKey: '-----BEGIN PRIVATE KEY-----...'
}
});
```
### Encrypting the Private Key
You can encrypt the private key with a passphrase using AES-256-CBC:
```js
const pems = await selfsigned.generate(null, {
passphrase: 'my-secret-passphrase'
});
// The private key will be in encrypted PKCS#8 format:
// -----BEGIN ENCRYPTED PRIVATE KEY-----
// ...
// -----END ENCRYPTED PRIVATE KEY-----
```
To use the encrypted key, provide the passphrase:
```js
const crypto = require('crypto');
// Decrypt the key
const privateKey = crypto.createPrivateKey({
key: pems.private,
passphrase: 'my-secret-passphrase'
});
// Or use directly with HTTPS server
const https = require('https');
https.createServer({
key: pems.private,
passphrase: 'my-secret-passphrase',
cert: pems.cert
}, app).listen(443);
```
### Signing with a CA
You can generate certificates signed by an existing Certificate Authority instead of self-signed certificates. This is useful for development environments where you want browsers to trust your certificates.
```js
const fs = require('fs');
const selfsigned = require('selfsigned');
const pems = await selfsigned.generate([
{ name: 'commonName', value: 'localhost' }
], {
algorithm: 'sha256',
ca: {
key: fs.readFileSync('/path/to/ca.key', 'utf8'),
cert: fs.readFileSync('/path/to/ca.crt', 'utf8')
}
});
```
The generated certificate will be signed by the provided CA and will include:
- Subject Alternative Name (SAN) extension with DNS name matching the commonName
- For `localhost`, an additional IP SAN for `127.0.0.1`
- Key Usage: digitalSignature, keyEncipherment
- Extended Key Usage: serverAuth, clientAuth
#### Using with mkcert
[mkcert](https://github.com/FiloSottile/mkcert) is a simple tool for making locally-trusted development certificates. Combining it with `selfsigned` provides an excellent developer experience:
- **No certificate files to manage** - generate trusted certificates on-the-fly at server startup
- **No git-ignored cert files** - nothing to store, share, or accidentally commit
- **Browsers trust the certificates automatically** - no security warnings during development
```js
const https = require('https');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const selfsigned = require('selfsigned');
// Get mkcert's CA (requires: brew install mkcert && mkcert -install)
const caroot = execSync('mkcert -CAROOT', { encoding: 'utf8' }).trim();
const pems = await selfsigned.generate([
{ name: 'commonName', value: 'localhost' }
], {
algorithm: 'sha256',
ca: {
key: fs.readFileSync(path.join(caroot, 'rootCA-key.pem'), 'utf8'),
cert: fs.readFileSync(path.join(caroot, 'rootCA.pem'), 'utf8')
}
});
// Start server with browser-trusted certificate - no files written to disk
https.createServer({ key: pems.private, cert: pems.cert }, app).listen(443);
```
See [examples/https-server-mkcert.js](examples/https-server-mkcert.js) for a complete working example.
## Attributes
Attributes follow the X.509 standard:
```js
const attrs = [
{ name: 'commonName', value: 'example.org' },
{ name: 'countryName', value: 'US' },
{ shortName: 'ST', value: 'Virginia' },
{ name: 'localityName', value: 'Blacksburg' },
{ name: 'organizationName', value: 'Test' },
{ shortName: 'OU', value: 'Test' }
];
```
## Generate Client Certificates
For environments where servers require client certificates, you can generate client keys signed by the original (server) key:
```js
const pems = await selfsigned.generate(null, { clientCertificate: true });
console.log(pems);
```
Output includes additional client certificate fields:
```js
{
private: '-----BEGIN PRIVATE KEY-----\n...',
public: '-----BEGIN PUBLIC KEY-----\n...',
cert: '-----BEGIN CERTIFICATE-----\n...',
fingerprint: 'XX:XX:XX:...',
clientprivate: '-----BEGIN PRIVATE KEY-----\n...',
clientpublic: '-----BEGIN PUBLIC KEY-----\n...',
clientcert: '-----BEGIN CERTIFICATE-----\n...'
}
```
### Client Certificate Options
The `clientCertificate` option can be `true` for defaults, or an options object for full control:
```js
const pems = await selfsigned.generate(null, {
clientCertificate: {
cn: 'jdoe', // common name (default: 'John Doe jdoe123')
keyType: 'rsa', // key type: 'rsa' or 'ec' (default: inherits from parent)
keySize: 4096, // key size in bits (default: 2048, RSA only)
curve: 'P-256', // elliptic curve (default: 'P-256', EC only)
algorithm: 'sha256', // signature algorithm (default: inherits from parent or 'sha1')
notBeforeDate: new Date(), // validity start (default: now)
notAfterDate: new Date('2026-01-01') // validity end (default: notBeforeDate + 1 year)
}
});
```
Simple example with just a custom CN:
```js
const pems = await selfsigned.generate(null, {
clientCertificate: { cn: 'FooBar' }
});
```
## PKCS#7 Support
PKCS#7 formatting is available through a separate module for better tree-shaking:
```js
const selfsigned = require('selfsigned');
const { createPkcs7 } = require('selfsigned/pkcs7');
const pems = await selfsigned.generate(attrs);
const pkcs7 = createPkcs7(pems.cert);
console.log(pkcs7); // PKCS#7 formatted certificate
```
You can also create PKCS#7 for client certificates:
```js
const pems = await selfsigned.generate(null, { clientCertificate: true });
const clientPkcs7 = createPkcs7(pems.clientcert);
```
## Migration from v4.x
Version 5.0 introduces breaking changes:
### Breaking Changes
1. **Async-only API**: The `generate()` function is now async and returns a Promise. Synchronous generation is no longer supported.
2. **No callback support**: Callbacks have been removed. Use `async`/`await` or `.then()`.
3. **Minimum Node.js version**: Now requires Node.js >= 15.6.0 (was >= 10).
4. **Dependencies**: Replaced `node-forge` with `@peculiar/x509` and `pkijs` (66% smaller bundle size).
5. **`days` option removed**: Use `notAfterDate` instead. Default validity is 365 days from `notBeforeDate`.
### Migration Examples
**Old (v4.x):**
```js
// Sync
const pems = selfsigned.generate(attrs, { days: 365 });
// Callback
selfsigned.generate(attrs, { days: 365 }, function(err, pems) {
if (err) throw err;
console.log(pems);
});
```
**New (v5.x):**
```js
// Async/await (default 365 days validity)
const pems = await selfsigned.generate(attrs);
// Custom validity with notAfterDate
const notAfter = new Date();
notAfter.setDate(notAfter.getDate() + 30); // 30 days
const pems = await selfsigned.generate(attrs, { notAfterDate: notAfter });
// Or with .then()
selfsigned.generate(attrs)
.then(pems => console.log(pems))
.catch(err => console.error(err));
```
## License
MIT
+66
View File
@@ -0,0 +1,66 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const selfsigned = require('../');
async function main() {
// Get mkcert's CAROOT path
let caroot;
try {
caroot = execSync('mkcert -CAROOT', { encoding: 'utf8' }).trim();
} catch (err) {
console.error('Error: mkcert is not installed or not in PATH');
console.error('Install mkcert: https://github.com/FiloSottile/mkcert');
process.exit(1);
}
const caKeyPath = path.join(caroot, 'rootCA-key.pem');
const caCertPath = path.join(caroot, 'rootCA.pem');
// Check if CA files exist
if (!fs.existsSync(caKeyPath) || !fs.existsSync(caCertPath)) {
console.error('Error: mkcert CA files not found');
console.error('Run "mkcert -install" first to create the local CA');
process.exit(1);
}
console.log('Using mkcert CA from:', caroot);
// Read CA certificate and key
const caKey = fs.readFileSync(caKeyPath, 'utf8');
const caCert = fs.readFileSync(caCertPath, 'utf8');
// Generate a certificate signed by mkcert's CA
const pems = await selfsigned.generate([
{ name: 'commonName', value: 'localhost' }
], {
days: 365,
keySize: 2048,
algorithm: 'sha256',
ca: {
key: caKey,
cert: caCert
}
});
// Create HTTPS server with the generated certificate
const server = https.createServer({
key: pems.private,
cert: pems.cert
}, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from HTTPS server with mkcert CA!\n');
});
const port = 3443;
server.listen(port, () => {
console.log(`HTTPS server running at https://localhost:${port}/`);
console.log('Certificate fingerprint:', pems.fingerprint);
console.log('\nSince this certificate is signed by mkcert\'s CA,');
console.log('your browser should trust it automatically (if mkcert -install was run).');
console.log('\nTest with: curl https://localhost:' + port);
});
}
main().catch(console.error);
+32
View File
@@ -0,0 +1,32 @@
const https = require('https');
const selfsigned = require('../');
async function main() {
// Generate a self-signed certificate
const pems = await selfsigned.generate([
{ name: 'commonName', value: 'localhost' }
], {
days: 365,
keySize: 2048,
algorithm: 'sha256'
});
// Create HTTPS server with the generated certificate
const server = https.createServer({
key: pems.private,
cert: pems.cert
}, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from self-signed HTTPS server!\n');
});
const port = 3443;
server.listen(port, () => {
console.log(`HTTPS server running at https://localhost:${port}/`);
console.log('Certificate fingerprint:', pems.fingerprint);
console.log('\nNote: Your browser will warn about the self-signed certificate.');
console.log('Test with: curl -k https://localhost:' + port);
});
}
main().catch(console.error);
+276
View File
@@ -0,0 +1,276 @@
declare enum ASN1Class {
UNIVERSAL = 0x00,
APPLICATION = 0x40,
CONTEXT_SPECIFIC = 0x80,
PRIVATE = 0xc0,
}
interface CertificateFieldOptions {
name?: string | undefined;
type?: string | undefined;
shortName?: string | undefined;
}
interface CertificateField extends CertificateFieldOptions {
valueConstructed?: boolean | undefined;
valueTagClass?: ASN1Class | undefined;
value?: any[] | string | undefined;
extensions?: any[] | undefined;
}
/**
* Subject Alternative Name entry types:
* - 1: email (rfc822Name)
* - 2: DNS name
* - 6: URI
* - 7: IP address
*/
declare interface SubjectAltNameEntry {
/**
* Type of the alternative name:
* - 1: email (rfc822Name)
* - 2: DNS name
* - 6: URI
* - 7: IP address
*/
type: 1 | 2 | 6 | 7;
/** Value for types 1, 2, 6 (email, DNS, URI) */
value?: string;
/** IP address for type 7 (IPv4 or IPv6) */
ip?: string;
}
declare interface BasicConstraintsExtension {
name: 'basicConstraints';
/** Is this a CA certificate? */
cA?: boolean;
/** Maximum depth of valid certificate chain */
pathLenConstraint?: number;
/** Mark extension as critical */
critical?: boolean;
}
declare interface KeyUsageExtension {
name: 'keyUsage';
digitalSignature?: boolean;
nonRepudiation?: boolean;
/** Also known as contentCommitment */
contentCommitment?: boolean;
keyEncipherment?: boolean;
dataEncipherment?: boolean;
keyAgreement?: boolean;
/** For CA certificates */
keyCertSign?: boolean;
/** For CA certificates */
cRLSign?: boolean;
encipherOnly?: boolean;
decipherOnly?: boolean;
/** Mark extension as critical */
critical?: boolean;
}
declare interface ExtKeyUsageExtension {
name: 'extKeyUsage';
/** TLS server authentication */
serverAuth?: boolean;
/** TLS client authentication */
clientAuth?: boolean;
codeSigning?: boolean;
emailProtection?: boolean;
timeStamping?: boolean;
/** Mark extension as critical */
critical?: boolean;
}
declare interface SubjectAltNameExtension {
name: 'subjectAltName';
altNames: SubjectAltNameEntry[];
/** Mark extension as critical */
critical?: boolean;
}
declare type CertificateExtension =
| BasicConstraintsExtension
| KeyUsageExtension
| ExtKeyUsageExtension
| SubjectAltNameExtension;
declare interface ClientCertificateOptions {
/**
* Key size for the client certificate in bits (RSA only)
* @default 2048
*/
keySize?: number
/**
* Key type for client certificate
* @default inherits from main keyType
*/
keyType?: 'rsa' | 'ec'
/**
* Elliptic curve for client certificate (EC only)
* @default "P-256"
*/
curve?: 'P-256' | 'P-384' | 'P-521'
/**
* Signature algorithm for client certificate
* @default inherits from main algorithm or "sha1"
*/
algorithm?: string
/**
* Client certificate's common name
* @default "John Doe jdoe123"
*/
cn?: string
/**
* The date before which the client certificate should not be valid
* @default now
*/
notBeforeDate?: Date
/**
* The date after which the client certificate should not be valid
* @default notBeforeDate + 1 year
*/
notAfterDate?: Date
}
declare interface SelfsignedOptions {
/**
* The date before which the certificate should not be valid
*
* @default now */
notBeforeDate?: Date
/**
* The date after which the certificate should not be valid
*
* @default notBeforeDate + 365 days */
notAfterDate?: Date
/**
* Key type: "rsa" or "ec" (elliptic curve)
* @default "rsa"
*/
keyType?: 'rsa' | 'ec'
/**
* the size for the private key in bits (RSA only)
* @default 2048
*/
keySize?: number
/**
* The elliptic curve to use (EC only): "P-256", "P-384", or "P-521"
* @default "P-256"
*/
curve?: 'P-256' | 'P-384' | 'P-521'
/**
* Certificate extensions. Supports basicConstraints, keyUsage, extKeyUsage, and subjectAltName.
* If not provided, defaults are used including DNS SAN matching commonName.
* @example
* ```typescript
* extensions: [
* { name: 'basicConstraints', cA: false },
* { name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
* { name: 'subjectAltName', altNames: [
* { type: 2, value: 'localhost' },
* { type: 7, ip: '127.0.0.1' },
* { type: 7, ip: '::1' }
* ]}
* ]
* ```
*/
extensions?: CertificateExtension[];
/**
* The signature algorithm: sha256, sha384, sha512 or sha1
* @default "sha1"
*/
algorithm?: string
/**
* include PKCS#7 as part of the output
* @default false
*/
pkcs7?: boolean
/**
* generate client cert signed by the original key
* Can be `true` for defaults or an options object
* @default false
*/
clientCertificate?: boolean | ClientCertificateOptions
/**
* client certificate's common name
* @default "John Doe jdoe123"
* @deprecated Use clientCertificate.cn instead
*/
clientCertificateCN?: string
/**
* the size for the client private key in bits
* @default 2048
* @deprecated Use clientCertificate.keySize instead
*/
clientCertificateKeySize?: number
/**
* existing key pair to use instead of generating new keys
*/
keyPair?: {
privateKey: string
publicKey: string
}
/**
* CA certificate and key for signing (if not provided, generates self-signed)
*/
ca?: {
/** CA private key in PEM format */
key: string
/** CA certificate in PEM format */
cert: string
}
/**
* Passphrase to encrypt the private key (PKCS#8 encrypted format)
* When provided, the private key will be encrypted using AES-256-CBC
*/
passphrase?: string
}
declare interface GenerateResult {
private: string
public: string
cert: string
fingerprint: string
pkcs7?: string
clientprivate?: string
clientpublic?: string
clientcert?: string
clientpkcs7?: string
}
/**
* Generate a certificate (async only)
*
* @param attrs Certificate attributes
* @param opts Generation options
* @returns Promise that resolves with certificate data
*
* @example
* ```typescript
* // Self-signed certificate
* const pems = await generate();
*
* const pems = await generate([{ name: 'commonName', value: 'example.com' }]);
*
* const pems = await generate(null, {
* keySize: 2048,
* algorithm: 'sha256'
* });
*
* // CA-signed certificate
* const pems = await generate([{ name: 'commonName', value: 'localhost' }], {
* algorithm: 'sha256',
* ca: {
* key: fs.readFileSync('/path/to/ca.key', 'utf8'),
* cert: fs.readFileSync('/path/to/ca.crt', 'utf8')
* }
* });
* ```
*/
export declare function generate(
attrs?: CertificateField[],
opts?: SelfsignedOptions
): Promise<GenerateResult>
+574
View File
@@ -0,0 +1,574 @@
const { X509CertificateGenerator, X509Certificate, cryptoProvider, X509ChainBuilder, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags, ExtendedKeyUsageExtension, ExtendedKeyUsage, SubjectAlternativeNameExtension, GeneralName } = require("@peculiar/x509");
const nodeCrypto = require("crypto");
// Use Node.js native webcrypto
const crypto = nodeCrypto.webcrypto;
// Patch global CryptoProvider to use Node.js crypto
cryptoProvider.set(crypto);
// a hexString is considered negative if it's most significant bit is 1
// because serial numbers use ones' complement notation
// this RFC in section 4.1.2.2 requires serial numbers to be positive
// http://www.ietf.org/rfc/rfc5280.txt
function toPositiveHex(hexString) {
var mostSiginficativeHexAsInt = parseInt(hexString[0], 16);
if (mostSiginficativeHexAsInt < 8) {
return hexString;
}
mostSiginficativeHexAsInt -= 8;
return mostSiginficativeHexAsInt.toString() + hexString.substring(1);
}
function getAlgorithmName(key) {
switch (key) {
case "sha256":
return "SHA-256";
case 'sha384':
return "SHA-384";
case 'sha512':
return "SHA-512";
default:
return "SHA-1";
}
}
function getSigningAlgorithm(hashKey, keyType) {
const hashAlg = getAlgorithmName(hashKey);
if (keyType === 'ec') {
return {
name: "ECDSA",
hash: hashAlg
};
}
return {
name: "RSASSA-PKCS1-v1_5",
hash: hashAlg
};
}
function getKeyAlgorithm(options) {
const keyType = options.keyType || 'rsa';
const hashAlg = getAlgorithmName(options.algorithm || 'sha1');
if (keyType === 'ec') {
const curve = options.curve || 'P-256';
return {
name: "ECDSA",
namedCurve: curve
};
}
return {
name: "RSASSA-PKCS1-v1_5",
modulusLength: options.keySize || 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: hashAlg
};
}
// Build extensions array from options or use defaults
// Supports the old node-forge extension format for backwards compatibility
function buildExtensions(userExtensions, commonName) {
if (!userExtensions || userExtensions.length === 0) {
// Default extensions
return [
new BasicConstraintsExtension(false, undefined, true),
new KeyUsagesExtension(KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, true),
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth, ExtendedKeyUsage.clientAuth], false),
new SubjectAlternativeNameExtension([
{ type: 'dns', value: commonName },
...(commonName === 'localhost' ? [{ type: 'ip', value: '127.0.0.1' }] : [])
], false)
];
}
// Convert user extensions from node-forge format to @peculiar/x509 format
const extensions = [];
for (const ext of userExtensions) {
const critical = ext.critical || false;
switch (ext.name) {
case 'basicConstraints':
extensions.push(new BasicConstraintsExtension(
ext.cA || false,
ext.pathLenConstraint,
critical
));
break;
case 'keyUsage':
let flags = 0;
if (ext.digitalSignature) flags |= KeyUsageFlags.digitalSignature;
if (ext.nonRepudiation || ext.contentCommitment) flags |= KeyUsageFlags.nonRepudiation;
if (ext.keyEncipherment) flags |= KeyUsageFlags.keyEncipherment;
if (ext.dataEncipherment) flags |= KeyUsageFlags.dataEncipherment;
if (ext.keyAgreement) flags |= KeyUsageFlags.keyAgreement;
if (ext.keyCertSign) flags |= KeyUsageFlags.keyCertSign;
if (ext.cRLSign) flags |= KeyUsageFlags.cRLSign;
if (ext.encipherOnly) flags |= KeyUsageFlags.encipherOnly;
if (ext.decipherOnly) flags |= KeyUsageFlags.decipherOnly;
extensions.push(new KeyUsagesExtension(flags, critical));
break;
case 'extKeyUsage':
const usages = [];
if (ext.serverAuth) usages.push(ExtendedKeyUsage.serverAuth);
if (ext.clientAuth) usages.push(ExtendedKeyUsage.clientAuth);
if (ext.codeSigning) usages.push(ExtendedKeyUsage.codeSigning);
if (ext.emailProtection) usages.push(ExtendedKeyUsage.emailProtection);
if (ext.timeStamping) usages.push(ExtendedKeyUsage.timeStamping);
extensions.push(new ExtendedKeyUsageExtension(usages, critical));
break;
case 'subjectAltName':
const altNames = (ext.altNames || []).map(alt => {
// node-forge type values:
// 1 = email (rfc822Name)
// 2 = DNS
// 6 = URI
// 7 = IP
switch (alt.type) {
case 1: // email
return { type: 'email', value: alt.value };
case 2: // DNS
return { type: 'dns', value: alt.value };
case 6: // URI
return { type: 'url', value: alt.value };
case 7: // IP
return { type: 'ip', value: alt.ip || alt.value };
default:
// Try to infer type from properties
if (alt.ip) return { type: 'ip', value: alt.ip };
if (alt.dns) return { type: 'dns', value: alt.dns };
if (alt.email) return { type: 'email', value: alt.email };
if (alt.uri || alt.url) return { type: 'url', value: alt.uri || alt.url };
return { type: 'dns', value: alt.value };
}
});
extensions.push(new SubjectAlternativeNameExtension(altNames, critical));
break;
default:
// Skip unknown extensions with a warning
console.warn(`Unknown extension "${ext.name}" ignored`);
}
}
return extensions;
}
// Convert attributes from node-forge format to X509 name format
function convertAttributes(attrs) {
const nameMap = {
'commonName': 'CN',
'countryName': 'C',
'ST': 'ST',
'localityName': 'L',
'organizationName': 'O',
'OU': 'OU'
};
return attrs.map(attr => {
const key = attr.name || attr.shortName;
const oid = nameMap[key] || key;
return `${oid}=${attr.value}`;
}).join(', ');
}
// Detect key type from PEM key using Node.js crypto
function detectKeyType(pemKey) {
const keyObject = nodeCrypto.createPrivateKey(pemKey);
return keyObject.asymmetricKeyType; // 'rsa' or 'ec'
}
// Map Node.js curve names to Web Crypto curve names
function normalizeECCurve(curveName) {
const curveMap = {
'prime256v1': 'P-256',
'secp384r1': 'P-384',
'secp521r1': 'P-521',
'P-256': 'P-256',
'P-384': 'P-384',
'P-521': 'P-521'
};
return curveMap[curveName] || curveName;
}
// Get EC curve from key object
function getECCurve(keyObject) {
const details = keyObject.asymmetricKeyDetails;
if (details && details.namedCurve) {
return normalizeECCurve(details.namedCurve);
}
return 'P-256'; // default
}
// Convert PEM key to CryptoKey
async function importPrivateKey(pemKey, algorithm, keyType) {
// Auto-detect key type if not provided
const keyObject = nodeCrypto.createPrivateKey(pemKey);
const detectedKeyType = keyObject.asymmetricKeyType;
const actualKeyType = keyType || detectedKeyType;
// Convert to PKCS#8 format
const pkcs8Pem = keyObject.export({ type: 'pkcs8', format: 'pem' });
const pemContents = pkcs8Pem
.replace(/-----BEGIN PRIVATE KEY-----/, '')
.replace(/-----END PRIVATE KEY-----/, '')
.replace(/\s/g, '');
const binaryDer = Buffer.from(pemContents, 'base64');
let importAlgorithm;
if (actualKeyType === 'ec') {
const curve = getECCurve(keyObject);
importAlgorithm = {
name: 'ECDSA',
namedCurve: curve
};
} else {
importAlgorithm = {
name: 'RSASSA-PKCS1-v1_5',
hash: getAlgorithmName(algorithm)
};
}
return await crypto.subtle.importKey(
'pkcs8',
binaryDer,
importAlgorithm,
true,
['sign']
);
}
async function importPublicKey(pemKey, algorithm, keyType, curve) {
const pemContents = pemKey
.replace(/-----BEGIN PUBLIC KEY-----/, '')
.replace(/-----END PUBLIC KEY-----/, '')
.replace(/\s/g, '');
const binaryDer = Buffer.from(pemContents, 'base64');
let importAlgorithm;
if (keyType === 'ec') {
importAlgorithm = {
name: 'ECDSA',
namedCurve: curve || 'P-256'
};
} else {
importAlgorithm = {
name: 'RSASSA-PKCS1-v1_5',
hash: getAlgorithmName(algorithm)
};
}
return await crypto.subtle.importKey(
'spki',
binaryDer,
importAlgorithm,
true,
['verify']
);
}
async function generatePemAsync(keyPair, attrs, options, ca) {
const { privateKey, publicKey } = keyPair;
// Generate serial number
const serialBytes = crypto.getRandomValues(new Uint8Array(9));
const serialHex = toPositiveHex(Buffer.from(serialBytes).toString('hex'));
// Set up dates
const notBefore = options.notBeforeDate || new Date();
let notAfter;
if (options.notAfterDate) {
notAfter = options.notAfterDate;
} else {
notAfter = new Date(notBefore);
notAfter.setDate(notAfter.getDate() + 365);
}
// Default attributes
attrs = attrs || [
{
name: "commonName",
value: "example.org",
},
{
name: "countryName",
value: "US",
},
{
shortName: "ST",
value: "Virginia",
},
{
name: "localityName",
value: "Blacksburg",
},
{
name: "organizationName",
value: "Test",
},
{
shortName: "OU",
value: "Test",
},
];
const subjectName = convertAttributes(attrs);
const keyType = options.keyType || 'rsa';
const signingAlg = getSigningAlgorithm(options.algorithm, keyType);
// Extract common name for SAN extension
const commonNameAttr = attrs.find(attr => attr.name === 'commonName' || attr.shortName === 'CN');
const commonName = commonNameAttr ? commonNameAttr.value : 'localhost';
// Build extensions array
const extensions = buildExtensions(options.extensions, commonName);
let cert;
if (ca) {
// Generate certificate signed by CA
const caCert = new X509Certificate(ca.cert);
const caPrivateKey = await importPrivateKey(ca.key, options.algorithm || "sha256", keyType);
cert = await X509CertificateGenerator.create({
serialNumber: serialHex,
subject: subjectName,
issuer: caCert.subject,
notBefore: notBefore,
notAfter: notAfter,
signingAlgorithm: signingAlg,
publicKey: publicKey,
signingKey: caPrivateKey,
extensions: extensions
});
} else {
// Generate self-signed certificate
cert = await X509CertificateGenerator.createSelfSigned({
serialNumber: serialHex,
name: subjectName,
notBefore: notBefore,
notAfter: notAfter,
signingAlgorithm: signingAlg,
keys: {
privateKey: privateKey,
publicKey: publicKey
},
extensions: extensions
});
}
// Calculate fingerprint (SHA-1 hash of the certificate)
const certRaw = cert.rawData;
const fingerprintBuffer = await crypto.subtle.digest('SHA-1', certRaw);
const fingerprint = Buffer.from(fingerprintBuffer)
.toString('hex')
.match(/.{2}/g)
.join(':');
// Export keys to PEM
const privateKeyDer = await crypto.subtle.exportKey('pkcs8', privateKey);
const publicKeyDer = await crypto.subtle.exportKey('spki', publicKey);
let privatePem;
if (options.passphrase) {
// Encrypt the private key with the passphrase using Node.js crypto
const keyObject = nodeCrypto.createPrivateKey({
key: Buffer.from(privateKeyDer),
format: 'der',
type: 'pkcs8'
});
privatePem = keyObject.export({
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: options.passphrase
});
} else {
privatePem =
'-----BEGIN PRIVATE KEY-----\n' +
Buffer.from(privateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
'\n-----END PRIVATE KEY-----\n';
}
const publicPem =
'-----BEGIN PUBLIC KEY-----\n' +
Buffer.from(publicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
'\n-----END PUBLIC KEY-----\n';
const certPem = cert.toString('pem');
const pem = {
private: privatePem,
public: publicPem,
cert: certPem,
fingerprint: fingerprint,
};
// Client certificate support
if (options && options.clientCertificate) {
// Parse clientCertificate options - can be boolean or object
const clientOpts = typeof options.clientCertificate === 'object' ? options.clientCertificate : {};
// Resolve client certificate options with fallbacks to deprecated options
const clientKeySize = clientOpts.keySize || options.clientCertificateKeySize || 2048;
const clientAlgorithm = clientOpts.algorithm || options.algorithm || "sha1";
const clientCN = clientOpts.cn || options.clientCertificateCN || "John Doe jdoe123";
// Client cert uses same key type and curve as main cert by default
const clientKeyType = clientOpts.keyType || keyType;
const clientCurve = clientOpts.curve || options.curve || 'P-256';
const clientKeyAlg = getKeyAlgorithm({
keyType: clientKeyType,
keySize: clientKeySize,
algorithm: clientAlgorithm,
curve: clientCurve
});
const clientKeyPair = await crypto.subtle.generateKey(
clientKeyAlg,
true,
["sign", "verify"]
);
const clientSerialBytes = crypto.getRandomValues(new Uint8Array(9));
const clientSerialHex = toPositiveHex(Buffer.from(clientSerialBytes).toString('hex'));
// Resolve client certificate validity dates
const clientNotBefore = clientOpts.notBeforeDate || new Date();
let clientNotAfter;
if (clientOpts.notAfterDate) {
clientNotAfter = clientOpts.notAfterDate;
} else {
clientNotAfter = new Date(clientNotBefore);
clientNotAfter.setFullYear(clientNotBefore.getFullYear() + 1);
}
const clientAttrs = JSON.parse(JSON.stringify(attrs));
for (let i = 0; i < clientAttrs.length; i++) {
if (clientAttrs[i].name === "commonName") {
clientAttrs[i] = {
name: "commonName",
value: clientCN
};
}
}
const clientSubjectName = convertAttributes(clientAttrs);
const issuerName = convertAttributes(attrs);
// Signing algorithm for client cert - uses main key type since signed by root
const clientSigningAlg = getSigningAlgorithm(clientAlgorithm, keyType);
// Create client cert signed by root key
const clientCertRaw = await X509CertificateGenerator.create({
serialNumber: clientSerialHex,
subject: clientSubjectName,
issuer: issuerName,
notBefore: clientNotBefore,
notAfter: clientNotAfter,
signingAlgorithm: clientSigningAlg,
publicKey: clientKeyPair.publicKey,
signingKey: privateKey // Sign with root private key
});
// Export client keys
const clientPrivateKeyDer = await crypto.subtle.exportKey('pkcs8', clientKeyPair.privateKey);
const clientPublicKeyDer = await crypto.subtle.exportKey('spki', clientKeyPair.publicKey);
pem.clientprivate =
'-----BEGIN PRIVATE KEY-----\n' +
Buffer.from(clientPrivateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
'\n-----END PRIVATE KEY-----\n';
pem.clientpublic =
'-----BEGIN PUBLIC KEY-----\n' +
Buffer.from(clientPublicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
'\n-----END PUBLIC KEY-----\n';
pem.clientcert = clientCertRaw.toString('pem');
}
// Verify certificate chain
const x509Cert = new X509Certificate(cert.rawData);
const certificates = [x509Cert];
// If CA-signed, include CA cert in the chain for verification
if (ca) {
const caCert = new X509Certificate(ca.cert);
certificates.push(caCert);
}
const chainBuilder = new X509ChainBuilder({
certificates: certificates
});
const chain = await chainBuilder.build(x509Cert);
if (chain.length === 0) {
throw new Error("Certificate could not be verified.");
}
return pem;
}
/**
* Generate a certificate (async)
*
* @param {CertificateField[]} attrs Attributes used for subject.
* @param {object} options
* @param {string} [options.keyType="rsa"] Key type: "rsa" or "ec" (elliptic curve)
* @param {number} [options.keySize=2048] the size for the private key in bits (RSA only)
* @param {string} [options.curve="P-256"] The elliptic curve to use: "P-256", "P-384", or "P-521" (EC only)
* @param {object} [options.extensions] additional extensions for the certificate
* @param {string} [options.algorithm="sha1"] The signature algorithm sha256, sha384, sha512 or sha1
* @param {Date} [options.notBeforeDate=new Date()] The date before which the certificate should not be valid
* @param {Date} [options.notAfterDate] The date after which the certificate should not be valid (default: notBeforeDate + 365 days)
* @param {boolean|object} [options.clientCertificate=false] Generate client cert signed by the original key. Can be `true` for defaults or an options object.
* @param {number} [options.clientCertificate.keySize=2048] Key size for the client certificate in bits (RSA only)
* @param {string} [options.clientCertificate.keyType] Key type for client cert (defaults to main keyType)
* @param {string} [options.clientCertificate.curve] Elliptic curve for client cert (EC only)
* @param {string} [options.clientCertificate.algorithm] Signature algorithm for client cert (defaults to options.algorithm or "sha1")
* @param {string} [options.clientCertificate.cn="John Doe jdoe123"] Client certificate's common name
* @param {Date} [options.clientCertificate.notBeforeDate=new Date()] The date before which the client certificate should not be valid
* @param {Date} [options.clientCertificate.notAfterDate] The date after which the client certificate should not be valid (default: notBeforeDate + 1 year)
* @param {string} [options.clientCertificateCN="John Doe jdoe123"] @deprecated Use options.clientCertificate.cn instead
* @param {number} [options.clientCertificateKeySize] @deprecated Use options.clientCertificate.keySize instead
* @param {object} [options.ca] CA certificate and key for signing (if not provided, generates self-signed)
* @param {string} [options.ca.key] CA private key in PEM format
* @param {string} [options.ca.cert] CA certificate in PEM format
* @param {string} [options.passphrase] Passphrase to encrypt the private key (uses AES-256-CBC)
* @returns {Promise<object>} Promise that resolves with certificate data
*/
exports.generate = async function generate(attrs, options) {
attrs = attrs || undefined;
options = options || {};
const keyType = options.keyType || 'rsa';
const curve = options.curve || 'P-256';
let keyPair;
if (options.keyPair) {
// Import existing key pair
keyPair = {
privateKey: await importPrivateKey(options.keyPair.privateKey, options.algorithm || "sha1", keyType),
publicKey: await importPublicKey(options.keyPair.publicKey, options.algorithm || "sha1", keyType, curve)
};
} else {
// Generate new key pair using appropriate algorithm
const keyAlg = getKeyAlgorithm(options);
keyPair = await crypto.subtle.generateKey(
keyAlg,
true,
["sign", "verify"]
);
}
return await generatePemAsync(keyPair, attrs, options, options.ca);
};
+47
View File
@@ -0,0 +1,47 @@
{
"name": "selfsigned",
"version": "5.5.0",
"description": "Generate self signed certificates private and public keys",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "mocha -t 10000"
},
"repository": {
"type": "git",
"url": "git://github.com/jfromaniello/selfsigned.git"
},
"keywords": [
"openssl",
"self",
"signed",
"certificates",
"x509",
"webcrypto"
],
"author": "José F. Romaniello <jfromaniello@gmail.com> (http://joseoncode.com)",
"contributors": [
{
"name": "Paolo Fragomeni",
"email": "paolo@async.ly",
"url": "http://async.ly"
},
{
"name": "Charles Bushong",
"email": "bushong1@gmail.com",
"url": "http://github.com/bushong1"
}
],
"license": "MIT",
"dependencies": {
"@peculiar/x509": "^1.14.2",
"pkijs": "^3.3.3"
},
"devDependencies": {
"chai": "^4.3.4",
"mocha": "^11.7.5"
},
"engines": {
"node": ">=18"
}
}
+70
View File
@@ -0,0 +1,70 @@
const pkijs = require("pkijs");
const nodeCrypto = require("crypto");
// Use Node.js native webcrypto
const crypto = nodeCrypto.webcrypto;
// Set up pkijs to use native crypto
// Note: This modifies global pkijs state. If the consumer also uses pkijs,
// they should set their own engine or use a version that supports per-instance engines.
let pkijsInitialized = false;
function ensurePkijsInitialized() {
if (!pkijsInitialized) {
pkijs.setEngine("nodeEngine", crypto, new pkijs.CryptoEngine({
name: "",
crypto: crypto,
subtle: crypto.subtle
}));
pkijsInitialized = true;
}
}
/**
* Create PKCS#7 formatted certificate from PEM certificate
*
* @param {string} certPem - PEM formatted certificate
* @returns {string} PKCS#7 PEM formatted certificate
*/
function createPkcs7(certPem) {
ensurePkijsInitialized();
// Parse the PEM certificate to get raw data
const certLines = certPem.split('\n').filter(line =>
!line.includes('BEGIN CERTIFICATE') &&
!line.includes('END CERTIFICATE') &&
line.trim()
);
const certBase64 = certLines.join('');
const certBuffer = Buffer.from(certBase64, 'base64');
// Parse certificate using pkijs
const asn1Cert = pkijs.Certificate.fromBER(certBuffer);
// Create PKCS#7 SignedData structure
const cmsSigned = new pkijs.SignedData({
version: 1,
encapContentInfo: new pkijs.EncapsulatedContentInfo({
eContentType: "1.2.840.113549.1.7.1" // data
}),
certificates: [asn1Cert]
});
// Wrap in ContentInfo
const cmsSignedSchema = cmsSigned.toSchema();
const cmsContentInfo = new pkijs.ContentInfo({
contentType: "1.2.840.113549.1.7.2", // signedData
content: cmsSignedSchema
});
// Convert to DER and then PEM
const cmsSignedDer = cmsContentInfo.toSchema().toBER(false);
const pkcs7Pem =
'-----BEGIN PKCS7-----\n' +
Buffer.from(cmsSignedDer).toString('base64').match(/.{1,64}/g).join('\n') +
'\n-----END PKCS7-----\n';
return pkcs7Pem;
}
module.exports = { createPkcs7 };
+242
View File
@@ -0,0 +1,242 @@
var { assert } = require('chai');
var crypto = require('crypto');
describe('CA signing', function () {
var generate = require('../index').generate;
it('should generate certificate signed by provided CA', async function () {
// First generate a self-signed CA certificate
const ca = await generate([
{ name: 'commonName', value: 'Test CA' },
{ name: 'organizationName', value: 'Test Organization' }
], {
algorithm: 'sha256'
});
// Generate a certificate signed by the CA
const pems = await generate([
{ name: 'commonName', value: 'localhost' }
], {
algorithm: 'sha256',
ca: {
key: ca.private,
cert: ca.cert
}
});
assert.ok(!!pems.private, 'has a private key');
assert.ok(!!pems.public, 'has a public key');
assert.ok(!!pems.cert, 'has a certificate');
assert.ok(!!pems.fingerprint, 'has fingerprint');
const cert = new crypto.X509Certificate(pems.cert);
const caCert = new crypto.X509Certificate(ca.cert);
// Verify issuer is the CA, not self-signed
assert.include(cert.issuer, 'CN=Test CA', 'issuer should be the CA');
assert.include(cert.subject, 'CN=localhost', 'subject should be localhost');
assert.notEqual(cert.issuer, cert.subject, 'should not be self-signed');
// Verify the certificate is signed by the CA
assert.isTrue(cert.verify(caCert.publicKey), 'certificate should be verified by CA public key');
});
it('should include Subject Alternative Name extension', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256'
});
const pems = await generate([
{ name: 'commonName', value: 'example.com' }
], {
algorithm: 'sha256',
ca: { key: ca.private, cert: ca.cert }
});
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subjectAltName, 'DNS:example.com', 'should have DNS SAN matching CN');
});
it('should include IP SAN for localhost', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256'
});
const pems = await generate([
{ name: 'commonName', value: 'localhost' }
], {
algorithm: 'sha256',
ca: { key: ca.private, cert: ca.cert }
});
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subjectAltName, 'DNS:localhost', 'should have DNS SAN');
assert.include(cert.subjectAltName, 'IP Address:127.0.0.1', 'should have IP SAN for localhost');
});
it('should support different hash algorithms with CA signing', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256'
});
// Test sha384
const pems384 = await generate([{ name: 'commonName', value: 'test384.local' }], {
algorithm: 'sha384',
ca: { key: ca.private, cert: ca.cert }
});
const cert384 = new crypto.X509Certificate(pems384.cert);
assert.ok(cert384.publicKey, 'should generate sha384 CA-signed cert');
// Test sha512
const pems512 = await generate([{ name: 'commonName', value: 'test512.local' }], {
algorithm: 'sha512',
ca: { key: ca.private, cert: ca.cert }
});
const cert512 = new crypto.X509Certificate(pems512.cert);
assert.ok(cert512.publicKey, 'should generate sha512 CA-signed cert');
});
it('should respect notAfterDate option with CA signing', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256'
});
const notBefore = new Date('2025-01-01T00:00:00Z');
const notAfter = new Date('2025-01-31T00:00:00Z'); // 30 days validity
const pems = await generate([{ name: 'commonName', value: 'short-lived.local' }], {
algorithm: 'sha256',
notBeforeDate: notBefore,
notAfterDate: notAfter,
ca: { key: ca.private, cert: ca.cert }
});
const cert = new crypto.X509Certificate(pems.cert);
const validFrom = new Date(cert.validFrom);
const validTo = new Date(cert.validTo);
assert.approximately(validFrom.getTime(), notBefore.getTime(), 5000, 'should use custom notBeforeDate');
assert.approximately(validTo.getTime(), notAfter.getTime(), 5000, 'should use custom notAfterDate');
});
it('should generate unique certificates with same CA', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256'
});
const pems1 = await generate([{ name: 'commonName', value: 'test1.local' }], {
algorithm: 'sha256',
ca: { key: ca.private, cert: ca.cert }
});
const pems2 = await generate([{ name: 'commonName', value: 'test2.local' }], {
algorithm: 'sha256',
ca: { key: ca.private, cert: ca.cert }
});
const cert1 = new crypto.X509Certificate(pems1.cert);
const cert2 = new crypto.X509Certificate(pems2.cert);
assert.notEqual(cert1.serialNumber, cert2.serialNumber, 'serial numbers should be unique');
assert.notEqual(pems1.private, pems2.private, 'private keys should be different');
});
it('should work with custom keySize and CA signing', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256',
keySize: 4096
});
const pems = await generate([{ name: 'commonName', value: 'bigkey.local' }], {
algorithm: 'sha256',
keySize: 4096,
ca: { key: ca.private, cert: ca.cert }
});
const privateKey = crypto.createPrivateKey(pems.private);
assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 4096, 'should use custom key size');
const cert = new crypto.X509Certificate(pems.cert);
const caCert = new crypto.X509Certificate(ca.cert);
assert.isTrue(cert.verify(caCert.publicKey), 'certificate should verify with CA');
});
it('should support existing keyPair with CA signing', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256'
});
// Generate a key pair first
const keyPair = await generate([{ name: 'commonName', value: 'keypair.local' }], {
algorithm: 'sha256'
});
// Use existing key pair with CA signing
const pems = await generate([{ name: 'commonName', value: 'reused.local' }], {
algorithm: 'sha256',
keyPair: {
privateKey: keyPair.private,
publicKey: keyPair.public
},
ca: { key: ca.private, cert: ca.cert }
});
assert.strictEqual(pems.private, keyPair.private, 'should use provided private key');
assert.strictEqual(pems.public, keyPair.public, 'should use provided public key');
const cert = new crypto.X509Certificate(pems.cert);
const caCert = new crypto.X509Certificate(ca.cert);
assert.isTrue(cert.verify(caCert.publicKey), 'certificate should verify with CA');
});
it('should include proper extended key usage extensions', async function () {
const ca = await generate([{ name: 'commonName', value: 'Test CA' }], {
algorithm: 'sha256'
});
const pems = await generate([{ name: 'commonName', value: 'server.local' }], {
algorithm: 'sha256',
ca: { key: ca.private, cert: ca.cert }
});
const cert = new crypto.X509Certificate(pems.cert);
// Check extended key usage (OIDs)
// 1.3.6.1.5.5.7.3.1 = serverAuth
// 1.3.6.1.5.5.7.3.2 = clientAuth
assert.include(cert.keyUsage, '1.3.6.1.5.5.7.3.1', 'should have serverAuth extended key usage');
assert.include(cert.keyUsage, '1.3.6.1.5.5.7.3.2', 'should have clientAuth extended key usage');
});
it('should work with PKCS#1 RSA key format', async function () {
// Generate a CA with PKCS#1 format key (like mkcert uses)
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
publicKeyEncoding: { type: 'spki', format: 'pem' }
});
// Create a self-signed CA cert using the PKCS#1 key
const ca = await generate([{ name: 'commonName', value: 'PKCS1 CA' }], {
algorithm: 'sha256'
});
// Now test that we can use a PKCS#1 formatted key as CA
// Convert our generated key to PKCS#1 for testing
const caKeyObject = crypto.createPrivateKey(ca.private);
const pkcs1Key = caKeyObject.export({ type: 'pkcs1', format: 'pem' });
const pems = await generate([{ name: 'commonName', value: 'pkcs1-test.local' }], {
algorithm: 'sha256',
ca: {
key: pkcs1Key,
cert: ca.cert
}
});
const cert = new crypto.X509Certificate(pems.cert);
const caCert = new crypto.X509Certificate(ca.cert);
assert.isTrue(cert.verify(caCert.publicKey), 'should work with PKCS#1 key format');
});
});
+142
View File
@@ -0,0 +1,142 @@
var { assert } = require('chai');
var crypto = require('crypto');
describe('EC keys', function () {
var generate = require('../index').generate;
it('should generate EC certificate with P-256 curve (default)', async function () {
var pems = await generate(null, { keyType: 'ec' });
assert.ok(!!pems.private, 'has a private key');
assert.ok(!!pems.public, 'has a public key');
assert.ok(!!pems.cert, 'has a certificate');
assert.ok(!!pems.fingerprint, 'has fingerprint');
const privateKey = crypto.createPrivateKey(pems.private);
assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'should be EC key');
assert.strictEqual(privateKey.asymmetricKeyDetails.namedCurve, 'prime256v1', 'should use P-256 curve');
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.subject, 'cert has a subject');
});
it('should generate EC certificate with P-384 curve', async function () {
var pems = await generate(null, { keyType: 'ec', curve: 'P-384' });
const privateKey = crypto.createPrivateKey(pems.private);
assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'should be EC key');
assert.strictEqual(privateKey.asymmetricKeyDetails.namedCurve, 'secp384r1', 'should use P-384 curve');
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.publicKey, 'can generate P-384 EC certs');
});
it('should generate EC certificate with P-521 curve', async function () {
var pems = await generate(null, { keyType: 'ec', curve: 'P-521' });
const privateKey = crypto.createPrivateKey(pems.private);
assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'should be EC key');
assert.strictEqual(privateKey.asymmetricKeyDetails.namedCurve, 'secp521r1', 'should use P-521 curve');
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.publicKey, 'can generate P-521 EC certs');
});
it('should generate valid EC key pair that work together', async function () {
var pems = await generate(null, { keyType: 'ec', curve: 'P-256' });
const testData = 'Hello, World!';
const privateKey = crypto.createPrivateKey(pems.private);
const publicKey = crypto.createPublicKey(pems.public);
// Sign with private key
const sign = crypto.createSign('SHA256');
sign.update(testData);
sign.end();
const signature = sign.sign(privateKey);
// Verify with public key
const verify = crypto.createVerify('SHA256');
verify.update(testData);
verify.end();
const isValid = verify.verify(publicKey, signature);
assert.isTrue(isValid, 'EC public key should verify signature from EC private key');
});
it('should support EC with sha256 algorithm', async function () {
var pems = await generate(null, { keyType: 'ec', algorithm: 'sha256' });
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.publicKey, 'can generate EC cert with sha256');
});
it('should support EC with sha384 algorithm', async function () {
var pems = await generate(null, { keyType: 'ec', curve: 'P-384', algorithm: 'sha384' });
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.publicKey, 'can generate EC cert with sha384');
});
it('should support EC with sha512 algorithm', async function () {
var pems = await generate(null, { keyType: 'ec', curve: 'P-521', algorithm: 'sha512' });
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.publicKey, 'can generate EC cert with sha512');
});
it('should generate EC client certificate', async function () {
var pems = await generate(null, { keyType: 'ec', clientCertificate: true });
assert.ok(!!pems.clientcert, 'should include a client cert');
assert.ok(!!pems.clientprivate, 'should include a client private key');
assert.ok(!!pems.clientpublic, 'should include a client public key');
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
assert.strictEqual(clientPrivateKey.asymmetricKeyType, 'ec', 'client key should be EC');
});
it('should support passphrase with EC keys', async function () {
const passphrase = 'ec-secret-passphrase';
var pems = await generate(null, { keyType: 'ec', passphrase: passphrase });
assert.include(pems.private, 'ENCRYPTED', 'EC private key should be encrypted');
const privateKey = crypto.createPrivateKey({
key: pems.private,
passphrase: passphrase
});
assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'decrypted key should be EC');
});
it('should support using existing EC keyPair', async function () {
const firstPems = await generate(null, { keyType: 'ec', curve: 'P-256' });
const secondPems = await generate(null, {
keyType: 'ec',
curve: 'P-256',
keyPair: {
privateKey: firstPems.private,
publicKey: firstPems.public
}
});
assert.strictEqual(firstPems.private, secondPems.private, 'should use provided EC private key');
assert.strictEqual(firstPems.public, secondPems.public, 'should use provided EC public key');
});
it('should support custom attributes with EC', async function () {
const attrs = [
{ name: 'commonName', value: 'ec-test.example.com' },
{ name: 'countryName', value: 'US' },
{ name: 'organizationName', value: 'EC Test Corp' }
];
var pems = await generate(attrs, { keyType: 'ec' });
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subject, 'CN=ec-test.example.com', 'should include custom CN');
assert.include(cert.subject, 'O=EC Test Corp', 'should include custom organization');
});
});
+605
View File
@@ -0,0 +1,605 @@
var { assert } = require('chai');
var fs = require('fs');
var { promisify } = require('util');
var exec = promisify(require('child_process').exec);
var crypto = require('crypto');
describe('generate', function () {
var generate = require('../index').generate;
var { createPkcs7 } = require('../pkcs7');
it('should work without attrs/options', async function () {
var pems = await generate();
assert.ok(!!pems.private, 'has a private key');
assert.ok(!!pems.fingerprint, 'has fingerprint');
assert.ok(!!pems.public, 'has a public key');
assert.ok(!!pems.cert, 'has a certificate');
assert.ok(!pems.pkcs7, 'should not include a pkcs7 by default');
assert.ok(!pems.clientcert, 'should not include a client cert by default');
assert.ok(!pems.clientprivate, 'should not include a client private key by default');
assert.ok(!pems.clientpublic, 'should not include a client public key by default');
// Verify cert can be read by Node.js crypto
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.subject, 'cert has a subject');
});
it('should generate client cert', async function () {
var pems = await generate(null, {clientCertificate: true});
assert.ok(!!pems.clientcert, 'should include a client cert when requested');
assert.ok(!!pems.clientprivate, 'should include a client private key when requested');
assert.ok(!!pems.clientpublic, 'should include a client public key when requested');
});
it('should include pkcs7', async function () {
var pems = await generate([{ name: 'commonName', value: 'contoso.com' }]);
var pkcs7 = createPkcs7(pems.cert);
assert.ok(!!pkcs7, 'has a pkcs7');
try {
fs.unlinkSync('/tmp/tmp.pkcs7');
} catch (er) {}
fs.writeFileSync('/tmp/tmp.pkcs7', pkcs7);
const { stdout, stderr } = await exec('openssl pkcs7 -print_certs -in /tmp/tmp.pkcs7');
if (stderr && stderr.length) {
throw new Error(stderr);
}
const expected = stdout.toString();
let [ subjectLine,issuerLine, ...cert ] = expected.split(/\r?\n/).filter(c => c);
cert = cert.filter(c => c);
assert.match(subjectLine, /subject=\/?CN\s?=\s?contoso.com/i);
assert.match(issuerLine, /issuer=\/?CN\s?=\s?contoso.com/i);
// Normalize line endings for comparison
const normalizedPemCert = pems.cert.replace(/\r\n/g, '\n').trim();
const normalizedExpected = cert.join('\n').trim();
assert.strictEqual(
normalizedPemCert,
normalizedExpected
);
});
it('should support sha1 algorithm', async function () {
var pems_sha1 = await generate(null, { algorithm: 'sha1' });
const cert = new crypto.X509Certificate(pems_sha1.cert);
// SHA-1 with RSA signature
assert.ok(cert.publicKey, 'can generate sha1 certs');
});
it('should support sha256 algorithm', async function () {
var pems_sha256 = await generate(null, { algorithm: 'sha256' });
const cert = new crypto.X509Certificate(pems_sha256.cert);
// SHA-256 with RSA signature
assert.ok(cert.publicKey, 'can generate sha256 certs');
});
it('should default to 2048 bit keysize', async function () {
var pems = await generate();
const privateKey = crypto.createPrivateKey(pems.private);
const keyDetails = privateKey.asymmetricKeyDetails;
assert.strictEqual(keyDetails.modulusLength, 2048, 'default key size should be 2048 bits');
});
it('should default to 2048 bit keysize for client certificate', async function () {
var pems = await generate(null, {clientCertificate: true});
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
assert.strictEqual(keyDetails.modulusLength, 2048, 'default client key size should be 2048 bits');
});
it('should support custom keySize', async function () {
var pems = await generate(null, { keySize: 4096 });
const privateKey = crypto.createPrivateKey(pems.private);
const keyDetails = privateKey.asymmetricKeyDetails;
assert.strictEqual(keyDetails.modulusLength, 4096, 'should support custom key size');
});
it('should support custom clientCertificateKeySize', async function () {
var pems = await generate(null, {
clientCertificate: true,
clientCertificateKeySize: 4096
});
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
assert.strictEqual(keyDetails.modulusLength, 4096, 'should support custom client key size');
});
it('should support sha384 algorithm', async function () {
var pems = await generate(null, { algorithm: 'sha384' });
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.publicKey, 'can generate sha384 certs');
});
it('should support sha512 algorithm', async function () {
var pems = await generate(null, { algorithm: 'sha512' });
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.publicKey, 'can generate sha512 certs');
});
it('should default to 365 days validity', async function () {
var pems = await generate();
const cert = new crypto.X509Certificate(pems.cert);
const validFrom = new Date(cert.validFrom);
const validTo = new Date(cert.validTo);
const diffTime = Math.abs(validTo - validFrom);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
assert.approximately(diffDays, 365, 1, 'certificate should default to 365 days validity');
});
it('should respect notBeforeDate option', async function () {
const customDate = new Date('2025-01-01T00:00:00Z');
var pems = await generate(null, { notBeforeDate: customDate });
const cert = new crypto.X509Certificate(pems.cert);
const validFrom = new Date(cert.validFrom);
// Allow small difference for processing time
assert.approximately(validFrom.getTime(), customDate.getTime(), 5000, 'should use custom notBeforeDate');
});
it('should respect notAfterDate option', async function () {
const notBefore = new Date('2025-01-01T00:00:00Z');
const notAfter = new Date('2025-02-15T00:00:00Z');
var pems = await generate(null, { notBeforeDate: notBefore, notAfterDate: notAfter });
const cert = new crypto.X509Certificate(pems.cert);
const validFrom = new Date(cert.validFrom);
const validTo = new Date(cert.validTo);
assert.approximately(validFrom.getTime(), notBefore.getTime(), 5000, 'should use custom notBeforeDate');
assert.approximately(validTo.getTime(), notAfter.getTime(), 5000, 'should use custom notAfterDate');
});
it('should generate valid fingerprint format', async function () {
var pems = await generate();
assert.match(pems.fingerprint, /^[0-9a-f]{2}(:[0-9a-f]{2}){19}$/i, 'fingerprint should be valid SHA-1 format');
});
it('should support custom attributes', async function () {
const attrs = [
{ name: 'commonName', value: 'test.example.com' },
{ name: 'countryName', value: 'GB' },
{ shortName: 'ST', value: 'London' },
{ name: 'localityName', value: 'Westminster' },
{ name: 'organizationName', value: 'Test Corp' },
{ shortName: 'OU', value: 'Engineering' }
];
var pems = await generate(attrs);
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subject, 'CN=test.example.com', 'should include custom CN');
assert.include(cert.subject, 'C=GB', 'should include custom country');
assert.include(cert.subject, 'O=Test Corp', 'should include custom organization');
});
it('should support custom clientCertificateCN (deprecated)', async function () {
var pems = await generate(null, {
clientCertificate: true,
clientCertificateCN: 'Custom User CN'
});
const clientCert = new crypto.X509Certificate(pems.clientcert);
assert.include(clientCert.subject, 'CN=Custom User CN', 'should use custom client CN');
});
it('should support clientCertificate as options object with cn', async function () {
var pems = await generate(null, {
clientCertificate: {
cn: 'Client Options CN'
}
});
const clientCert = new crypto.X509Certificate(pems.clientcert);
assert.include(clientCert.subject, 'CN=Client Options CN', 'should use cn from clientCertificate options');
});
it('should support clientCertificate.keySize', async function () {
var pems = await generate(null, {
clientCertificate: {
keySize: 4096
}
});
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
assert.strictEqual(keyDetails.modulusLength, 4096, 'should use keySize from clientCertificate options');
});
it('should support clientCertificate.notBeforeDate and notAfterDate', async function () {
const notBefore = new Date('2025-06-01T00:00:00Z');
const notAfter = new Date('2025-06-30T00:00:00Z');
var pems = await generate(null, {
clientCertificate: {
notBeforeDate: notBefore,
notAfterDate: notAfter
}
});
const clientCert = new crypto.X509Certificate(pems.clientcert);
const validFrom = new Date(clientCert.validFrom);
const validTo = new Date(clientCert.validTo);
assert.approximately(validFrom.getTime(), notBefore.getTime(), 5000, 'should use notBeforeDate from clientCertificate options');
assert.approximately(validTo.getTime(), notAfter.getTime(), 5000, 'should use notAfterDate from clientCertificate options');
});
it('should support clientCertificate.algorithm', async function () {
var pems = await generate(null, {
algorithm: 'sha1', // main cert uses sha1
clientCertificate: {
algorithm: 'sha256' // client cert uses sha256
}
});
// Both certs should be valid
const serverCert = new crypto.X509Certificate(pems.cert);
const clientCert = new crypto.X509Certificate(pems.clientcert);
assert.ok(serverCert.publicKey, 'server cert should be valid');
assert.ok(clientCert.publicKey, 'client cert should be valid');
});
it('clientCertificate options should take precedence over deprecated options', async function () {
var pems = await generate(null, {
clientCertificateCN: 'Deprecated CN',
clientCertificateKeySize: 2048,
clientCertificate: {
cn: 'New Options CN',
keySize: 4096
}
});
const clientCert = new crypto.X509Certificate(pems.clientcert);
assert.include(clientCert.subject, 'CN=New Options CN', 'clientCertificate.cn should take precedence');
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
assert.strictEqual(keyDetails.modulusLength, 4096, 'clientCertificate.keySize should take precedence');
});
it('should generate valid key pair that work together', async function () {
var pems = await generate();
// Test data
const testData = 'Hello, World!';
// Create sign and verify objects
const privateKey = crypto.createPrivateKey(pems.private);
const publicKey = crypto.createPublicKey(pems.public);
// Sign with private key
const sign = crypto.createSign('SHA256');
sign.update(testData);
sign.end();
const signature = sign.sign(privateKey);
// Verify with public key
const verify = crypto.createVerify('SHA256');
verify.update(testData);
verify.end();
const isValid = verify.verify(publicKey, signature);
assert.isTrue(isValid, 'public key should verify signature from private key');
});
it('should create client cert signed by server cert', async function () {
var pems = await generate(null, { clientCertificate: true });
const serverCert = new crypto.X509Certificate(pems.cert);
const clientCert = new crypto.X509Certificate(pems.clientcert);
// Client cert should have different subject than server
assert.notEqual(clientCert.subject, serverCert.subject, 'client and server should have different subjects');
// Both certs should be valid
assert.ok(serverCert.publicKey, 'server cert should be valid');
assert.ok(clientCert.publicKey, 'client cert should be valid');
});
it('should support using existing keyPair', async function () {
// First generate a key pair
const firstPems = await generate();
// Reuse the key pair
const secondPems = await generate(null, {
keyPair: {
privateKey: firstPems.private,
publicKey: firstPems.public
}
});
// Keys should be identical
assert.strictEqual(firstPems.private, secondPems.private, 'should use provided private key');
assert.strictEqual(firstPems.public, secondPems.public, 'should use provided public key');
// Certificates will be different (different serial, dates) but keys are same
const firstCert = new crypto.X509Certificate(firstPems.cert);
const secondCert = new crypto.X509Certificate(secondPems.cert);
assert.strictEqual(firstCert.publicKey.export({ format: 'pem', type: 'spki' }),
secondCert.publicKey.export({ format: 'pem', type: 'spki' }),
'certificates should contain the same public key');
});
it('should create PKCS#7 for client certificate', async function () {
var pems = await generate([{ name: 'commonName', value: 'server.example.com' }], {
clientCertificate: true
});
var clientPkcs7 = createPkcs7(pems.clientcert);
assert.ok(!!clientPkcs7, 'should create PKCS#7 for client cert');
assert.include(clientPkcs7, 'BEGIN PKCS7', 'should be valid PKCS#7 format');
// Verify with openssl
try {
fs.unlinkSync('/tmp/tmp-client.pkcs7');
} catch (er) {}
fs.writeFileSync('/tmp/tmp-client.pkcs7', clientPkcs7);
const { stdout, stderr } = await exec('openssl pkcs7 -print_certs -in /tmp/tmp-client.pkcs7');
if (stderr && stderr.length) {
throw new Error(stderr);
}
assert.ok(stdout.toString().length > 0, 'openssl should be able to read client PKCS#7');
});
it('should generate unique serial numbers', async function () {
const pems1 = await generate();
const pems2 = await generate();
const cert1 = new crypto.X509Certificate(pems1.cert);
const cert2 = new crypto.X509Certificate(pems2.cert);
assert.notEqual(cert1.serialNumber, cert2.serialNumber, 'serial numbers should be unique');
});
it('should handle minimal attributes', async function () {
const attrs = [{ name: 'commonName', value: 'minimal.test' }];
var pems = await generate(attrs);
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subject, 'CN=minimal.test', 'should work with minimal attributes');
});
describe('extensions', function () {
it('should support custom subjectAltName with IPv6 (issue #79)', async function () {
var pems = await generate(
[{ name: 'commonName', value: 'localhost' }],
{
algorithm: 'sha256',
extensions: [
{
name: 'basicConstraints',
cA: false
},
{
name: 'keyUsage',
digitalSignature: true,
keyEncipherment: true
},
{
name: 'subjectAltName',
altNames: [
{ type: 2, value: 'localhost' }, // DNS
{ type: 7, ip: '127.0.0.1' }, // IPv4
{ type: 7, ip: '::1' } // IPv6
]
}
]
}
);
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.subjectAltName, 'should have subjectAltName');
assert.include(cert.subjectAltName, 'localhost', 'should include DNS name');
assert.include(cert.subjectAltName, '127.0.0.1', 'should include IPv4');
// IPv6 ::1 may be expanded to full form 0:0:0:0:0:0:0:1
const hasIPv6 = cert.subjectAltName.includes('::1') || cert.subjectAltName.includes('0:0:0:0:0:0:0:1');
assert.ok(hasIPv6, 'should include IPv6');
});
it('should support basicConstraints with cA=true', async function () {
var pems = await generate(
[{ name: 'commonName', value: 'Test CA' }],
{
extensions: [
{
name: 'basicConstraints',
cA: true,
critical: true
},
{
name: 'keyUsage',
keyCertSign: true,
cRLSign: true,
critical: true
}
]
}
);
const cert = new crypto.X509Certificate(pems.cert);
assert.ok(cert.ca, 'certificate should be a CA');
});
it('should support keyUsage extension', async function () {
var pems = await generate(
[{ name: 'commonName', value: 'test.example.com' }],
{
extensions: [
{
name: 'basicConstraints',
cA: false
},
{
name: 'keyUsage',
digitalSignature: true,
keyEncipherment: true,
dataEncipherment: true
}
]
}
);
const cert = new crypto.X509Certificate(pems.cert);
// Node.js X509Certificate doesn't expose keyUsage directly,
// but we can verify the cert is valid and can be used
assert.ok(cert.publicKey, 'should generate valid cert with keyUsage');
// Verify by using openssl to check extensions
const fs = require('fs');
fs.writeFileSync('/tmp/test-keyusage.crt', pems.cert);
const { execSync } = require('child_process');
const output = execSync('openssl x509 -in /tmp/test-keyusage.crt -text -noout').toString();
assert.include(output, 'Digital Signature', 'should have digitalSignature');
assert.include(output, 'Key Encipherment', 'should have keyEncipherment');
});
it('should support extKeyUsage extension', async function () {
var pems = await generate(
[{ name: 'commonName', value: 'test.example.com' }],
{
extensions: [
{
name: 'basicConstraints',
cA: false
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true
}
]
}
);
const cert = new crypto.X509Certificate(pems.cert);
// Node.js crypto doesn't expose extended key usage directly, but cert should be valid
assert.ok(cert.publicKey, 'should generate valid cert with extKeyUsage');
});
it('should support subjectAltName with DNS names', async function () {
var pems = await generate(
[{ name: 'commonName', value: 'example.com' }],
{
extensions: [
{
name: 'basicConstraints',
cA: false
},
{
name: 'subjectAltName',
altNames: [
{ type: 2, value: 'example.com' },
{ type: 2, value: 'www.example.com' },
{ type: 2, value: '*.example.com' }
]
}
]
}
);
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subjectAltName, 'example.com', 'should include example.com');
assert.include(cert.subjectAltName, 'www.example.com', 'should include www.example.com');
assert.include(cert.subjectAltName, '*.example.com', 'should include wildcard');
});
it('should support subjectAltName with email and URI', async function () {
var pems = await generate(
[{ name: 'commonName', value: 'test.example.com' }],
{
extensions: [
{
name: 'basicConstraints',
cA: false
},
{
name: 'subjectAltName',
altNames: [
{ type: 2, value: 'test.example.com' },
{ type: 1, value: 'admin@example.com' }, // email
{ type: 6, value: 'http://example.com/webid#me' } // URI
]
}
]
}
);
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subjectAltName, 'test.example.com', 'should include DNS');
assert.include(cert.subjectAltName, 'admin@example.com', 'should include email');
assert.include(cert.subjectAltName, 'http://example.com/webid#me', 'should include URI');
});
it('should use default extensions when extensions option is empty array', async function () {
var pems = await generate(
[{ name: 'commonName', value: 'localhost' }],
{
extensions: []
}
);
const cert = new crypto.X509Certificate(pems.cert);
// Default behavior includes localhost and 127.0.0.1
assert.include(cert.subjectAltName, 'localhost', 'should use default SAN');
assert.include(cert.subjectAltName, '127.0.0.1', 'should include default IP for localhost');
});
it('should use default extensions when extensions option is not provided', async function () {
var pems = await generate([{ name: 'commonName', value: 'myhost.local' }]);
const cert = new crypto.X509Certificate(pems.cert);
assert.include(cert.subjectAltName, 'myhost.local', 'should use commonName in default SAN');
});
});
it('should support passphrase for private key encryption', async function () {
const passphrase = 'my-secret-passphrase';
var pems = await generate(null, { passphrase: passphrase });
assert.ok(!!pems.private, 'has a private key');
assert.include(pems.private, 'ENCRYPTED', 'private key should be encrypted');
// Verify the key can be decrypted with the correct passphrase
const privateKey = crypto.createPrivateKey({
key: pems.private,
passphrase: passphrase
});
assert.ok(privateKey, 'should be able to decrypt private key with passphrase');
// Verify signing works with decrypted key
const testData = 'Hello, World!';
const sign = crypto.createSign('SHA256');
sign.update(testData);
sign.end();
const signature = sign.sign({ key: pems.private, passphrase: passphrase });
const verify = crypto.createVerify('SHA256');
verify.update(testData);
verify.end();
const isValid = verify.verify(pems.public, signature);
assert.isTrue(isValid, 'encrypted key should work for signing');
});
it('should fail to decrypt private key with wrong passphrase', async function () {
const passphrase = 'correct-passphrase';
var pems = await generate(null, { passphrase: passphrase });
assert.throws(() => {
crypto.createPrivateKey({
key: pems.private,
passphrase: 'wrong-passphrase'
});
});
});
});