diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..618a234
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2352650..edca6e2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,19 +1,75 @@
-name: CI
on:
push:
- branches: ['master']
+ branches:
+ - main
+ - 'v*'
pull_request:
- branches: ['*']
+ paths-ignore:
+ - LICENSE
+ - '*.md'
+
+name: CI
+
jobs:
- test:
+ lint:
+ permissions:
+ contents: read
+ name: Lint
runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Install Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: v24.x
+ cache: 'npm'
+ cache-dependency-path: package.json
+
+ - name: Install dependencies
+ run: npm install
+ - name: Check linting
+ run: npm run lint:ci
+
+ tests:
+ permissions:
+ contents: read
+ name: Tests
strategy:
+ fail-fast: false
matrix:
- node-version: [18, 17, 16, 14]
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ node-version: [20, 22, 24]
+ runs-on: ${{matrix.os}}
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ matrix.node-version }}
- - run: npm install
- - run: npm test
+ cache: 'npm'
+ cache-dependency-path: package.json
+
+ - name: Install Dependencies
+ run: npm install
+
+ - name: Run Tests
+ # TODO: extend to test:ci
+ run: npm run test
+
+ automerge:
+ if: >
+ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
+ needs:
+ - tests
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: Merge Dependabot PR
+ uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d2e8040
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,18 @@
+name: Release
+on:
+ push:
+ tags:
+ - 'v[0-9]+.[0-9]+.[0-9]+'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ name: Release Creation
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1.18.0
+ with:
+ bodyFile: 'CHANGELOG.md'
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 71ba149..7cca3ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,147 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.*
+!.env.example
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+.output
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp directory
+.temp
+
+# Sveltekit cache directory
+.svelte-kit/
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# Firebase cache directory
+.firebase/
+
+# TernJS port file
+.tern-port
+
+# Stores Visual Studio Code versions used for testing Visual Studio Code extensions
+.vscode-test
+
+# pnpm
+.pnpm-store
+
+# yarn v3
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+# Vite files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+.vite/
+
cert.pem
key.pem
-node_modules
+.worktrees
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..849adc0
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,150 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.*
+!.env.example
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+.output
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp directory
+.temp
+
+# Sveltekit cache directory
+.svelte-kit/
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# Firebase cache directory
+.firebase/
+
+# TernJS port file
+.tern-port
+
+# Stores Visual Studio Code versions used for testing Visual Studio Code extensions
+.vscode-test
+
+# pnpm
+.pnpm-store
+
+# yarn v3
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+# Vite files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+.vite/
+
+cert.pem
+key.pem
+.github
+.git
+tsconfig.json
+.worktrees
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..9cf9495
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+package-lock=false
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3f8d8e5
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+### [1.0.1](https://github.com/metcoder95/https-pem/compare/v1.0.0...v1.0.1) (2026-09-14)
+
+
+### Bug Fixes
+
+* retry certificate generation on serial numbers OpenSSL rejects ([#2](https://github.com/metcoder95/https-pem/issues/2)) ([6d42161](https://github.com/metcoder95/https-pem/commit/6d421619cc71b9e5da1b51f18efa27c19cbe32fe))
+
+## 1.0.0 (2025-08-08)
+
+
+### Features
+
+* v1 ([#1](https://github.com/metcoder95/https-pem/issues/1)) ([b64af0b](https://github.com/metcoder95/https-pem/commit/b64af0b16b5e96957f5f25a0c52669b0cc00a81a))
diff --git a/README.md b/README.md
index c6958af..5828c7a 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,19 @@
-# https-pem
+# @metcoder95/https-pem
+
+> This is a fork of the original [https-pem](https://github.com/watson/https-pem) package, modified to work with Node.js 24 and above.
Self-signed PEM key and certificate ready for use in your HTTPS server.
A dead simple way to get an HTTPS server running in development with no
need to generate the self signed PEM key and certificate.
-[](https://travis-ci.org/watson/https-pem)
+[](https://github.com/metcoder95/https-pem/actions/workflows/ci.yml)
[](https://github.com/feross/standard)
## Installation
```
-npm install https-pem
+npm install @metcoder95/https-pem
```
**Warning:** Upon installation a private key and a self signed
@@ -21,36 +23,38 @@ this secure in any way. I suggest only using this for testing and
development where you just need an easy and quick way to run an HTTPS
server with Node.js.
+> **Note**: for Node.js 24 and above, the key size has been increased to 2048 bits due to changes in OpenSSL. For earlier versions, the default key size is used.
+
## Example Usage
```js
-var https = require('https')
-var pem = require('https-pem')
+const https = require('node:https');
+const pem = require('@metcoder95/https-pem');
-var server = https.createServer(pem, function (req, res) {
- res.end('This is servered over HTTPS')
-})
+const server = https.createServer(pem, function (req, res) {
+ res.end('This is servered over HTTPS');
+});
server.listen(443, function () {
- console.log('The server is running on https://localhost')
-})
+ console.log('The server is running on https://localhost');
+});
```
### Connecting
When connecting to an HTTPS server from Node.js that uses a self-signed
certificate, `https.request` will normally emit an `error` and refuse to
-complete the reuqest. To get around that simply set the
+complete the request. To get around that simply set the
`rejectUnauthorized` option to `false`:
```js
-var opts = { rejectUnauthorized: false }
+const opts = { rejectUnauthorized: false };
-var req = https.request(opts, function (res) {
+const req = https.request(opts, function (res) {
// ...
-})
+});
-req.end()
+req.end();
```
If using `curl` to connect to a Node.js HTTPS server using a
@@ -60,18 +64,70 @@ self-signed certificate, use the `-k` option:
curl -k https://localhost:443
```
+If you want to use the `fetch` API with the self-signed certificate, you can do so by using the `undici` package:
+
+```js
+const https = require('node:https');
+const { Agent } = require('undici');
+const pem = require('@metcoder95/https-pem');
+
+const agent = new Agent({
+ connect: {
+ rejectUnauthorized: false,
+ },
+});
+
+const server = https.createServer(pem, function (req, res) {
+ res.end('This is served over HTTPS with fetch');
+});
+
+server.listen(443, function () {
+ console.log('The server is running on https://localhost');
+ fetch('https://localhost', { dispatcher: agent })
+ .then((res) => res.text())
+ .then((body) => {
+ console.log('Response from server:', body);
+ })
+ .catch((err) => {
+ console.error('Error fetching from server:', err);
+ });
+});
+```
+
## API
The `https-pem` module simply exposes an object with two properties:
`key` and `cert`.
+### `pem.generate()`
+
+Generates a new self-signed certificate and key pair.
+
+```js
+const pem = require('@metcoder95/https-pem');
+pem.generate();
+```
+
+#### Parameters
+
+- `HttpPEMGeneratorOptions`: An object that can be passed to the `selfsigned.generate` method. This allows you to customize the generation of the key and certificate, such as setting the key size or adding custom attributes.
+ - `attr`: (optional) An array of attributes to include in the certificate. See the [selfsigned](https://github.com/jfromaniello/selfsigned#attributes) documentation for more information.
+ - `opts`: (optional) An object with options for the `selfsigned.generate` method. See the [selfsigned](https://github.com/jfromaniello/selfsigned#attributes) documentation for available options.
+- `done`: (optional) A callback function that will be called with the generated key and certificate. If not provided, the function will return a promise that resolves with the generated key and certificate.
+
+#### Returns
+
+- HttpsPEMGenerateResult: An object containing the generated key and certificate.
+ - `key`: The generated private key in PEM format.
+ - `cert`: The generated self-signed certificate in PEM format.
+
### `pem.key`
-The private key (RSA).
+The autogenerated private key (RSA).
### `pem.cert`
-The certificate.
+The autogenerated self-signed certificate.
## License
diff --git a/generate.js b/generate.js
new file mode 100644
index 0000000..b0b5e6b
--- /dev/null
+++ b/generate.js
@@ -0,0 +1,74 @@
+'use strict'
+
+const { X509Certificate } = require('node:crypto')
+const selfsigned = require('selfsigned')
+
+// `selfsigned` derives the certificate serial number from 9 random bytes and
+// runs them through its own `toPositiveHex()`, which clears the sign bit but
+// does not re-minimise the resulting DER INTEGER. Roughly 1 in 65536 draws end
+// up with two redundant leading zero bytes, and node-forge's encoder strips
+// only one of them (see the "should all leading bytes be stripped vs just one?"
+// TODO in its `asn1.js`), so the serial goes out as a positive INTEGER with
+// illegal padding.
+//
+// node-forge's own parser accepts that encoding, so the
+// `verifyCertificateChain()` check `selfsigned` runs before returning passes
+// and the pair looks fine. OpenSSL rejects it, so the certificate only blows
+// up later, as ERR_OSSL_ASN1_ILLEGAL_PADDING from the middle of a TLS
+// handshake. That made every consumer building a server from a freshly
+// generated pair intermittently fail (nodejs/undici#5245).
+//
+// Each serial number is drawn independently, so generating again is enough to
+// get past it: three attempts bring the odds down to about 1 in 2.8e14. All of
+// this can go away once `selfsigned` emits minimally encoded serial numbers.
+const ATTEMPTS = 3
+
+// Returns the error OpenSSL refused the certificate with, or `null` if it
+// loads. node-forge parsing it successfully says nothing about OpenSSL.
+function loadError (cert) {
+ try {
+ new X509Certificate(cert) // eslint-disable-line no-new
+ return null
+ } catch (err) {
+ return err
+ }
+}
+
+function unusable (cause) {
+ return new Error(
+ `could not generate a certificate OpenSSL can load in ${ATTEMPTS} attempts`,
+ { cause }
+ )
+}
+
+function generateSync (attrs, opts) {
+ let lastError
+
+ for (let i = 0; i < ATTEMPTS; i++) {
+ const pems = selfsigned.generate(attrs, opts)
+ const err = loadError(pems.cert)
+
+ if (err === null) return pems
+
+ lastError = err
+ }
+
+ throw unusable(lastError)
+}
+
+function generate (attrs, opts, done) {
+ let remaining = ATTEMPTS
+
+ selfsigned.generate(attrs, opts, function onPems (err, pems) {
+ if (err) return done(err)
+
+ const loadErr = loadError(pems.cert)
+
+ if (loadErr === null) return done(null, pems)
+ if (--remaining > 0) return selfsigned.generate(attrs, opts, onPems)
+
+ done(unusable(loadErr))
+ })
+}
+
+module.exports = { generate, generateSync }
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..82e726a
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,35 @@
+///
+import { SelfsignedOptions, CertificateField } from 'selfsigned';
+
+type CertificateAttribute = CertificateField;
+
+type HttpsPEMGenerateOpts = {
+ attr?: CertificateAttribute[];
+ opts?: SelfsignedOptions;
+};
+type HttpsPEMGenerateResult = {
+ key: string;
+ cert: string;
+};
+
+type HttpsPEM = {
+ key: string | null;
+ cert: string | null;
+ generate(config?: HttpsPEMGenerateOpts): Promise;
+ generate(
+ config: HttpsPEMGenerateOpts | null,
+ done: (err: Error | null, result: HttpsPEMGenerateResult) => void
+ ): void;
+};
+
+export default HttpsPEM;
+export declare const key: HttpsPEM['key'];
+export declare const cert: HttpsPEM['cert'];
+export declare const generate: HttpsPEM['generate'];
+
+export {
+ SelfsignedOptions,
+ CertificateAttribute,
+ HttpsPEMGenerateResult,
+ HttpsPEMGenerateOpts,
+};
diff --git a/index.js b/index.js
index 8bf44e7..89724b2 100644
--- a/index.js
+++ b/index.js
@@ -1,7 +1,31 @@
'use strict'
-const path = require('path')
-const fs = require('fs')
+const path = require('node:path')
+const fs = require('node:fs')
-exports.key = fs.readFileSync(path.join(__dirname, 'key.pem'))
-exports.cert = fs.readFileSync(path.join(__dirname, 'cert.pem'))
+let generator
+const keyPath = path.join(__dirname, 'key.pem')
+const certPath = path.join(__dirname, 'cert.pem')
+
+// In case npm scripts are disabled, we still want to provide the key and cert
+exports.key = fs.existsSync(keyPath) ? fs.readFileSync(keyPath) : null
+exports.cert = fs.existsSync(certPath) ? fs.readFileSync(certPath) : null
+exports.generate = generate
+exports.default = exports
+
+function generate ({ attr, opts } = { attr: [], opts: null }, done) {
+ if (done == null) {
+ const promise = new Promise((resolve, reject) => {
+ generate({ attr, opts }, (err, pems) => {
+ if (err) return reject(err)
+ resolve({ key: pems.private, cert: pems.cert })
+ })
+ })
+
+ return promise
+ }
+
+ if (!generator) generator = require('./generate')
+
+ generator.generate(attr, opts, done)
+}
diff --git a/install.js b/install.js
index b59dae8..fc26bca 100644
--- a/install.js
+++ b/install.js
@@ -1,10 +1,16 @@
'use strict'
-const fs = require('fs')
-const path = require('path')
-const selfsigned = require('selfsigned')
+const fs = require('node:fs')
+const path = require('node:path')
+const { generateSync } = require('./generate')
-const pems = selfsigned.generate()
+const nodeVersion = Number(process.versions.node.split('.')[0])
+const pems =
+ // Due to new version of openssl, we need to use a larger key size
+ // for node 24 and above, otherwise the default key size is sufficient
+ nodeVersion > 22
+ ? generateSync({}, { keySize: 2048 })
+ : generateSync()
fs.writeFileSync(path.join(__dirname, 'key.pem'), pems.private)
fs.writeFileSync(path.join(__dirname, 'cert.pem'), pems.cert)
diff --git a/package.json b/package.json
index bc8b998..b78df33 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,25 @@
{
- "name": "https-pem",
- "version": "3.0.0",
- "description": "Self-signed PEM key and certificate ready for use in your HTTPS server",
+ "name": "@metcoder95/https-pem",
+ "version": "1.0.1",
+ "description": "Self-signed PEM key and certificate ready for use in your HTTPS server (fork from https-pem)",
"main": "index.js",
+ "types": "index.d.ts",
+ "private": false,
"scripts": {
"postinstall": "node install.js",
- "test": "standard && node test.js"
+ "clean": "rm -rf *.pem",
+ "lint": "standard",
+ "lint:ci": "standard | snazzy",
+ "test": "node tests/index.js",
+ "test:ts": "tsd",
+ "release": "standard-version --no-verify"
},
"repository": {
"type": "git",
- "url": "git+https://github.com/watson/https-pem.git"
+ "url": "git+https://github.com/metcoder95/https-pem.git"
+ },
+ "engines": {
+ "node": ">=20"
},
"keywords": [
"tls",
@@ -26,16 +36,39 @@
"server"
],
"author": "Thomas Watson Steen (https://twitter.com/wa7son)",
+ "contributors": [
+ {
+ "name": "Carlos Fuentes",
+ "email": "me@metcoder.dev",
+ "url": "https://bsky.app/profile/metcoder.dev"
+ }
+ ],
"license": "MIT",
"bugs": {
- "url": "https://github.com/watson/https-pem/issues"
+ "url": "https://github.com/metcoder95/https-pem/issues"
},
- "homepage": "https://github.com/watson/https-pem#readme",
+ "homepage": "https://github.com/metcoder95/https-pem#readme",
"dependencies": {
- "selfsigned": "^2.0.1"
+ "selfsigned": "^3.0.1"
},
"devDependencies": {
- "standard": "^17.0.0"
+ "@types/node": "^24.2.0",
+ "node-forge": "^1.3.1",
+ "snazzy": "^9.0.0",
+ "standard": "^17.0.0",
+ "standard-version": "^9.5.0",
+ "tsd": "^0.33.0",
+ "typescript": "^5.9.2",
+ "undici": "^7.13.0"
+ },
+ "tsd": {
+ "directory": "test"
+ },
+ "standard": {
+ "ignore": [
+ "*.d.ts",
+ "*.test-d.ts"
+ ]
},
"coordinates": [
55.7774667,
diff --git a/test.js b/test.js
deleted file mode 100644
index 7e70655..0000000
--- a/test.js
+++ /dev/null
@@ -1,23 +0,0 @@
-'use strict'
-
-const assert = require('assert')
-const https = require('https')
-const pem = require('./')
-
-const server = https.createServer(pem, function (req, res) {
- res.end('foo')
-})
-
-server.listen(function () {
- const opts = {
- port: server.address().port,
- rejectUnauthorized: false
- }
- https.request(opts, function (res) {
- assert.strictEqual(res.statusCode, 200)
- res.on('data', function (chunk) {
- assert.strictEqual(chunk.toString(), 'foo')
- process.exit(0)
- })
- }).end()
-})
diff --git a/tests/index.js b/tests/index.js
new file mode 100644
index 0000000..4322131
--- /dev/null
+++ b/tests/index.js
@@ -0,0 +1,192 @@
+'use strict'
+
+const https = require('node:https')
+const { once } = require('node:events')
+const { test } = require('node:test')
+const { X509Certificate } = require('node:crypto')
+
+const { Agent } = require('undici')
+const selfsigned = require('selfsigned')
+const forge = require('node-forge')
+
+const client = new Agent({
+ connect: {
+ rejectUnauthorized: false
+ }
+})
+
+test('https-pem (default)', async t => {
+ const pem = require('..')
+ const server = https.createServer(pem, function (req, res) {
+ res.end('foo')
+ })
+
+ server.listen()
+ await once(server, 'listening')
+ t.after(() => server.close())
+
+ const response = await client.request({
+ origin: `https://localhost:${server.address().port}`,
+ path: '/',
+ method: 'GET'
+ })
+
+ t.plan(2)
+ t.assert.strictEqual(response.statusCode, 200)
+ t.assert.strictEqual(await response.body.text(), 'foo')
+})
+
+test('https-pem (generate)', async t => {
+ const pem = require('..')
+ const pems = await pem.generate({
+ attr: [{ name: 'commonName', value: 'localhost' }],
+ opts: { keySize: 5120 }
+ })
+
+ const server = https.createServer(pems, function (req, res) {
+ res.end('foo')
+ })
+
+ server.listen()
+ await once(server, 'listening')
+ t.after(() => server.close())
+
+ const response = await client.request({
+ origin: `https://localhost:${server.address().port}`,
+ path: '/',
+ method: 'GET'
+ })
+
+ t.plan(2)
+ t.assert.strictEqual(response.statusCode, 200)
+ t.assert.strictEqual(await response.body.text(), 'foo')
+})
+
+// `selfsigned` clears the sign bit of the 9 random bytes it draws for the
+// serial number without re-minimising the DER INTEGER, and node-forge strips
+// only one of the redundant leading zero bytes. These 9 bytes are one of the
+// roughly 1 in 65536 draws that come out as `00 00 01 ...` and leave a
+// positive INTEGER with illegal padding, which OpenSSL refuses to load.
+const ILLEGAL_PADDING_SEED = '\x80\x00\x01\x02\x03\x04\x05\x06\x07'
+
+// Turns the next `attempts` serial numbers into the pathological one above.
+// Returns a getter for how many of them were actually drawn, so a test can
+// tell whether it exercised the bad path at all.
+function forceIllegalSerialNumber (t, attempts = 1) {
+ const getBytesSync = forge.random.getBytesSync
+ let remaining = attempts
+
+ forge.random.getBytesSync = function (count) {
+ // 9 bytes are only ever drawn for the serial number
+ if (count === 9 && remaining > 0) {
+ remaining--
+ return ILLEGAL_PADDING_SEED
+ }
+
+ return getBytesSync.call(this, count)
+ }
+ t.after(() => { forge.random.getBytesSync = getBytesSync })
+
+ return () => attempts - remaining
+}
+
+test('selfsigned on its own produces a certificate OpenSSL rejects', async t => {
+ const drawn = forceIllegalSerialNumber(t)
+
+ const { cert } = await new Promise((resolve, reject) => {
+ selfsigned.generate(undefined, { keySize: 1024 }, (err, pems) => {
+ if (err) return reject(err)
+ resolve(pems)
+ })
+ })
+ t.assert.strictEqual(drawn(), 1, 'the serial number seed was not drawn')
+
+ let err
+ try {
+ new X509Certificate(cert) // eslint-disable-line no-new
+ } catch (e) {
+ err = e
+ }
+
+ if (err === undefined) {
+ // Nothing left to work around: `generate.js` can go away and `index.js`
+ // can call `selfsigned.generate()` directly again.
+ t.diagnostic('selfsigned no longer emits non-minimal serial numbers')
+ return
+ }
+
+ t.assert.strictEqual(err.code, 'ERR_OSSL_ASN1_ILLEGAL_PADDING')
+})
+
+test('https-pem (generate) draws a new serial number when OpenSSL rejects one', async t => {
+ const pem = require('..')
+ const drawn = forceIllegalSerialNumber(t)
+
+ const { key, cert } = await pem.generate({ opts: { keySize: 1024 } })
+ t.assert.strictEqual(drawn(), 1, 'the serial number seed was not drawn')
+
+ t.assert.ok(key)
+ // A minimally encoded positive INTEGER never starts with a zero byte
+ const parsed = new X509Certificate(cert)
+ t.assert.doesNotMatch(parsed.serialNumber, /^00/)
+})
+
+test('https-pem (generate) survives consecutive rejected serial numbers', async t => {
+ const pem = require('..')
+ const drawn = forceIllegalSerialNumber(t, 2)
+
+ const { cert } = await pem.generate({ opts: { keySize: 1024 } })
+ t.assert.strictEqual(drawn(), 2, 'not every serial number seed was drawn')
+
+ new X509Certificate(cert) // eslint-disable-line no-new
+})
+
+test('https-pem (generate) gives up instead of returning an unusable pair', async t => {
+ const pem = require('..')
+ forceIllegalSerialNumber(t, Infinity)
+
+ await t.assert.rejects(
+ pem.generate({ opts: { keySize: 1024 } }),
+ err => {
+ t.assert.match(err.message, /could not generate a certificate OpenSSL can load/)
+ t.assert.strictEqual(err.cause.code, 'ERR_OSSL_ASN1_ILLEGAL_PADDING')
+ return true
+ }
+ )
+})
+
+test('https-pem (default) ships a certificate OpenSSL can load', t => {
+ const pem = require('..')
+
+ t.assert.ok(pem.key)
+ const cert = new X509Certificate(pem.cert)
+ t.assert.doesNotMatch(cert.serialNumber, /^00/)
+})
+
+test('https-pem (generate) serves over TLS past a rejected serial number', async t => {
+ const pem = require('..')
+ forceIllegalSerialNumber(t)
+
+ const pems = await pem.generate({
+ attr: [{ name: 'commonName', value: 'localhost' }],
+ opts: { keySize: 2048 }
+ })
+
+ const server = https.createServer(pems, function (req, res) {
+ res.end('foo')
+ })
+
+ server.listen()
+ await once(server, 'listening')
+ t.after(() => server.close())
+
+ const response = await client.request({
+ origin: `https://localhost:${server.address().port}`,
+ path: '/',
+ method: 'GET'
+ })
+
+ t.plan(2)
+ t.assert.strictEqual(response.statusCode, 200)
+ t.assert.strictEqual(await response.body.text(), 'foo')
+})
diff --git a/tests/index.test-d.ts b/tests/index.test-d.ts
new file mode 100644
index 0000000..4f4b65a
--- /dev/null
+++ b/tests/index.test-d.ts
@@ -0,0 +1,19 @@
+import { expectAssignable } from 'tsd';
+
+import {
+ generate,
+ key,
+ cert,
+ HttpsPEMGenerateResult,
+ HttpsPEMGenerateOpts,
+} from '..';
+
+expectAssignable(key);
+expectAssignable(cert);
+expectAssignable>(generate({}));
+expectAssignable(generate({}, () => {}));
+expectAssignable(generate(null, () => {}));
+expectAssignable({
+ attr: [{ name: 'hello', value: 'world' }],
+ opts: { keySize: 1234 },
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..61a9e4e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,101 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Enable incremental compilation */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+
+ /* Language and Environment */
+ "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
+ // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+
+ /* Modules */
+ "module": "commonjs" /* Specify what module code is generated. */,
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "resolveJsonModule": true, /* Enable importing .json files */
+ // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
+
+ /* Emit */
+ "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
+ "declarationMap": true /* Create sourcemaps for d.ts files. */,
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
+ "outDir": "./" /* Specify an output folder for all emitted files. */,
+ // "removeComments": true, /* Disable emitting comments. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ "stripInternal": true /* Disable emitting declarations that have `@internal` in their JSDoc comments. */,
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
+ "declarationDir": "./types" /* Specify the output directory for generated declaration files. */,
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+ /* Interop Constraints */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+
+ /* Type Checking */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
+ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
+ // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
+ "alwaysStrict": true /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ // "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}