pem: generating self-signed keys and certificates from Node.js
Create private keys and certificates with node.js
At a glance
- What is it?
- pem is a Node.js library that drives the openssl binary to create private keys, certificate signing requests and certificates in JavaScript, so a development server can serve HTTPS without a certificate authority. It suits a developer who needs throwaway TLS material for local testing, and it is the wrong tool for production certificates.
- Who is it for?
- pem fits a developer who needs an HTTPS server running locally or in a test environment without arranging a certificate authority, since createCertificate with selfSigned returns a usable key and certificate in one call and the library handles both callbacks and promises.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A wrapper around the openssl binary
pem describes itself in one line: create private keys and certificates with node.js. The mechanism underneath is worth being explicit about, because it determines both what the library is good for and where it will fail.
pem drives the openssl command line tool. It writes a configuration, invokes openssl with the right arguments, and parses the output back into JavaScript objects. It is not a crypto library, and it does not implement key generation or signing itself. Its one runtime dependency is which, the small package that locates an executable on the system path.
That design has an obvious consequence, and the readme states it as a warning rather than burying it: openssl or libressl must already be installed on the system or container, and without them pem will not work. For a library that people reach for inside Docker images, that is the single most common failure, and it is worth checking before anything else.
The other consequence is that pem inherits openssl's behaviour exactly, including its version differences. The package manifest shows a dedicated script for testing against one specific version, test:opensslv3.0.1, which runs the build and then the test suite with the output redirected to a log file. A project whose CI matrix spans several openssl versions is telling you that the wrapper's behaviour is version-sensitive.
The API is documented in jsdoc, published at dexus.github.io/pem/jsdoc, and the readme notes that the API section in the readme itself is partly inside a commented-out block, with the words we had to clean up a bit above it. The jsdoc site is the reference.
One call for a self-signed certificate and a key
The smallest useful example creates a self-signed certificate valid for a day and serves HTTPS with it:
var https = require('https')
var pem = require('pem')
pem.createCertificate({ days: 1, selfSigned: true }, function (err, keys) {
if (err) {
throw err
}
https.createServer({ key: keys.clientKey, cert: keys.certificate }, function (req, res) {
res.end('o hai!')
}).listen(443)
})Two details in that snippet carry the whole design. The days option controls the expiry, and the selfSigned option tells the library to use the client key to sign the certificate when no service key is supplied. The callback receives a keys object with the clientKey and the certificate, which is exactly what https.createServer wants, so there is no file to write and no path to keep track of.
The readme is upfront about the port. 443 is the standard HTTPS port but requires root permissions on most systems, and it suggests a higher port such as 4300 and then visiting https://localhost:4300.
The Express variant is the same shape with an application instance in the middle, and there is also an async and await version, where createCertificate is awaited and the server is created from the resolved keys:
const https = require('https')
const pem = require('pem')
async function main() {
const keys = await pem.createCertificate({ days: 1, selfSigned: true })
const server = https.createServer({ key: keys.clientKey, cert: keys.certificate }, (req, res) => {
res.end('o hai!')
})
server.listen(443)
}That last example is the current recommended style. The readme explains that every asynchronous function in pem still accepts a Node-style callback and now also returns a Promise when no callback is given, and that the legacy pem.promisified helper is deprecated in favour of the primary exports for both flows.
The option surface for CSRs and certificates
The API section of the readme lists the options, and they are worth knowing because they determine what you can actually produce.
For createCSR, the options include an existing clientKey to use, a clientKeyPassword for it, a keyBitsize used when a new key is generated, a hash function that can be md5, sha1 or sha256 and defaults to sha256, and the subject fields country, state, locality, organization, organizationUnit and commonName, with commonName defaulting to localhost. There is an altNames list for subjectAltName entries, an emailAddress field, and a csrConfigFile option.
createCertificate takes all of the createCSR options, and adds the ones that matter for signing: a serviceKey which is the private key used to sign, a serviceKeyPassword, an optional serviceCertificate for that key, a serial which is required when a serviceCertificate is defined, selfSigned which uses clientKey for signing when no serviceKey is given, a csr to sign, days for the expiry, an extFile for an extension config file written without the -extensions v3_req flag, and a config for an extension config file written with it.
That last pair is the detail that bites people. Whether you get a usable extension block depends on which file you point at and whether it already carries the v3_req marker. The readme is precise about the difference and the practical consequence is that you cannot just reuse a config file between the two calls without checking.
Elsewhere there is createPrivateKey with an optional keyBitsize defaulting to 2048 and an optional cipher and password pair for encrypting the key, with a list of supported ciphers, and getPublicKey, which takes a PEM-encoded private key, CSR or certificate and returns the public key. The readme also mentions createDhparam and createEcparam, with keyBitsize defaulting to 512 and a curve name defaulting to secp256k1 respectively, though those appear in the commented-out part of the API section and the current jsdoc site is the place to confirm their status.
TypeScript definitions and the modernised public API
The readme has a short section on TypeScript, and it describes a fairly substantial tidy-up rather than a token gesture.
TypeScript declaration files are bundled with the package via types/pem.d.ts, and the readme states that the public API, including the legacy promisified helpers and the CA utility, now has precise typings. The types directory is one of the three directories listed in the package manifest's files array, alongside dist and lib, so the definitions ship with the package rather than being resolved from a separate @types entry.
The typings are guarded by a test. The readme says to run yarn test:types to execute lightweight tsd assertions that guard the definitions, and the package manifest confirms this with a test:types script pointing at tsd.
There is also a CA utility mentioned in the readme, alongside the legacy promisified helpers, both of which are part of the now-typed surface. The deprecation of pem.promisified in favour of calling the primary exports directly is a small but real API change, and it is the kind of thing that breaks a codebase on upgrade: code written against the old helper still runs, but the readme is telling you to move to the direct exports, which work with both callbacks and promises.
The repository layout backs up the shape of the project: lib/ holds the implementation as plain CommonJS modules, dist/ holds a bundled build produced by ncc, bin/ holds the command line entry, types/ holds the declarations, and there is a jsdoc.json at the root driving the documentation build. The package is CommonJS, with main pointing at ./dist/index.js, and the build script is ncc build with a minified output to dist.
Running the tests and the linting setup
The test setup is described in the package manifest rather than the readme, and it has a few details worth knowing if you plan to contribute or to run the suite in CI.
The coverage script runs nyc over the mocha specs, with the html and text reporters, and it searches for files matching *.spec.js while excluding the spec files themselves and the test helper from the coverage report. The lint script runs eslint with the project's own .eslintrc.js over lib, the markdown files, the JSON files and the test files, with --fix, so linting can rewrite your working tree. The default test script runs lint and then coverage in sequence, which means a failing lint stops the suite before any test runs.
There is a version-specific script as well, test:opensslv3.0.1, which runs the build and then the test suite with the output appended to a log file. A project that keeps a script per openssl version is telling you the wrapper's behaviour varies with the binary it drives.
The contributing surface in the repository listing is conventional: CONTRIBUTING.md, CODE_OF_CONDUCT.md, a .editorconfig, .eslintrc.js, .eslintignore, renovate.json for dependency updates, HISTORY.md, SECURITY.md, and a yarn.lock with a .yarnrc.yml, which indicates the project moved to Yarn. The changelog is generated by the auto-changelog tool into HISTORY.md.
The repository also carries a .old.travis.yml, which is a leftover from the Travis CI era, and the current .github/ directory holds the present workflows. The badge in the readme still points at Travis, so the status badge is stale even though the project has moved on.
Where pem is the wrong tool
The first limitation is that it depends on an external binary being present. The readme warns about it directly, and it is the reason pem will not work in a slim container image without installing openssl or libressl first. A team that wants its test suite to run in a minimal image now has to either install openssl in the image or use a different approach, and for many projects that is the whole decision.
The second is that the certificates it produces are self-signed and therefore trusted by nobody. That is fine for a local development server that you visit once and click through the browser warning, and it is exactly wrong for a production deployment, where the certificate has to chain to a certificate authority that the browser already trusts. pem can create a CSR, which is the piece you submit to a public authority, but it does not replace the authority, the domain validation or the renewal automation.
The third is that it is a shell wrapper rather than a crypto implementation. That means the security properties of what it produces are openssl's properties, and the behaviours it inherits include openssl's defaults, which have changed across versions, notably around the default hash and the handling of weak algorithms. The presence of a per-version test script in the manifest suggests the authors knew this was version-sensitive.
The fourth is the release and licence situation. The published releases stop at v1.14.4 from 2020-01-25, while the version in the package manifest is 1.14.8, so there is a gap between what npm users last got as a tagged release and what the manifest says the code is at. The last push was on 2026-10-16, so work is continuing on master. And the licence field in the package manifest says MIT while the licence field on the repository is one GitHub cannot classify, with a LICENSE file in the repository root. For a package that generates private keys, the licence terms of the package itself are not the same as the licence of the material it produces, but the discrepancy is worth resolving before it lands in a dependency tree.
The fifth is that the alternative is not a library at all. pem is a convenience wrapper, and a team that only needs one self-signed certificate in a test fixture can call the openssl binary directly from a script, or shell out with child_process, and get the same artefact with nothing added to the dependency tree. The difference pem buys is a JavaScript API with typed options rather than command line arguments, which is worth having when you are generating keys and CSRs with several options and reading the results back as objects, and worth less when you are generating one throwaway certificate.
Editorial conclusion
pem fits a developer who needs an HTTPS server running locally or in a test environment without arranging a certificate authority, since createCertificate with selfSigned returns a usable key and certificate in one call and the library handles both callbacks and promises. It is the wrong choice for production certificates, since a self-signed certificate is trusted by nobody by default, and it is the wrong choice in a container image where openssl is not present, which the readme flags as a hard requirement. Before adopting it, confirm openssl is on the path in every environment that will run your tests, read the LICENSE file since the metadata records a custom licence while the package manifest says MIT, and note that the last push was on 2026-10-16 with the newest published release still v1.14.4 from 2020-01-25.
Frequently asked questions
How do I install pem?
Run npm install pem, or yarn add pem. The readme warns that openssl or libressl must already be installed on your system or container, because pem will not work without them.
How do I create a self-signed certificate with pem?
Call pem.createCertificate with days and selfSigned set to true, and pass the resulting keys.clientKey and keys.certificate to https.createServer. The function accepts a Node-style callback or returns a Promise when no callback is given, and the legacy pem.promisified helper is deprecated.
Does pem support TypeScript?
Yes. Declaration files are bundled with the package via types/pem.d.ts, and the public API including the legacy promisified helpers and the CA utility has precise typings. The definitions are guarded by tsd assertions run with yarn test:types.
Can pem create a certificate signing request?
Yes. createCSR takes options including an existing clientKey, a keyBitsize that defaults to 2048, a hash function defaulting to sha256, the subject fields country, state, locality, organization, organizationUnit and commonName, plus an altNames list for subjectAltName entries, and returns the csr and clientKey.
What licence is pem released under?
The package manifest declares MIT, and there is a LICENSE file in the repository root, but the licence field on the repository is one GitHub cannot classify. The last push was on 2026-10-16 and the newest published release is v1.14.4 from 2020-01-25, while the manifest version is 1.14.8.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dexus-pem)Was this useful?