ACME.js (RFC 8555 / November 2019)
| Built by Root for Greenlock
Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers
Lightweight. Fast. Modern Crypto. Zero external dependecies.
Features
| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |
The primary goal of this library is to make it easy to
get Accounts and Certificates through Let's Encrypt.
* 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.
API Overview
ACME.create({ maintainerEmail, packageAgent, notify });
acme.init(directoryUrl);
acme.accounts.create({ subscriberEmail, agreeToTerms, accountKey });
acme.certificates.create({
customerEmail,
account,
accountKey,
csr,
domains,
challenges
});
Parameter | Description |
---|
account | an object containing the Let's Encrypt Account ID as "kid" (misnomer, not actually a key id/thumbprint) |
accountKey | an RSA or EC public/private keypair in JWK format |
agreeToTerms | set to true to agree to the Let's Encrypt Subscriber Agreement |
challenges | the 'http-01', 'alpn-01', and/or 'dns-01' challenge plugins (get , set , and remove callbacks) to use |
csr | a Certificate Signing Request (CSR), which may be generated with csr.js, openssl, or another |
customerEmail | Don't use this. Given as an example to differentiate between Maintainer, Subscriber, and End-User |
directoryUrl | should be the Let's Encrypt Directory URL
https://acme-staging-v02.api.letsencrypt.org/directory |
domains | the list of altnames (subject first) that are listed in the CSR and will be listed on the certificate |
notify | all callback for logging events and errors in the form function (ev, args) { ... } |
maintainerEmail | should be a contact for the author of the code to receive critical bug and security notices |
packageAgent | should be an RFC72321-style user-agent string to append to the ACME client (ex: mypackage/v1.1.1) |
subscriberEmail | should be a contact for the service provider to receive renewal failure notices and manage the ACME account |
Helper Functions
ACME.computeChallenge({
accountKey: jwk,
hostname: 'example.com',
challenge: { type: 'dns-01', token: 'xxxx' }
});
Install
To make it easy to generate, encode, and decode keys and certificates,
ACME.js uses Keypairs.js
and CSR.js
Node.js
npm install --save @root/acme
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"
)
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.all.js"></script>
acme.min.js
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.all.min.js"></script>
Use
var ACME = window['@root/acme'];
Usage 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.
Overview
- Create an instance of ACME.js
- Create and SAVE a Subscriber Account private key
- Retrieve the Let's Encrypt Subscriber account (with the key)
- the account will be created if it doesn't exist
- Create a Server Key
- this should be per-server, or perhaps per-end-user
- Create a Certificate Signing Request
- International Domain Names must be converted with
punycode
- Create an ACME Order
- use a challenge plugin for HTTP-01 or DNS-01 challenges
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() {
$('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 accountPrivateJwk;
var account;
Keypairs.generate({ kty: 'EC' }).then(function(pair) {
accountPrivateJwk = 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);
}
},
accountKey: 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'
});
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');
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({
account: account,
accountKey: accountPrivateJwk,
csr: csr,
domains: sortedDomains,
challenges: challenges,
customerEmail: null,
skipChallengeTests: false,
skipDryRun: false
});
console.log('Got SSL Certificate:');
console.log(results.expires);
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();
}
}
};
Many challenge plugins are already available for popular platforms.
Search acme-http-01-
or acme-dns-01-
on npm to find more.
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:
git clone https://git.rootprojects.org/root/acme.js
pushd acme.js/
npm install --save-dev acme-dns-01-digitalocean
rsync -av examples/example.env .env
code .env
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