Lightweight library for getting Free SSL certifications through Let's Encrypt v2, using ACME (RFC 8555)
Go to file
AJ ONeal f7fd435443 v3.0.0-wip.3: correct deps 2019-10-15 19:00:58 -06:00
asn1 relocate packages for easier export 2019-10-08 13:40:01 -06:00
bin make Prettier 2019-10-02 15:04:54 -06:00
dist updates 2019-10-08 14:50:53 -06:00
examples updates 2019-10-08 14:50:53 -06:00
fixtures make Prettier 2019-10-08 13:02:43 -06:00
lib moved common code to own modules 2019-10-15 05:01:52 -06:00
tests moved common code to own modules 2019-10-15 05:01:52 -06:00
.gitignore updates 2019-10-08 14:50:53 -06:00
.prettierrc make Prettier 2019-10-02 15:04:54 -06:00
LICENSE ready-ish to release Bluecrypt ACME 2019-05-07 01:52:33 -06:00
README.md update docs 2019-10-08 15:13:13 -06:00
acme.js moved common code to own modules 2019-10-15 05:01:52 -06:00
package-lock.json v3.0.0-wip.3: correct deps 2019-10-15 19:00:58 -06:00
package.json v3.0.0-wip.3: correct deps 2019-10-15 19:00:58 -06:00
pem.js relocate packages for easier export 2019-10-08 13:40:01 -06:00
webpack.config.js updates 2019-10-08 14:50:53 -06:00
x509.js relocate packages for easier export 2019-10-08 13:40:01 -06:00

README.md

ACME.js

Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers

Lightweight. Fast. Modern Crypto. Zero dependecies.

Features

| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |

  • Let's Encrypt v2.1+ (November 2019)
    • ACME draft 15 (supports POST-as-GET)
    • Secure support for EC and RSA for account and server keys
    • Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations
  • Supports International Domain Names (i.e. .中国)
  • VanillaJS, Zero External Dependencies
    • Node.js* (v6+)
    • WebPack

* Although we use async/await in the examples, the code is written in CommonJS, with Promises, so you can use it in Node.js and Browsers without transpiling.

Want Quick and Easy?

ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers.

If you're looking for maximum convenience, try Greenlock.js.

Online Demos

We expect that our hosted versions will meet all of yours needs. If they don't, please open an issue to let us know why.

We'd much rather improve the app than have a hundred different versions running in the wild. However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.

QuickStart

To make it easy to generate, encode, and decode keys and certificates, ACME.js embeds Keypairs.js and CSR.js

Node.js

var ACME = require('@root/acme');

WebPack

<meta charset="UTF-8" />

