diff --git a/.gitignore b/.gitignore index 8072530..d5ac8b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .env +*.gz +.*.sw* +.ignore *.pem @@ -14,4 +17,5 @@ coverage # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git + node_modules diff --git a/.prettierrc b/.prettierrc index 420e082..7e5d770 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,7 +2,7 @@ "bracketSpacing": true, "printWidth": 80, "singleQuote": true, - "tabWidth": 2, + "tabWidth": 4, "trailingComma": "none", "useTabs": true } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..56bb2dc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +- v3 (Oct 2019) + - Add POST-as-GET for Let's Encrypt v2 release 2 (ACME / RFC 8555) + - Jump to v3 for parity with Greenlock + - Merge browser and node.js versions in one + - Drop all backwards-compat complexity + - Move to zero-external deps, using @root packages only +- v1.8 + - more transitional prepwork for new v2 API + - support newer (simpler) dns-01 and http-01 libraries +- v1.5 + - perform full test challenge first (even before nonce) +- v1.3 + - Use node RSA keygen by default + - No non-optional external deps! +- v1.2 + - fix some API out-of-specness + - doc some magic numbers (status) + - updated deps +- v1.1.0 + - reduce dependencies (use lightweight @coolaj86/request instead of request) +- v1.0.5 - cleanup logging +- v1.0.4 - v6- compat use `promisify` from node's util or bluebird +- v1.0.3 - documentation cleanup +- v1.0.2 + - use `options.contact` to provide raw contact array + - made `options.email` optional + - file cleanup +- v1.0.1 + - Compat API is ready for use + - Eliminate debug logging +- Apr 10, 2018 - tested backwards-compatibility using greenlock.js +- Apr 5, 2018 - export http and dns challenge tests +- Apr 5, 2018 - test http and dns challenges (success and failure) +- Apr 5, 2018 - test subdomains and its wildcard +- Apr 5, 2018 - test two subdomains +- Apr 5, 2018 - test wildcard +- Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) +- Mar 21, 2018 - _mostly_ matches le-acme-core.js API +- Mar 21, 2018 - can now accept values (not hard coded) +- Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) +- Mar 20, 2018 - download certificate +- Mar 20, 2018 - poll for status +- Mar 20, 2018 - finalize order (submit csr) +- Mar 20, 2018 - generate domain keypair +- Mar 20, 2018 - respond to challenges +- Mar 16, 2018 - get challenges +- Mar 16, 2018 - new order +- Mar 15, 2018 - create account +- Mar 15, 2018 - generate account keypair +- Mar 15, 2018 - get nonce +- Mar 15, 2018 - get directory diff --git a/LICENSE b/LICENSE index 3435503..2f965da 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2018 AJ ONeal +Copyright 2015-2019 AJ ONeal Mozilla Public License Version 2.0 ================================== diff --git a/README.md b/README.md index f450bbd..46d39cd 100644 --- a/README.md +++ b/README.md @@ -1,239 +1,355 @@ -# ACME.js v3 on its way (Nov 1st, 2019) +# [ACME.js](https://git.rootprojects.org/root/acme.js) v3 -ACME.js v3 is in private beta and will be available by Nov 1st. +| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains) -Follow the updates on the [campaign page](https://indiegogo.com/at/greenlock), -and contribute to support the project and get beta access now. +Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers -| **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2)) -| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) -| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) -| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) +Lightweight. Fast. Modern Crypto. Zero external dependecies. -# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project +# Features -A **Zero (External) Dependency**\* library for building -Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates. +| 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. -# Features +- [x] Let's Encrypt v2 / ACME RFC 8555 (November 2019) + - [x] POST-as-GET support + - [x] Secure support for EC and RSA for account and server keys + - [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations + - [ ] (in-progress) StartTLS Everywhere™ +- [x] Supports International Domain Names (i.e. `.中国`) +- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js) + - [x] **http-01** for single or multiple domains per certificate + - [x] **dns-01** for wildcards, localhost, private networks, etc +- [x] VanillaJS, Zero External Dependencies + - [x] Safe, Efficient, Maintained + - [x] Node.js\* (v6+) + - [x] WebPack +- [x] Online Demo + - See https://greenlock.domains -- [x] Let's Encrypt™ v2 / ACME Draft 12 - - [ ] (in-progress) Let's Encrypt™ v2.1 / ACME Draft 18 - - [ ] (in-progress) StartTLS Everywhere™ -- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js) - - [x] **http-01** for single or multiple domains per certificate - - [x] **dns-01** for wildcards, localhost, private networks, etc -- [x] VanillaJS - - [x] Zero External Dependencies - - [x] Safe, Efficient, Maintained - - [x] Works in Node v6+ - - [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains)) +\* 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. -\* The only required dependencies were built by us, specifically for this and related libraries. -There are some, truly optional, backwards-compatibility dependencies for node v6. +# Want Quick and Easy? -## Looking for Quick 'n' Easy™? +ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers. -If you want something that's more "batteries included" give -[greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) -a try. +If you're looking for maximum convenience, try +[Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js). -- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +- -## v1.7+: Transitional v2 Support +# Online Demos -By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18). +- Greenlock for the Web +- ACME.js Demo -Although the draft 18 changes themselves don't requiring breaking the API, -we've been keeping backwards compatibility for a long time and the API has become messy. +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're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify** -the code with a fresh new release. +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. -As of **v1.7** we started adding **transitional support** for the **next major version**, v2.0 of acme-v2.js. -We've been really good about backwards compatibility for +# QuickStart -## Recommended Example +To make it easy to generate, encode, and decode keys and certificates, +ACME.js uses [Keypairs.js](https://git.rootprojects.org/root/keypairs.js) +and [CSR.js](https://git.rootprojects.org/root/csr.js) -Due to the upcoming changes we've removed the old documentation. - -Instead we recommend that you take a look at the -[Digital Ocean DNS-01 Example](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js) - -- [examples/dns-01-digitalocean.js](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js) - -That's not exactly the new API, but it's close. - -## Let's Encrypt v02 Directory URLs - -``` -# Production URL -https://acme-v02.api.letsencrypt.org/directory -``` - -``` -# Staging URL -https://acme-staging-v02.api.letsencrypt.org/directory -``` - - - -## API - -Status: Small, but breaking changes coming in v2 - -This API is a simple evolution of le-acme-core, -but tries to provide a better mapping to the new draft 11 APIs. +## Node.js ```js -var ACME = require('acme-v2').ACME.create({ - // used for overriding the default user-agent - userAgent: 'My custom UA String', - getUserAgentString: function(deps) { - return 'My custom UA String'; - }, +var ACME = require('@root/acme'); +``` - // don't try to validate challenges locally - skipChallengeTest: false, - skipDryRun: false, +## WebPack - // ask if the certificate can be issued up to 10 times before failing - retryPoll: 8, - // ask if the certificate has been validated up to 6 times before cancelling - retryPending: 4, - // Wait 1000ms between retries - retryInterval: 1000, - // Wait 10,000ms after deauthorizing a challenge before retrying - deauthWait: 10 * 1000 +```html + +``` + +(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`) + +```js +var ACME = require('@root/acme'); +``` + +## Vanilla JS + +```html + +``` + +(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`) + +`acme.js` + +```html + +``` + +`acme.min.js` + +```html + +``` + +Use + +```js +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. + +```js +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. + +```js +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 + +```js +var certKeypair = await Keypairs.generate({ kty: 'RSA' }); +var pem = await Keypairs.export({ + jwk: certKeypair.private, + encoding: 'pem' }); -// Discover Directory URLs -ACME.init(acmeDirectoryUrl); // returns Promise +// This should be saved as `privkey.pem` +console.log(pem); +``` -// Accounts -ACME.accounts.create(options); // returns Promise registration data +### Generate a CSR -options = { - email: '', // valid email (server checks MX records) - accountKeypair: { - // privateKeyPem or privateKeyJwt - privateKeyPem: '' +The easiest way to generate a Certificate Signing Request will be either with `openssl` or with `@root/CSR`. + +```js +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. + +```js +var certinfo = await acme.certificates.create({ + agreeToTerms: function(tos) { + return tos; }, - agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back -}; + account: account, + accountKeypair: { privateKeyJwk: accountPrivateKey }, + csr: csr, + domains: sortedDomains, + challenges: challenges, // must be implemented + customerEmail: null, + skipChallengeTests: false, + skipDryRun: false +}); -// Registration -ACME.certificates.create(options); // returns Promise +console.log('Got SSL Certificate:'); +console.log(results.expires); -options = { - domainKeypair: { - privateKeyPem: '' - }, - accountKeypair: { - privateKeyPem: '' - }, - domains: ['example.com'], +// This should be saved as `fullchain.pem` +console.log([results.cert, results.chain].join('\n')); +``` - getZones: function(opts) {}, // should Promise an array of domain zone names - setChallenge: function(opts) {}, // should Promise the record id, or name - removeChallenge: function(opts) {} // should Promise null +### 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. + +```js +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(); + } + } }; ``` -# Changelog +# IDN - International Domain Names -- v1.8 - - more transitional prepwork for new v2 API - - support newer (simpler) dns-01 and http-01 libraries -- v1.5 - - perform full test challenge first (even before nonce) -- v1.3 - - Use node RSA keygen by default - - No non-optional external deps! -- v1.2 - - fix some API out-of-specness - - doc some magic numbers (status) - - updated deps -- v1.1.0 - - reduce dependencies (use lightweight @coolaj86/request instead of request) -- v1.0.5 - cleanup logging -- v1.0.4 - v6- compat use `promisify` from node's util or bluebird -- v1.0.3 - documentation cleanup -- v1.0.2 - - use `options.contact` to provide raw contact array - - made `options.email` optional - - file cleanup -- v1.0.1 - - Compat API is ready for use - - Eliminate debug logging -- Apr 10, 2018 - tested backwards-compatibility using greenlock.js -- Apr 5, 2018 - export http and dns challenge tests -- Apr 5, 2018 - test http and dns challenges (success and failure) -- Apr 5, 2018 - test subdomains and its wildcard -- Apr 5, 2018 - test two subdomains -- Apr 5, 2018 - test wildcard -- Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) -- Mar 21, 2018 - _mostly_ matches le-acme-core.js API -- Mar 21, 2018 - can now accept values (not hard coded) -- Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) -- Mar 20, 2018 - download certificate -- Mar 20, 2018 - poll for status -- Mar 20, 2018 - finalize order (submit csr) -- Mar 20, 2018 - generate domain keypair -- Mar 20, 2018 - respond to challenges -- Mar 16, 2018 - get challenges -- Mar 16, 2018 - new order -- Mar 15, 2018 - create account -- Mar 15, 2018 - generate account keypair -- Mar 15, 2018 - get nonce -- Mar 15, 2018 - get directory +Convert domain names to `punycode` before creating the certificate: -# Legal +```js +var punycode = require('punycode'); -[acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | +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](https://www.npmjs.com/search?q=acme-dns-01-) +to run the test locally. + +You'll also need a `.env` that looks something like the one in `examples/example.env`: + +```bash +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: + +```bash +# 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 ` + + + + diff --git a/examples/server.js b/examples/server.js new file mode 100644 index 0000000..89111ca --- /dev/null +++ b/examples/server.js @@ -0,0 +1,174 @@ +'use strict'; + +var crypto = require('crypto'); +//var dnsjs = require('dns-suite'); +var dig = require('dig.js/dns-request'); +var request = require('util').promisify(require('@root/request')); +var express = require('express'); +var app = express(); + +var nameservers = require('dns').getServers(); +var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length; +var nameserver = nameservers[index]; + +app.use('/', express.static(__dirname)); +app.use('/api', express.json()); +app.get('/api/dns/:domain', function(req, res, next) { + var domain = req.params.domain; + var casedDomain = domain + .toLowerCase() + .split('') + .map(function(ch) { + // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is + // ch = ch | 0x20; + return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase(); + }) + .join(''); + var typ = req.query.type; + var query = { + header: { + id: crypto.randomBytes(2).readUInt16BE(0), + qr: 0, + opcode: 0, + aa: 0, // Authoritative-Only + tc: 0, // NA + rd: 1, // Recurse + ra: 0, // NA + rcode: 0 // NA + }, + question: [ + { + name: casedDomain, + //, type: typ || 'A' + typeName: typ || 'A', + className: 'IN' + } + ] + }; + var opts = { + onError: function(err) { + next(err); + }, + onMessage: function(packet) { + var fail0x20; + + if (packet.id !== query.id) { + console.error( + "[SECURITY] ignoring packet for '" + + packet.question[0].name + + "' due to mismatched id" + ); + console.error(packet); + return; + } + + packet.question.forEach(function(q) { + // if (-1 === q.name.lastIndexOf(cli.casedQuery)) + if (q.name !== casedDomain) { + fail0x20 = q.name; + } + }); + + ['question', 'answer', 'authority', 'additional'].forEach(function( + group + ) { + (packet[group] || []).forEach(function(a) { + var an = a.name; + var i = domain + .toLowerCase() + .lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM + var j = a.name + .toLowerCase() + .lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM + + // it's important to note that these should only relpace changes in casing that we expected + // any abnormalities should be left intact to go "huh?" about + // TODO detect abnormalities? + if (-1 !== i) { + // "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4)) + a.name = a.name.replace( + casedDomain.substr(i), + domain.substr(i) + ); + } else if (-1 !== j) { + // "www.example.com".replace("EXamPLE.cOm", "example.com") + a.name = + a.name.substr(0, j) + + a.name.substr(j).replace(casedDomain, domain); + } + + // NOTE: right now this assumes that anything matching the query matches all the way to the end + // it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly + // (but I don't think it should need to) + if (a.name.length !== an.length) { + console.error( + "[ERROR] question / answer mismatch: '" + + an + + "' != '" + + a.length + + "'" + ); + console.error(a); + } + }); + }); + + if (fail0x20) { + console.warn( + ";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" + + casedDomain + + "' but got response for '" + + fail0x20 + + "'." + ); + return; + } + + res.send({ + header: packet.header, + question: packet.question, + answer: packet.answer, + authority: packet.authority, + additional: packet.additional, + edns_options: packet.edns_options + }); + }, + onListening: function() {}, + onSent: function(/*res*/) {}, + onTimeout: function(res) { + console.error('dns timeout:', res); + next(new Error('DNS timeout - no response')); + }, + onClose: function() {}, + //, mdns: cli.mdns + nameserver: nameserver, + port: 53, + timeout: 2000 + }; + + dig.resolveJson(query, opts); +}); +app.get('/api/http', function(req, res) { + var url = req.query.url; + return request({ method: 'GET', url: url }).then(function(resp) { + res.send(resp.body); + }); +}); +app.get('/api/_acme_api_', function(req, res) { + res.send({ success: true }); +}); + +module.exports = app; +if (require.main === module) { + // curl -L http://localhost:3000/api/dns/example.com?type=A + console.info('Listening on localhost:3000'); + app.listen(3000); + console.info('Try this:'); + console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'"); + console.info( + "\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'" + ); + console.info( + "\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'" + ); +} diff --git a/fixtures/account.jwk.json b/fixtures/account.jwk.json new file mode 100644 index 0000000..507063c --- /dev/null +++ b/fixtures/account.jwk.json @@ -0,0 +1,17 @@ +{ + "private": { + "kty": "EC", + "crv": "P-256", + "d": "HB1OvdHfLnIy2mYYO9cLU4BqP36CeyS8OsDf3OnYP-M", + "x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8", + "y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ", + "kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs" + }, + "public": { + "kty": "EC", + "crv": "P-256", + "x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8", + "y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ", + "kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs" + } +} diff --git a/fixtures/account.registration.json b/fixtures/account.registration.json new file mode 100644 index 0000000..7736000 --- /dev/null +++ b/fixtures/account.registration.json @@ -0,0 +1,13 @@ +{ + "key": { + "kty": "EC", + "crv": "P-256", + "x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8", + "y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ", + "kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11265299" + }, + "contact": [], + "initialIp": "66.219.236.169", + "createdAt": "2019-10-04T22:54:28.569489074Z", + "status": "valid" +} diff --git a/fixtures/server.jwk.json b/fixtures/server.jwk.json new file mode 100644 index 0000000..ca65589 --- /dev/null +++ b/fixtures/server.jwk.json @@ -0,0 +1,20 @@ +{ + "private": { + "kty": "RSA", + "n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw", + "e": "AQAB", + "d": "YCzN9yVr4Jw5D_UK7WEMuzGUcMAZZs-TQFgY4UK7Ovbj18_QQrhKElb6Zfhepcf1HUYkO6PVjpuZ1tEl9hWgVcFa781AROyvSj04beiaVMDeSCCwjgW3MM3w6olnxTOUDaBMl9NNiqq0v9riDImkQbAQbe3To-KAH2ig4AMNlSZJAhmI2zAMiJhQE_pAcCxc-bQ5oNO-WSU0GRHWdMJSXp9mFgoBhVPDYGW-dmnoFzuNWssxlSqGXY-8a2YOuiunK6XM5_80c1eQqmy-k1InUIViR_wljskc8UiH6xa8BCznZYacgSz4PnvKsiKWKQQ1eliIucV3MC6BzMD3N8EWqQ", + "p": "8NUtOIglu0dvDGmEB7QC5eC02Y2jZKnoxHSPKMAEPxQ0131_2aL49IzADWoTvae3NBPzU7ol3RwJo_GvS967OysfOr6Od699p1FSLwLfK89aql7_uVPJh4Q43H-W_NtRHKUkv0OmkDiwa4WqBQTVfREdPQ3NJT7vIY-cqH_AMRc", + "q": "xZNIl9NRl3b0_V8Y-7_6_foIu9Sx5ILv2XV7WONDx2jp4vuT7byLm1UWdYPBbxLyd5TAvWqtyvaRtVNyplrD0PyyPK3NxqVJde0uzScAU-bf25DeK30V22Xo7IEZiPZoizrjtzGnS6VVNJmZ-Ictz3xmWIudw5d5XDH12fFRlmU", + "dp": "F1Ld9UqiNNf_NjmF0uUpHrA7c5JXD6mw5E3Ri4XFI4LGd1QtLJuu9qgm9WWfkc-LW5zPBP3TKu3LNThz3KougdV0SdEopQi255xllC34BRso0bUvmPg3XUt94kTtD4ICAf8wZuGbYP5Mf61LQP8t2dXtefs7Me89Y4ewCVWN_HM", + "dq": "oPuT35lgVtCnZ7dPrPjNMpnC-gCg_fcuJPqTiWaLuHQkdjzUWJYTDnqy9Qdo2e8PPx4mOXAtsT1clekrdp5oBOWQ-N4I172fcIXUZ3ZKzxJD_iw4yih-YajUs7exLabQoflWx9KeZIWPOm-ZRCYoznGnFqiT4GWQje1rS6xT9P0", + "qi": "aXkK-w4Npw0BpUEzQ1PURVGm5y5cKIdd-CfEYwub19rronI9EEvuQHoqR7ODtZ_mlIIffHmHaM3ug50fJDB9QDOG4Ioc5S4YxVURT58Ps8at-dQAAP1UgSlV3vhXh4WZRaDECUI_728U3fxQqH78bJsy81mU8MtGU8LR_eTMXx8", + "kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk" + }, + "public": { + "kty": "RSA", + "n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw", + "e": "AQAB", + "kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk" + } +} diff --git a/lib/browser/http.js b/lib/browser/http.js new file mode 100644 index 0000000..1a10790 --- /dev/null +++ b/lib/browser/http.js @@ -0,0 +1,32 @@ +'use strict'; + +var http = module.exports; + +http.request = function(opts) { + return window.fetch(opts.url, opts).then(function(resp) { + var headers = {}; + var result = { + statusCode: resp.status, + headers: headers, + toJSON: function() { + return this; + } + }; + Array.from(resp.headers.entries()).forEach(function(h) { + headers[h[0]] = h[1]; + }); + if (!headers['content-type']) { + return result; + } + if (/json/.test(headers['content-type'])) { + return resp.json().then(function(json) { + result.body = json; + return result; + }); + } + return resp.text().then(function(txt) { + result.body = txt; + return result; + }); + }); +}; diff --git a/lib/browser/sha2.js b/lib/browser/sha2.js new file mode 100644 index 0000000..8252d19 --- /dev/null +++ b/lib/browser/sha2.js @@ -0,0 +1,13 @@ +'use strict'; + +var sha2 = module.exports; + +var encoder = new TextEncoder(); +sha2.sum = function(alg, str) { + var data = str; + if ('string' === typeof data) { + data = encoder.encode(str); + } + var sha = 'SHA-' + String(alg).replace(/^sha-?/i, ''); + return window.crypto.subtle.digest(sha, data); +}; diff --git a/lib/node/http.js b/lib/node/http.js new file mode 100644 index 0000000..61ee480 --- /dev/null +++ b/lib/node/http.js @@ -0,0 +1,19 @@ +'use strict'; + +var http = module.exports; +var promisify = require('util').promisify; +var request = promisify(require('@root/request')); + +http.request = function(opts) { + if (!opts.headers) { + opts.headers = {}; + } + if ( + !Object.keys(opts.headers).some(function(key) { + return 'user-agent' === key.toLowerCase(); + }) + ) { + // TODO opts.headers['User-Agent'] = 'TODO'; + } + return request(opts); +}; diff --git a/lib/node/sha2.js b/lib/node/sha2.js new file mode 100644 index 0000000..150ee47 --- /dev/null +++ b/lib/node/sha2.js @@ -0,0 +1,17 @@ +/* global Promise */ +'use strict'; + +var sha2 = module.exports; +var crypto = require('crypto'); + +sha2.sum = function(alg, str) { + return Promise.resolve().then(function() { + var sha = 'sha' + String(alg).replace(/^sha-?/i, ''); + // utf8 is the default for strings + var buf = Buffer.from(str); + return crypto + .createHash(sha) + .update(buf) + .digest(); + }); +}; diff --git a/native.js b/native.js new file mode 100644 index 0000000..88c90ef --- /dev/null +++ b/native.js @@ -0,0 +1,33 @@ +'use strict'; + +var native = module.exports; +var promisify = require('util').promisify; +var resolveTxt = promisify(require('dns').resolveTxt); + +native._canCheck = function(me) { + me._canCheck = {}; + me._canCheck['http-01'] = true; + me._canCheck['dns-01'] = true; + return Promise.resolve(); +}; + +native._dns01 = function(me, ch) { + // TODO use digd.js + return resolveTxt(ch.dnsHost).then(function(records) { + return { + answer: records.map(function(rr) { + return { + data: rr + }; + }) + }; + }); +}; + +native._http01 = function(me, ch) { + return new me.request({ + url: ch.challengeUrl + }).then(function(resp) { + return resp.body; + }); +}; diff --git a/package-lock.json b/package-lock.json index 9d0a412..00bf7af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,46 +1,227 @@ { - "name": "acme-v2", - "version": "1.8.6", + "name": "@root/acme", + "version": "3.0.0-wip.4", "lockfileVersion": 1, "requires": true, "dependencies": { + "@root/asn1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz", + "integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==", + "requires": { + "@root/encoding": "^1.0.1" + } + }, + "@root/csr": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz", + "integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==", + "dev": true, + "requires": { + "@root/asn1": "^1.0.0", + "@root/pem": "^1.0.4", + "@root/x509": "^0.7.2" + } + }, + "@root/encoding": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz", + "integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==" + }, + "@root/keypairs": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz", + "integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==", + "requires": { + "@root/encoding": "^1.0.1", + "@root/pem": "^1.0.4", + "@root/x509": "^0.7.2" + } + }, + "@root/pem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz", + "integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA==" + }, "@root/request": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz", "integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw==" }, - "dotenv": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", - "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==", + "@root/x509": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz", + "integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==", + "requires": { + "@root/asn1": "^1.0.0", + "@root/encoding": "^1.0.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "eckles": { + "bluebird": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz", + "integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "dig.js": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/dig.js/-/dig.js-1.3.9.tgz", + "integrity": "sha512-O/tSWZuW7AwpjsgePPmTanwvSDL9xF+FzLTJD9byN3C6lk79iMejC/Ahz9CERAXTW4e2TXL1vtqh3T0Ug79ocA==", + "dev": true, + "requires": { + "cli": "^1.0.1", + "dns-suite": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2", + "hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4" + }, + "dependencies": { + "dns-suite": { + "version": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#092008f766540909d27c934211495c9e03705bf3", + "from": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4" + } + } + } + }, + "dns-suite": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/dns-suite/-/dns-suite-1.2.13.tgz", + "integrity": "sha512-veYKPHUc2RfRCe7c4G/iKxhRv0S4InJ3JsW8tEhW6Yb7dn3ac34iozC6cNX0uzHYZUw0BG5V9Fu65L1bx1GeBg==", + "dev": true, + "requires": { + "@root/hexdump": "^1.1.1" + }, + "dependencies": { + "@root/hexdump": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@root/hexdump/-/hexdump-1.1.1.tgz", + "integrity": "sha512-AmrmLOutlzctR599ittO06lINOco1TIqb0c1wu83fP2Eoi5iSvx7kVWC4mDufze8rxPewC+aQOx4e6Pw7izV4A==", + "dev": true + } + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz", + "integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "hexdump.js": { + "version": "git+https://git.coolaj86.com/coolaj86/hexdump.js#222fa7de5036a16397de2fe703c35ac54a3d8d0c", + "from": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "punycode": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", - "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true }, - "keypairs": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", - "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", - "requires": { - "eckles": "^1.4.1", - "rasha": "^1.2.4" - } - }, - "rasha": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz", - "integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" - }, - "rsa-compat": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz", - "integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==", - "requires": { - "keypairs": "^1.2.14" - } + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true } } } diff --git a/package.json b/package.json index a28d431..022a9fd 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,60 @@ { - "name": "acme-v2", - "version": "1.8.6", - "description": "A lightweight library for getting Free SSL certifications through Let's Encrypt, using the ACME protocol.", - "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", - "main": "index.js", + "name": "@root/acme", + "version": "3.0.0-wip.4", + "description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt", + "homepage": "https://rootprojects.org/acme/", + "main": "acme.js", + "browser": { + "./native.js": "./browser.js", + "./lib/node/sha2.js": "./lib/browser/sha2.js", + "./lib/node/http.js": "./lib/browser/http.js" + }, "files": [ - "compat.js", + "*.js", "lib", - "scripts" + "dist" ], "scripts": { + "build": "node_xxx bin/bundle.js", + "lint": "jshint lib bin", "postinstall": "node scripts/postinstall", - "test": "node ./test.js" + "test": "node server.js", + "start": "node server.js" }, "repository": { "type": "git", - "url": "https://git.coolaj86.com/coolaj86/acme-v2.js.git" + "url": "https://git.rootprojects.org/root/acme.js.git" }, "keywords": [ - "Let's Encrypt", "ACME", - "v02", - "v2", - "draft-11", - "draft-12", - "free ssl", - "tls", - "automated https", - "letsencrypt" + "Let's Encrypt", + "EC", + "RSA", + "CSR", + "browser", + "greenlock", + "VanillaJS", + "ZeroSSL" ], - "author": "AJ ONeal (https://solderjs.com/)", + "author": "AJ ONeal (https://coolaj86.com/)", "license": "MPL-2.0", "dependencies": { + "@root/encoding": "^1.0.1", + "@root/keypairs": "^0.9.0", + "@root/pem": "^1.0.4", "@root/request": "^1.3.11", - "rsa-compat": "^2.0.8" + "@root/x509": "^0.7.2" }, "devDependencies": { - "dotenv": "^8.0.0" + "@root/csr": "^0.8.1", + "dig.js": "^1.3.9", + "dns-suite": "^1.2.13", + "dotenv": "^8.1.0", + "punycode": "^1.4.1" + }, + "trulyOptionalDependencies": { + "eslint": "^6.5.1", + "webpack": "^4.41.0", + "webpack-cli": "^3.3.9" } } diff --git a/scripts/postinstall b/scripts/postinstall index c4d6d8b..7c7ba52 100755 --- a/scripts/postinstall +++ b/scripts/postinstall @@ -1,24 +1,4 @@ #!/usr/bin/env node 'use strict'; -// BG WH \u001b[47m -// BOLD \u001b[1m -// RED \u001b[31m -// GREEN \u001b[32m -// RESET \u001b[0m - -setTimeout(function() { - [ - '', - '\u001b[31mGreenlock and ACME.js v3 are on the way!\u001b[0m', - 'Watch for updates at https://indiegogo.com/at/greenlock', - '' - ] - .forEach(function(line) { - console.info(line); - }); -}, 300); - -setTimeout(function() { - // give time to read -}, 1500); +// TODO put postinstall back diff --git a/test.js b/test.js deleted file mode 100644 index b8ff270..0000000 --- a/test.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('dotenv').config(); -require('./examples/dns-01-digitalocean.js'); diff --git a/tests/cb.js b/tests/cb.js deleted file mode 100644 index 68b343b..0000000 --- a/tests/cb.js +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2018 AJ ONeal. All rights reserved -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; - -module.exports.run = function run( - directoryUrl, - RSA, - web, - chType, - email, - accountKeypair, - domainKeypair -) { - // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - var acme2 = require('../').ACME.create({ RSA: RSA }); - acme2.init(directoryUrl).then(function() { - var options = { - agreeToTerms: function(tosUrl, agree) { - agree(null, tosUrl); - }, - setChallenge: function(opts, cb) { - var pathname; - - console.log(''); - console.log('identifier:'); - console.log(opts.identifier); - console.log('hostname:'); - console.log(opts.hostname); - console.log('type:'); - console.log(opts.type); - console.log('token:'); - console.log(opts.token); - console.log('thumbprint:'); - console.log(opts.thumbprint); - console.log('keyAuthorization:'); - console.log(opts.keyAuthorization); - console.log('dnsAuthorization:'); - console.log(opts.dnsAuthorization); - console.log(''); - - if ('http-01' === opts.type) { - pathname = - opts.hostname + - acme2.challengePrefixes['http-01'] + - '/' + - opts.token; - console.log( - "Put the string '" + - opts.keyAuthorization + - "' into a file at '" + - pathname + - "'" - ); - console.log( - "echo '" + opts.keyAuthorization + "' > '" + pathname + "'" - ); - } else if ('dns-01' === opts.type) { - pathname = - acme2.challengePrefixes['dns-01'] + - '.' + - opts.hostname.replace(/^\*\./, ''); - console.log( - "Put the string '" + - opts.dnsAuthorization + - "' into the TXT record '" + - pathname + - "'" - ); - console.log( - 'ddig TXT ' + pathname + " '" + opts.dnsAuthorization + "'" - ); - } else { - cb(new Error('[acme-v2] unrecognized challenge type')); - return; - } - console.log("\nThen hit the 'any' key to continue..."); - - function onAny() { - console.log("'any' key was hit"); - process.stdin.pause(); - process.stdin.removeListener('data', onAny); - process.stdin.setRawMode(false); - cb(); - } - - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on('data', onAny); - }, - removeChallenge: function(opts, cb) { - // hostname, key - console.log( - '[acme-v2] remove challenge', - opts.hostname, - opts.keyAuthorization - ); - setTimeout(cb, 1 * 1000); - }, - challengeType: chType, - email: email, - accountKeypair: accountKeypair, - domainKeypair: domainKeypair, - domains: web - }; - - acme2.accounts.create(options).then(function(account) { - console.log('[acme-v2] account:'); - console.log(account); - - acme2.certificates.create(options).then(function(fullchainPem) { - console.log('[acme-v2] fullchain.pem:'); - console.log(fullchainPem); - }); - }); - }); -}; diff --git a/tests/compat.js b/tests/compat.js deleted file mode 100644 index 363dfba..0000000 --- a/tests/compat.js +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2018 AJ ONeal. All rights reserved -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; - -module.exports.run = function( - directoryUrl, - RSA, - web, - chType, - email, - accountKeypair, - domainKeypair -) { - console.log('[DEBUG] run', web, chType, email); - - var acme2 = require('../compat.js').ACME.create({ RSA: RSA }); - acme2.getAcmeUrls(acme2.stagingServerUrl, function(err /*, directoryUrls*/) { - if (err) { - console.log('err 1'); - throw err; - } - - var options = { - agreeToTerms: function(tosUrl, agree) { - agree(null, tosUrl); - }, - setChallenge: function(hostname, token, val, cb) { - var pathname; - - if ('http-01' === cb.type) { - pathname = hostname + acme2.acmeChallengePrefix + token; - console.log( - "Put the string '" + - val /*keyAuthorization*/ + - "' into a file at '" + - pathname + - "'" - ); - console.log( - "echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'" - ); - console.log("\nThen hit the 'any' key to continue..."); - } else if ('dns-01' === cb.type) { - // forwards-backwards compat - pathname = - acme2.challengePrefixes['dns-01'] + - '.' + - hostname.replace(/^\*\./, ''); - console.log( - "Put the string '" + - cb.dnsAuthorization + - "' into the TXT record '" + - pathname + - "'" - ); - console.log('dig TXT ' + pathname + " '" + cb.dnsAuthorization + "'"); - console.log("\nThen hit the 'any' key to continue..."); - } else { - cb(new Error('[acme-v2] unrecognized challenge type: ' + cb.type)); - return; - } - - function onAny() { - console.log("'any' key was hit"); - process.stdin.pause(); - process.stdin.removeListener('data', onAny); - process.stdin.setRawMode(false); - cb(); - } - - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on('data', onAny); - }, - removeChallenge: function(hostname, key, cb) { - console.log('[DEBUG] remove challenge', hostname, key); - setTimeout(cb, 1 * 1000); - }, - challengeType: chType, - email: email, - accountKeypair: accountKeypair, - domainKeypair: domainKeypair, - domains: web - }; - - acme2.registerNewAccount(options, function(err, account) { - if (err) { - console.log('err 2'); - throw err; - } - if (options.debug) console.debug('account:'); - if (options.debug) console.log(account); - - acme2.getCertificate(options, function(err, fullchainPem) { - if (err) { - console.log('err 3'); - throw err; - } - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }); - }); - }); -}; diff --git a/tests/generate-cert-key.js b/tests/generate-cert-key.js new file mode 100644 index 0000000..70e89c9 --- /dev/null +++ b/tests/generate-cert-key.js @@ -0,0 +1,15 @@ +'use strict'; + +async function run() { + var Keypairs = require('@root/keypairs'); + + var certKeypair = await Keypairs.generate({ kty: 'RSA' }); + console.log(certKeypair); + var pem = await Keypairs.export({ + jwk: certKeypair.private, + encoding: 'pem' + }); + console.log(pem); +} + +run(); diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..9df6e21 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,225 @@ +'use strict'; + +require('dotenv').config(); + +var CSR = require('@root/csr'); +var Enc = require('@root/encoding/base64'); +var PEM = require('@root/pem'); +var punycode = require('punycode'); +var ACME = require('../acme.js'); +var Keypairs = require('@root/keypairs'); +var acme = ACME.create({ + // debug: true +}); + +// TODO exec npm install --save-dev CHALLENGE_MODULE + +var config = { + env: process.env.ENV, + email: process.env.SUBSCRIBER_EMAIL, + domain: process.env.BASE_DOMAIN, + challengeType: process.env.CHALLENGE_TYPE, + challengeModule: process.env.CHALLENGE_PLUGIN, + challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS) +}; +config.debug = !/^PROD/i.test(config.env); +var pluginPrefix = 'acme-' + config.challengeType + '-'; +var pluginName = config.challengeModule; +var plugin; + +function badPlugin(err) { + if ('MODULE_NOT_FOUND' !== err.code) { + console.error(err); + return; + } + console.error("Couldn't find '" + pluginName + "'. Is it installed?"); + console.error("\tnpm install --save-dev '" + pluginName + "'"); +} +try { + plugin = require(pluginName); +} catch (err) { + if ( + 'MODULE_NOT_FOUND' !== err.code || + 0 === pluginName.indexOf(pluginPrefix) + ) { + badPlugin(err); + process.exit(1); + } + try { + pluginName = pluginPrefix + pluginName; + plugin = require(pluginName); + } catch (e) { + badPlugin(e); + process.exit(1); + } +} + +config.challenger = plugin.create(config.challengeOptions); +if (!config.challengeType || !config.domain) { + console.error( + new Error('Missing config variables. Check you .env and the docs') + .message + ); + console.error(config); + process.exit(1); +} + +var challenges = {}; +challenges[config.challengeType] = config.challenger; + +async function happyPath(accKty, srvKty, rnd) { + var agreed = false; + var metadata = await acme.init( + 'https://acme-staging-v02.api.letsencrypt.org/directory' + ); + + // Ready to use, show page + if (config.debug) { + console.info('ACME.js initialized'); + console.info(metadata); + console.info(); + console.info(); + } + + var accountKeypair = await Keypairs.generate({ kty: accKty }); + if (config.debug) { + console.info('Account Key Created'); + console.info(JSON.stringify(accountKeypair, null, 2)); + console.info(); + console.info(); + } + + var account = await acme.accounts.create({ + agreeToTerms: agree, + // TODO detect jwk/pem/der? + accountKeypair: { privateKeyJwk: accountKeypair.private }, + subscriberEmail: config.email + }); + + // TODO top-level agree + function agree(tos) { + if (config.debug) { + console.info('Agreeing to Terms of Service:'); + console.info(tos); + console.info(); + console.info(); + } + agreed = true; + return Promise.resolve(tos); + } + if (config.debug) { + console.info('New Subscriber Account'); + console.info(JSON.stringify(account, null, 2)); + console.info(); + console.info(); + } + if (!agreed) { + throw new Error('Failed to ask the user to agree to terms'); + } + + var certKeypair = await Keypairs.generate({ kty: srvKty }); + var pem = await Keypairs.export({ + jwk: certKeypair.private, + encoding: 'pem' + }); + if (config.debug) { + console.info('Server Key Created'); + console.info('privkey.jwk.json'); + console.info(JSON.stringify(certKeypair, null, 2)); + // This should be saved as `privkey.pem` + console.info(); + console.info('privkey.' + srvKty.toLowerCase() + '.pem:'); + console.info(pem); + console.info(); + } + + // 'subject' should be first in list + var domains = randomDomains(rnd); + if (config.debug) { + console.info('Get certificates for random domains:'); + console.info( + domains + .map(function(puny) { + var uni = punycode.toUnicode(puny); + if (puny !== uni) { + return puny + ' (' + uni + ')'; + } + return puny; + }) + .join('\n') + ); + console.info(); + } + + // Create CSR + var csrDer = await CSR.csr({ + jwk: certKeypair.private, + domains: domains, + encoding: 'der' + }); + var csr = Enc.bufToUrlBase64(csrDer); + var csrPem = PEM.packBlock({ + type: 'CERTIFICATE REQUEST', + bytes: csrDer /* { jwk: jwk, domains: opts.domains } */ + }); + if (config.debug) { + console.info('Certificate Signing Request'); + console.info(csrPem); + console.info(); + } + + var results = await acme.certificates.create({ + account: account, + accountKeypair: { privateKeyJwk: accountKeypair.private }, + csr: csr, + domains: domains, + challenges: challenges, // must be implemented + customerEmail: null + }); + + if (config.debug) { + console.info('Got SSL Certificate:'); + console.info(Object.keys(results)); + console.info(results.expires); + console.info(results.cert); + console.info(results.chain); + console.info(); + console.info(); + } +} + +// Try EC + RSA +var rnd = random(); +happyPath('EC', 'RSA', rnd) + .then(function() { + // Now try RSA + EC + rnd = random(); + return happyPath('RSA', 'EC', rnd).then(function() { + console.info('success'); + }); + }) + .catch(function(err) { + console.error('Error:'); + console.error(err.stack); + }); + +function randomDomains(rnd) { + return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map( + function(pre) { + return punycode.toASCII(pre + '-' + rnd + '.' + config.domain); + } + ); +} + +function random() { + return ( + parseInt( + Math.random() + .toString() + .slice(2, 99), + 10 + ) + .toString(16) + .slice(0, 4) + '例' + ); +} diff --git a/tests/promise.js b/tests/promise.js deleted file mode 100644 index 90107a5..0000000 --- a/tests/promise.js +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2018 AJ ONeal. All rights reserved -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; - -/* global Promise */ -module.exports.run = function run( - directoryUrl, - RSA, - web, - chType, - email, - accountKeypair, - domainKeypair -) { - var acme2 = require('../').ACME.create({ RSA: RSA }); - // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - acme2.init(directoryUrl).then(function() { - var options = { - agreeToTerms: function(tosUrl) { - return Promise.resolve(tosUrl); - }, - setChallenge: function(opts) { - return new Promise(function(resolve, reject) { - var pathname; - - console.log(''); - console.log('identifier:'); - console.log(opts.identifier); - console.log('hostname:'); - console.log(opts.hostname); - console.log('type:'); - console.log(opts.type); - console.log('token:'); - console.log(opts.token); - console.log('thumbprint:'); - console.log(opts.thumbprint); - console.log('keyAuthorization:'); - console.log(opts.keyAuthorization); - console.log('dnsAuthorization:'); - console.log(opts.dnsAuthorization); - console.log(''); - - if ('http-01' === opts.type) { - pathname = - opts.hostname + - acme2.challengePrefixes['http-01'] + - '/' + - opts.token; - console.log( - "Put the string '" + - opts.keyAuthorization + - "' into a file at '" + - pathname + - "'" - ); - console.log( - "echo '" + opts.keyAuthorization + "' > '" + pathname + "'" - ); - } else if ('dns-01' === opts.type) { - pathname = - acme2.challengePrefixes['dns-01'] + - '.' + - opts.hostname.replace(/^\*\./, ''); - console.log( - "Put the string '" + - opts.dnsAuthorization + - "' into the TXT record '" + - pathname + - "'" - ); - console.log( - 'dig TXT ' + pathname + " '" + opts.dnsAuthorization + "'" - ); - } else { - reject(new Error('[acme-v2] unrecognized challenge type')); - return; - } - console.log("\nThen hit the 'any' key to continue..."); - - function onAny() { - console.log("'any' key was hit"); - process.stdin.pause(); - process.stdin.removeListener('data', onAny); - process.stdin.setRawMode(false); - resolve(); - return; - } - - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on('data', onAny); - }); - }, - removeChallenge: function(opts) { - console.log( - '[acme-v2] remove challenge', - opts.hostname, - opts.keyAuthorization - ); - return new Promise(function(resolve) { - // hostname, key - setTimeout(resolve, 1 * 1000); - }); - }, - challengeType: chType, - email: email, - accountKeypair: accountKeypair, - domainKeypair: domainKeypair, - domains: web - }; - - acme2.accounts.create(options).then(function(account) { - console.log('[acme-v2] account:'); - console.log(account); - - acme2.certificates.create(options).then(function(fullchainPem) { - console.log('[acme-v2] fullchain.pem:'); - console.log(fullchainPem); - }); - }); - }); -}; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..d3d2b12 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,20 @@ +'use strict'; + +var path = require('path'); + +module.exports = { + entry: './examples/app.js', + //entry: './acme.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'app.js' + //filename: 'acme.js', + //library: '@root/acme', + //libraryTarget: 'umd' + //globalObject: "typeof self !== 'undefined' ? self : this" + }, + resolve: { + aliasFields: ['webpack', 'browser'], + mainFields: ['browser', 'main'] + } +};