(necessary in case the webserver headers don't specify plain/text; charset="UTF-8")

var ACME = require('@root/acme');

Vanilla JS

<meta charset="UTF-8" />

(necessary in case the webserver headers don't specify plain/text; charset="UTF-8")

acme.js

<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.js"></script>

acme.min.js

<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.min.js"></script>

Use

var ACME = window['@root/acme'];

Examples

You can see tests/index.js, examples/index.html, examples/app.js in the repo for full example usage.

Emails: Maintainer vs Subscriber vs Customer

  • maintainerEmail should be the email address of the author of the code. This person will receive critical security and API change notifications.
  • subscriberEmail should be the email of the admin of the hosting service. This person agrees to the Let's Encrypt Terms of Service and will be notified when a certificate fails to renew.
  • customerEmail should be the email of individual who owns the domain. This is optional (not currently implemented).

Generally speaking YOU are the maintainer and you or your employer is the subscriber.

If you (or your employer) is running any type of service you SHOULD NOT pass the customer email as the subscriber email.

If you are not running a service (you may be building a CLI, for example), then you should prompt the user for their email address, and they are the subscriber.

Instantiate ACME.js

Although built for Let's Encrypt, ACME.js will work with any server that supports draft-15 of the ACME spec (includes POST-as-GET support).

The init() method takes a directory url and initializes internal state according to its response.

var acme = ACME.create({
	maintainerEmail: 'jon@example.com'
});
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(
	function() {
		// Ready to use, show page
		$('body').hidden = false;
	}
);

Create ACME Account with Let's Encrypt

ACME Accounts are key and device based, with an email address as a backup identifier.

A public account key must be registered before an SSL certificate can be requested.

var accountPrivateKey;
var account;

Keypairs.generate({ kty: 'EC' }).then(function(pair) {
	accountPrivateKey = pair.private;

	return acme.accounts
		.create({
			agreeToTerms: function(tos) {
				if (
					window.confirm(
						"Do you agree to the ACME.js and Let's Encrypt Terms of Service?"
					)
				) {
					return Promise.resolve(tos);
				}
			},
			accountKeypair: { privateKeyJwk: pair.private },
			subscriberEmail: $('.js-email-input').value
		})
		.then(function(_account) {
			account = _account;
		});
});

Generate a Certificate Private Key

var certKeypair = await Keypairs.generate({ kty: 'RSA' });
var pem = await Keypairs.export({
	jwk: certKeypair.private,
	encoding: 'pem'
});

// This should be saved as `privkey.pem`
console.log(pem);

Generate a CSR

The easiest way to generate a Certificate Signing Request will be either with openssl or with @root/CSR.

var CSR = require('@root/csr');
var Enc = require('@root/encoding');

// 'subject' should be first in list
var sortedDomains = ['example.com', 'www.example.com'];
var csr = await CSR.csr({
	jwk: certKeypair.private,
	domains: sortedDomains,
	encoding: 'der'
}).then(function(der) {
	return Enc.bufToUrlBase64(der);
});

Get Free 90-day SSL Certificate

Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key, the names of domains to be secured, and a distinctly separate server private key.

A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record) is a required part of the process, which requires set and remove callbacks/promises.

var certinfo = await acme.certificates.create({
	agreeToTerms: function(tos) {
		return tos;
	},
	account: account,
	accountKeypair: { privateKeyJwk: accountPrivateKey },
	csr: csr,
	domains: sortedDomains,
	challenges: challenges, // must be implemented
	customerEmail: null,
	skipChallengeTests: false,
	skipDryRun: false
});

console.log('Got SSL Certificate:');
console.log(results.expires);

// This should be saved as `fullchain.pem`
console.log([results.cert, results.chain].join('\n'));

Example "Challenge" Implementation

Typically here you're just presenting some sort of dialog to the user to ask them to upload a file or set a DNS record.

It may be possible to do something fancy like using OAuth2 to login to Google Domanis to set a DNS address, etc, but it seems like that sort of fanciness is probably best reserved for server-side plugins.

var challenges = {
	'http-01': {
		set: function(opts) {
			console.info('http-01 set challenge:');
			console.info(opts.challengeUrl);
			console.info(opts.keyAuthorization);
			while (
				!window.confirm('Upload the challenge file before continuing.')
			) {}
			return Promise.resolve();
		},
		remove: function(opts) {
			console.log('http-01 remove challenge:', opts.challengeUrl);
			return Promise.resolve();
		}
	}
};

IDN - International Domain Names

Convert domain names to punycode before creating the certificate:

var punycode = require('punycode');

acme.certificates.create({
	// ...
	domains: ['example.com', 'www.example.com'].map(function(name) {
		return punycode.toASCII(name);
	})
});

The punycode library itself is lightweight and dependency-free. It is available both in node and for browsers.

Testing

You will need to use one of the acme-dns-01-* plugins to run the test locally.

You'll also need a .env that looks something like the one in examples/example.env:

ENV=DEV
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
BASE_DOMAIN=test.example.com
CHALLENGE_TYPE=dns-01
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'

For example:

# Get the repo and change directories into it
git clone https://git.rootprojects.org/root/bluecrypt-acme.js
pushd bluecrypt-acme.js/

# Install the challenge plugin you'll use for the tests
npm install --save-dev acme-dns-01-digitalocean

# Copy the sample .env file
rsync -av examples/example.env .env

# Edit the config file to use a domain in your account, and your API token
#vim .env
code .env

# Run the tests
node tests/index.js

Developing

You can see <script> tags in the index.html in the repo, which references the original source files.

Join @rootprojects #general on Keybase if you'd like to chat with us.

Commercial Support

We have both commercial support and commercial licensing available.

You're welcome to contact us in regards to IoT, On-Prem, Enterprise, and Internal installations, integrations, and deployments.

We also offer consulting for all-things-ACME and Let's Encrypt.

Legal & Rules of the Road

Greenlock™ is a trademark of AJ ONeal

The rule of thumb is "attribute, but don't confuse". For example:

Built with ACME.js (a Root project).

Please contact us if have any questions in regards to our trademark, attribution, and/or visible source policies. We want to build great software and a great community.

ACME.js | MPL-2.0 | Terms of Use | Privacy Policy