diff --git a/.gitignore b/.gitignore index d5ac8b7..8072530 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ .env -*.gz -.*.sw* -.ignore *.pem @@ -17,5 +14,4 @@ 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 7e5d770..420e082 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,7 +2,7 @@ "bracketSpacing": true, "printWidth": 80, "singleQuote": true, - "tabWidth": 4, + "tabWidth": 2, "trailingComma": "none", "useTabs": true } diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 56bb2dc..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,53 +0,0 @@ -# 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 2f965da..3435503 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2015-2019 AJ ONeal +Copyright 2018 AJ ONeal Mozilla Public License Version 2.0 ================================== diff --git a/README.md b/README.md index f8d57e4..b0c1f9a 100644 --- a/README.md +++ b/README.md @@ -1,437 +1,232 @@ -# Let's Encrypt™ + JavaScript = [ACME.js](https://git.rootprojects.org/root/acme.js) +| **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) -| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub) +# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project -## Automated Certificate Management Environment +A **Zero (External) Dependency**\* library for building +Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates. -ACME ([RFC 8555](https://tools.ietf.org/html/rfc8555)) is the protocol that powers **Let's Encrypt**. - -ACME.js is a _low-level_ client that speaks RFC 8555 to get Free SSL certificates through Let's Encrypt. - -Looking for an **easy**, _high-level_ client? Check out [Greenlock.js](https://git.rootprojects.org/root/greenlock.js). - -# Quick Start - -```js -var acme = ACME.create({ maintainerEmail, packageAgent, notify }); -await acme.init(directoryUrl); - -// Create Let's Encrypt Account -var accountOptions = { subscriberEmail, agreeToTerms, accountKey }; -var account = await acme.accounts.create(accountOptions); - -// Validate Domains -var certificateOptions = { account, accountKey, csr, domains, challenges }; -var pems = await acme.certificates.create(certificateOptions); - -// Get SSL Certificate -var fullchain = pems.cert + '\n' + pems.chain + '\n'; -await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii'); -``` - -# Online Demo - -See https://greenlock.domains - - +The primary goal of this library is to make it easy to +get Accounts and Certificates through Let's Encrypt. # Features -| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments | +- [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)) -Supports the latest (Nov 2019) release of Let's Encrypt in a small, lightweight, Vanilla JS package. +\* 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. -- [x] Let's Encrypt v2 - - [x] ACME RFC 8555 - - [x] November 2019 - - [x] POST-as-GET - - [ ] StartTLS Everywhere™ (in-progress) -- [x] IDN (i.e. `.中国`) -- [x] ECDSA and RSA keypairs - - [x] JWK - - [x] PEM - - [x] DER - - [x] Native Crypto in Node.js - - [x] WebCrypto in Browsers -- [x] Domain Validation Plugins - - [x] tls-alpn-01 - - [x] http-01 - - [x] dns-01 - - [x] **Wildcards** - - [x] **Localhost** - - [x] Private Networks - - [x] [Create your own](https://git.rootprojects.org/root/acme-challenge-test.js) -- [x] Vanilla JS\* - - [x] No Transpiling Necessary! - - [x] Node.js - - [x] Browsers - - [x] WebPack - - [x] Zero External Dependencies -- [x] Commercial Support - - [x] Safe, Efficient, Maintained +## Looking for Quick 'n' Easy™? -\* Although we use `async/await` in the examples, -the codebase is written entirely in Common JS. +If you want something that's more "batteries included" give +[greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +a try. -# Use Cases +- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) -- Home Servers -- IoT -- Enterprise On-Prem -- Web Hosting -- Cloud Services -- Localhost Development +## v1.7+: Transitional v2 Support -# API +By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18). -The public API encapsulates the three high-level steps of the ACME protocol: +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. -1. API Discovery -2. Account Creation - - Subscriber Agreement -3. Certificate Issuance - - Certificate Request - - Authorization Challenges - - Challenge Presentation - - Certificate Redemption +We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify** +the code with a fresh new release. -## API Overview +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 -The core API can be show in just four functions: +## Recommended Example + +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. ```js -ACME.create({ maintainerEmail, packageAgent, notify }); -acme.init(directoryUrl); -acme.accounts.create({ subscriberEmail, agreeToTerms, accountKey }); -acme.certificates.create({ - customerEmail, // do not use - account, - accountKey, - csr, - domains, - challenges +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'; + }, + + // don't try to validate challenges locally + skipChallengeTest: false, + skipDryRun: false, + + // 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 }); + +// Discover Directory URLs +ACME.init(acmeDirectoryUrl); // returns Promise + +// Accounts +ACME.accounts.create(options); // returns Promise registration data + +options = { + email: '', // valid email (server checks MX records) + accountKeypair: { + // privateKeyPem or privateKeyJwt + privateKeyPem: '' + }, + agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back +}; + +// Registration +ACME.certificates.create(options); // returns Promise + +options = { + domainKeypair: { + privateKeyPem: '' + }, + accountKeypair: { + privateKeyPem: '' + }, + domains: ['example.com'], + + 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 +}; ``` -Helper Functions - -```js -ACME.computeChallenge({ - accountKey, - hostname: 'example.com', - challenge: { type: 'dns-01', token: 'xxxx' } -}); -``` - -| 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 `@root/csr`, 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 | -| maintainerEmail | should be a contact for the author of the code to receive critical bug and security notices | -| notify | all callback for logging events and errors in the form `function (ev, args) { ... }` | -| packageAgent | should be an RFC72321-style user-agent string to append to the ACME client (ex: mypackage/v1.1.1) | -| skipChallengeTests | do not do a self-check that the ACME-issued challenges will pass (not recommended) | -| skipDryRun: false | do not do a self-check with self-issued challenges (not recommended) | -| subscriberEmail | should be a contact for the service provider to receive renewal failure notices and manage the ACME account | - -**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. - -## Events - -These `notify` events are intended for _logging_ and debugging, NOT as a data API. - -| Event Name | Example Message | -| -------------------- | --------------------------------------------------------------------------------- | -| `certificate_order` | `{ subject: 'example.com', altnames: ['...'], account: { key: { kid: '...' } } }` | -| `challenge_select` | `{ altname: '*.example.com', type: 'dns-01' }` | -| `challenge_status` | `{ altname: '*.example.com', type: 'dns-01', status: 'pending' }` | -| `challenge_remove` | `{ altname: '*.example.com', type: 'dns-01' }` | -| `certificate_status` | `{ subject: 'example.com', status: 'valid' }` | -| `warning` | `{ message: 'what went wrong', description: 'what action to take about it' }` | -| `error` | `{ message: 'a background process failed, and it may have side-effects' }` | - -Note: DO NOT rely on **undocumented properties**. They are experimental and **will break**. -If you have a use case for a particular property **open an issue** - we can lock it down and document it. - -# Example (Full Walkthrough) - -### See [examples/README.md](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/README.md) - -A basic example includes the following: - -1. Initialization - - maintainer contact - - package user-agent - - log events -2. Discover API - - retrieves Terms of Service and API endpoints -3. Get Subscriber Account - - create an ECDSA (or RSA) Account key in JWK format - - agree to terms - - register account by the key -4. Prepare a Certificate Signing Request - - create a RSA (or ECDSA) Server key in PEM format - - select domains - - choose challenges - - sign CSR - - order certificate - -[examples/README.md](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/README.md) -covers all of these steps, with comments. - -# Install - -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) - -
-Node.js - -```js -npm install --save @root/acme -``` - -```js -var ACME = require('@root/acme'); -``` - -
- -
-WebPack - -```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"`) - -```html - -``` - -`acme.min.js` - -```html - -``` - -Use - -```js -var ACME = window['@root/acme']; -``` - -
- -# Challenge Callbacks - -The challenge callbacks are documented in the [test suite](https://git.rootprojects.org/root/acme-dns-01-test.js), -essentially: - -```js -function create(options) { - var plugin = { - init: async function(deps) { - // for http requests - plugin.request = deps.request; - }, - zones: async function(args) { - // list zones relevant to the altnames - }, - set: async function(args) { - // set TXT record - }, - get: async function(args) { - // get TXT records - }, - remove: async function(args) { - // remove TXT record - }, - // how long to wait after *all* TXT records are set - // before presenting them for validation - propagationDelay: 5000 - }; - return plugin; -} -``` - -The `http-01` plugin is similar, but without `zones` or `propagationDelay`. - -Many challenge plugins are already available for popular platforms. - -Search `acme-http-01-` or `acme-dns-01-` on npm to find more. - -| Type | Service | Plugin | -| ----------- | ----------------------------------------------------------------------------------- | ------------------------ | -| dns-01 | CloudFlare | acme-dns-01-cloudflare | -| dns-01 | [Digital Ocean](https://git.rootprojects.org/root/acme-dns-01-digitalocean.js) | acme-dns-01-digitalocean | -| dns-01 | [DNSimple](https://git.rootprojects.org/root/acme-dns-01-dnsimple.js) | acme-dns-01-dnsimple | -| dns-01 | [DuckDNS](https://git.rootprojects.org/root/acme-dns-01-duckdns.js) | acme-dns-01-duckdns | -| http-01 | File System / [Web Root](https://git.rootprojects.org/root/acme-http-01-webroot.js) | acme-http-01-webroot | -| dns-01 | [GoDaddy](https://git.rootprojects.org/root/acme-dns-01-godaddy.js) | acme-dns-01-godaddy | -| dns-01 | [Gandi](https://git.rootprojects.org/root/acme-dns-01-gandi.js) | acme-dns-01-gandi | -| dns-01 | [NameCheap](https://git.rootprojects.org/root/acme-dns-01-namecheap.js) | acme-dns-01-namecheap | -| dns-01 | [Name.com](https://git.rootprojects.org/root/acme-dns-01-namedotcom.js) | acme-dns-01-namedotcom | -| dns-01 | Route53 (AWS) | acme-dns-01-route53 | -| http-01 | S3 (AWS, Digital Ocean, Scaleway) | acme-http-01-s3 | -| dns-01 | [Vultr](https://git.rootprojects.org/root/acme-dns-01-vultr.js) | acme-dns-01-vultr | -| dns-01 | [Build your own](https://git.rootprojects.org/root/acme-dns-01-test.js) | acme-dns-01-test | -| http-01 | [Build your own](https://git.rootprojects.org/root/acme-http-01-test.js) | acme-http-01-test | -| tls-alpn-01 | [Contact us](mailto:support@therootcompany.com) | - | - -# Running the Tests - -```bash -npm test -``` - -## Usa a dns-01 challenge - -Although you can run the tests from a public facing server, its easiest to do so using a dns-01 challenge. - -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. - -```bash -ENV=DEV -MAINTAINER_EMAIL=letsencrypt+staging@example.com -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/acme.js -pushd acme.js/ - -# Install the challenge plugin you'll use for the tests -npm install --save-dev acme-dns-01-digitalocean -``` - -## Create a `.env` config - -You'll need a `.env` in the project root that looks something like the one in `examples/example.env`: - -```bash -# 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 - -Join `@rootprojects` `#general` on [Keybase](https://keybase.io) if you'd like to chat with us. - -# Contributions - -Did this project save you some time? Maybe make your day? Even save the day? - -Please say "thanks" via Paypal or Patreon: - -- Paypal: [\$5](https://paypal.me/rootprojects/5) | [\$10](https://paypal.me/rootprojects/10) | Any amount: -- Patreon: - -Where does your contribution go? - -[Root](https://therootcompany.com) is a collection of experts -who trust each other and enjoy working together on deep-tech, -Indie Web projects. - -Our goal is to operate as a sustainable community. - -Your contributions - both in code and _especially_ financially - -help to not just this project, but also our broader work -of [projects](https://rootprojects.org) that fuel the **Indie Web**. - -Also, we chat on [Keybase](https://keybase.io) -in [#rootprojects](https://keybase.io/team/rootprojects) - -# Commercial Support - -Do you need... - -- more features? -- bugfixes, on _your_ timeline? -- custom code, built by experts? -- commercial support and licensing? - -You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem, -Enterprise, and Internal installations, integrations, and deployments. - -We have both commercial support and commercial licensing available. - -We also offer consulting for all-things-ACME and Let's Encrypt. - -# Legal & Rules of the Road - -ACME.js™ is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal - -The rule of thumb is "attribute, but don't confuse". For example: - -> Built with [ACME.js](https://git.rootprojects.org/root/acme.js) (a [Root](https://rootprojects.org) project). - -Please [contact us](mailto:aj@therootcompany.com) 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](https://git.rootprojects.org/root/acme.js) | +# Changelog + +- 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 + +# Legal + +[acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | MPL-2.0 | [Terms of Use](https://therootcompany.com/legal/#terms) | [Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/account.js b/account.js deleted file mode 100644 index 14777b0..0000000 --- a/account.js +++ /dev/null @@ -1,175 +0,0 @@ -'use strict'; - -var A = module.exports; -var U = require('./utils.js'); - -var Keypairs = require('@root/keypairs'); -var Enc = require('@root/encoding/bytes'); -var agreers = {}; - -A._getAccountKid = function (me, options) { - // It's just fine if there's no account, we'll go get the key id we need via the existing key - var kid = - options.kid || - (options.account && options.account.key && options.account.key.kid); - - if (kid) { - return Promise.resolve(kid); - } - - //return Promise.reject(new Error("must include KeyID")); - // This is an idempotent request. It'll return the same account for the same public key. - return A._registerAccount(me, options).then(function (account) { - return account.key.kid; - }); -}; - -// ACME RFC Section 7.3 Account Creation -/* - { - "protected": base64url({ - "alg": "ES256", - "jwk": {...}, - "nonce": "6S8IqOGY7eL2lsGoTZYifg", - "url": "https://example.com/acme/new-account" - }), - "payload": base64url({ - "termsOfServiceAgreed": true, - "onlyReturnExisting": false, - "contact": [ - "mailto:cert-admin@example.com", - "mailto:admin@example.com" - ] - }), - "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" - } -*/ -A._registerAccount = function (me, options) { - //#console.debug('[ACME.js] accounts.create'); - - function agree(agreed) { - var err; - if (!agreed) { - err = new Error("must agree to '" + me._tos + "'"); - err.code = 'E_AGREE_TOS'; - throw err; - } - return true; - } - - function getAccount() { - return U._importKeypair(options.accountKey).then(function (pair) { - var contact; - if (options.contact) { - contact = options.contact.slice(0); - } else if (options.subscriberEmail) { - contact = ['mailto:' + options.subscriberEmail]; - } - - var accountRequest = { - termsOfServiceAgreed: true, - onlyReturnExisting: false, - contact: contact - }; - - var pub = pair.public; - return attachExtAcc(pub, accountRequest).then(function (accReq) { - var payload = JSON.stringify(accReq); - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: me._directoryUrls.newAccount, - protected: { kid: false, jwk: pair.public }, - payload: Enc.strToBuf(payload) - }).then(function (resp) { - var account = resp.body; - - if (resp.statusCode < 200 || resp.statusCode >= 300) { - if ('string' !== typeof account) { - account = JSON.stringify(account); - } - throw new Error( - 'account error: ' + - resp.statusCode + - ' ' + - account + - '\n' + - payload - ); - } - - // the account id url is the "kid" - var kid = resp.headers.location; - if (!account) { - account = { _emptyResponse: true }; - } - if (!account.key) { - account.key = {}; - } - account.key.kid = kid; - return account; - }); - }); - }); - } - - // for external accounts (probably useless, but spec'd) - function attachExtAcc(pubkey, accountRequest) { - if (!options.externalAccount) { - return Promise.resolve(accountRequest); - } - - return Keypairs.signJws({ - // TODO is HMAC the standard, or is this arbitrary? - secret: options.externalAccount.secret, - protected: { - alg: options.externalAccount.alg || 'HS256', - kid: options.externalAccount.id, - url: me._directoryUrls.newAccount - }, - payload: Enc.strToBuf(JSON.stringify(pubkey)) - }).then(function (jws) { - accountRequest.externalAccountBinding = jws; - return accountRequest; - }); - } - - return Promise.resolve() - .then(function () { - //#console.debug('[ACME.js] agreeToTerms'); - var agreeToTerms = options.agreeToTerms; - if (!agreeToTerms) { - agreeToTerms = function (terms) { - if (agreers[options.subscriberEmail]) { - return true; - } - agreers[options.subscriberEmail] = true; - console.info(); - console.info( - 'By using this software you (' + - options.subscriberEmail + - ') are agreeing to the following:' - ); - console.info( - 'ACME Subscriber Agreement:', - terms.acmeSubscriberTermsUrl - ); - console.info( - 'Greenlock/ACME.js Terms of Use:', - terms.acmeJsTermsUrl - ); - console.info(); - return true; - }; - } else if (true === agreeToTerms) { - agreeToTerms = function (terms) { - return terms && true; - }; - } - return agreeToTerms({ - acmeSubscriberTermsUrl: me._tos, - acmeJsTermsUrl: 'https://rootprojects.org/legal/#terms' - }); - }) - .then(agree) - .then(getAccount); -}; diff --git a/acme.js b/acme.js deleted file mode 100644 index 91415d3..0000000 --- a/acme.js +++ /dev/null @@ -1,1403 +0,0 @@ -// Copyright 2018-present 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'; -/* globals Promise */ - -require('@root/encoding/bytes'); -var Enc = require('@root/encoding/base64'); -var ACME = module.exports; -var Keypairs = require('@root/keypairs'); -var CSR = require('@root/csr'); -var sha2 = require('@root/keypairs/lib/node/sha2.js'); -var http = require('./lib/node/http.js'); -var A = require('./account.js'); -var U = require('./utils.js'); -var E = require('./errors.js'); -var M = require('./maintainers.js'); - -var native = require('./lib/native.js'); - -ACME.create = function create(me) { - if (!me) { - me = {}; - } - - // me.debug = true; - me._nonces = []; - me._canCheck = {}; - - if (!/.+@.+\..+/.test(me.maintainerEmail)) { - throw new Error( - 'you should supply `maintainerEmail` as a contact for security and critical bug notices' - ); - } - - if (!/\w\/v?\d/.test(me.packageAgent) && false !== me.packageAgent) { - console.error( - "\nyou should supply `packageAgent` as an rfc7231-style User-Agent such as Foo/v1.1\n\n\t// your package agent should be this:\n\tvar pkg = require('./package.json');\n\tvar agent = pkg.name + '/' + pkg.version\n" - ); - process.exit(1); - return; - } - - if (!me.dns01) { - me.dns01 = function (ch) { - return native._dns01(me, ch); - }; - } - - if (!me.http01) { - // for browser version only - if (!me._baseUrl) { - me._baseUrl = ''; - } - me.http01 = function (ch) { - return native._http01(me, ch); - }; - } - - if (!me.__request) { - me.__request = http.request; - } - // passed to dependencies - me.request = function (opts) { - return U._request(me, opts); - }; - - me.init = function (opts) { - M.init(me); - - function fin(dir) { - me._directoryUrls = dir; - me._tos = dir.meta.termsOfService; - return dir; - } - if (opts && opts.meta && opts.termsOfService) { - return Promise.resolve(fin(opts)); - } - if (!me.directoryUrl) { - me.directoryUrl = opts; - } - if ('string' !== typeof me.directoryUrl) { - throw new Error( - 'you must supply either the ACME directory url as a string or an object of the ACME urls' - ); - } - - var p = Promise.resolve(); - if (!me.skipChallengeTest) { - p = native._canCheck(me); - } - return p.then(function () { - return ACME._directory(me).then(function (resp) { - return fin(resp.body); - }); - }); - }; - me.accounts = { - create: function (options) { - try { - return A._registerAccount(me, options); - } catch (e) { - return Promise.reject(e); - } - } - }; - /* - me.authorizations = { - // create + get challlenges - get: function(options) { - return A._getAccountKid(me, options).then(function(kid) { - ACME._normalizePresenters(me, options, options.challenges); - return ACME._orderCert(me, options, kid).then(function(order) { - return order.claims; - }); - }); - }, - // set challenges, check challenges, finalize order, return order - present: function(options) { - return A._getAccountKid(me, options).then(function(kid) { - ACME._normalizePresenters(me, options, options.challenges); - return ACME._finalizeOrder(me, options, kid, options.order); - }); - } - }; - */ - me.certificates = { - create: function (options) { - return A._getAccountKid(me, options).then(function (kid) { - ACME._normalizePresenters(me, options, options.challenges); - return ACME._getCertificate(me, options, kid); - }); - } - }; - return me; -}; - -// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} -// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" -ACME.challengePrefixes = { - 'http-01': '/.well-known/acme-challenge', - 'dns-01': '_acme-challenge' -}; -ACME.challengeTests = { - 'http-01': function (me, auth) { - var ch = auth.challenge; - return me.http01(ch).then(function (keyAuth) { - var err; - - // TODO limit the number of bytes that are allowed to be downloaded - if (ch.keyAuthorization === (keyAuth || '').trim()) { - return true; - } - - err = new Error( - 'Error: Failed HTTP-01 Pre-Flight / Dry Run.\n' + - "curl '" + - ch.challengeUrl + - "'\n" + - "Expected: '" + - ch.keyAuthorization + - "'\n" + - "Got: '" + - keyAuth + - "'\n" + - 'See https://git.rootprojects.org/root/acme.js/issues/4' - ); - err.code = 'E_FAIL_DRY_CHALLENGE'; - throw err; - }); - }, - 'dns-01': function (me, auth) { - // remove leading *. on wildcard domains - var ch = auth.challenge; - return me.dns01(ch).then(function (ans) { - var err; - - if ( - ans.answer.some(function (txt) { - return ch.dnsAuthorization === txt.data[0]; - }) - ) { - return true; - } - - err = new Error( - 'Error: Failed DNS-01 Pre-Flight Dry Run.\n' + - "dig TXT '" + - ch.dnsHost + - "' does not return '" + - ch.dnsAuthorization + - "'\n" + - 'See https://git.rootprojects.org/root/acme.js/issues/4' - ); - err.code = 'E_FAIL_DRY_CHALLENGE'; - throw err; - }); - } -}; - -ACME._directory = function (me) { - // TODO cache the directory URL - - // GET-as-GET ok - return U._request(me, { method: 'GET', url: me.directoryUrl, json: true }); -}; - -// registerAccount -// postChallenge -// finalizeOrder -// getCertificate -ACME._getCertificate = function (me, options, kid) { - //#console.debug('[ACME.js] certificates.create'); - return ACME._orderCert(me, options, kid).then(function (order) { - return ACME._finalizeOrder(me, options, kid, order); - }); -}; -ACME._normalizePresenters = function (me, options, presenters) { - // Prefer this order for efficiency: - // * http-01 is the fasest - // * tls-alpn-01 is for networks that don't allow plain traffic - // * dns-01 is the slowest (due to DNS propagation), - // but is required for private networks and wildcards - var presenterTypes = Object.keys(options.challenges || {}); - options._presenterTypes = ['http-01', 'tls-alpn-01', 'dns-01'].filter( - function (typ) { - return -1 !== presenterTypes.indexOf(typ); - } - ); - if ( - presenters['dns-01'] && - 'number' !== typeof presenters['dns-01'].propagationDelay - ) { - if (!ACME._propagationDelayWarning) { - var err = new Error( - "dns-01 challenge's `propagationDelay` not set, defaulting to 5000ms" - ); - err.code = 'E_NO_DNS_DELAY'; - err.description = - "Each dns-01 challenge should specify challenges['dns-01'].propagationDelay as an estimate of how long DNS propagation will take."; - ACME._notify(me, options, 'warning', err); - presenters['dns-01'].propagationDelay = 5000; - ACME._propagationDelayWarning = true; - } - } - Object.keys(presenters || {}).forEach(function (k) { - var ch = presenters[k]; - var warned = false; - - if (!ch.set || !ch.remove) { - throw new Error('challenge plugin must have set() and remove()'); - } - if (!ch.get) { - if ('dns-01' === k) { - console.warn('dns-01 challenge plugin should have get()'); - } else { - throw new Error( - 'http-01 and tls-alpn-01 challenge plugins must have get()' - ); - } - } - - if ('dns-01' === k) { - if (!ch.zones) { - console.warn('dns-01 challenge plugin should have zones()'); - } - } - - function warn() { - if (warned) { - return; - } - warned = true; - console.warn( - "'" + - k + - "' may have incorrect function signatures, or contains deprecated use of callbacks" - ); - } - - function promisify(fn) { - return function (opts) { - new Promise(function (resolve, reject) { - fn(opts, function (err, result) { - if (err) { - reject(err); - return; - } - resolve(result); - }); - }); - }; - } - - // init, zones, set, get, remove - if (ch.init && 2 === ch.init.length) { - warn(); - ch._thunk_init = ch.init; - ch.init = promisify(ch._thunk_init); - } - if (ch.zones && 2 === ch.zones.length) { - warn(); - ch._thunk_zones = ch.zones; - ch.zones = promisify(ch._thunk_zones); - } - if (2 === ch.set.length) { - warn(); - ch._thunk_set = ch.set; - ch.set = promisify(ch._thunk_set); - } - if (2 === ch.remove.length) { - warn(); - ch._thunk_remove = ch.remove; - ch.remove = promisify(ch._thunk_remove); - } - if (ch.get && 2 === ch.get.length) { - warn(); - ch._thunk_get = ch.get; - ch.get = promisify(ch._thunk_get); - } - - return ch; - }); -}; - -/* - POST /acme/new-order HTTP/1.1 - Host: example.com - Content-Type: application/jose+json - - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "5XJ1L3lEkMG7tR6pA00clA", - "url": "https://example.com/acme/new-order" - }), - "payload": base64url({ - "identifiers": [{"type:"dns","value":"example.com"}], - "notBefore": "2016-01-01T00:00:00Z", - "notAfter": "2016-01-08T00:00:00Z" - }), - "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" - } -*/ -ACME._getAuthorization = function (me, options, kid, zonenames, authUrl) { - //#console.debug('\n[DEBUG] getAuthorization\n'); - - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: authUrl, - protected: { kid: kid }, - payload: '' - }).then(function (resp) { - // Pre-emptive rather than lazy for interfaces that need to show the - // challenges to the user first - return ACME._computeAuths( - me, - options, - '', - resp.body, - zonenames, - false - ).then(function (auths) { - resp.body._rawChallenges = resp.body.challenges; - resp.body.challenges = auths; - return resp.body; - }); - }); -}; - -ACME._testChallengeOptions = function () { - // we want this to be the same for the whole group - var chToken = ACME._prnd(16); - return [ - { - type: 'http-01', - status: 'pending', - url: 'https://acme-staging-v02.example.com/0', - token: 'test-' + chToken + '-0' - }, - { - type: 'dns-01', - status: 'pending', - url: 'https://acme-staging-v02.example.com/1', - token: 'test-' + chToken + '-1', - _wildcard: true - }, - { - type: 'tls-alpn-01', - status: 'pending', - url: 'https://acme-staging-v02.example.com/3', - token: 'test-' + chToken + '-3' - } - ]; -}; - -ACME._thumber = function (options, thumb) { - var thumbPromise; - return function (key) { - if (thumb) { - return Promise.resolve(thumb); - } - if (thumbPromise) { - return thumbPromise; - } - if (!key) { - key = options.accountKey || options.accountKeypair; - } - thumbPromise = U._importKeypair(key).then(function (pair) { - return Keypairs.thumbprint({ - jwk: pair.public - }); - }); - return thumbPromise; - }; -}; - -ACME._dryRun = function (me, realOptions, zonenames) { - var noopts = {}; - Object.keys(realOptions).forEach(function (key) { - noopts[key] = realOptions[key]; - }); - noopts.order = {}; - - // memoized so that it doesn't run until it's first called - var getThumbprint = ACME._thumber(noopts, ''); - - return Promise.all( - noopts.domains.map(function (identifierValue) { - // TODO we really only need one to pass, not all to pass - var challenges = ACME._testChallengeOptions(); - var wild = '*.' === identifierValue.slice(0, 2); - if (wild) { - challenges = challenges.filter(function (ch) { - return ch._wildcard; - }); - } - challenges = challenges.filter(function (auth) { - return me._canCheck[auth.type]; - }); - - return getThumbprint().then(function (accountKeyThumb) { - var resp = { - body: { - identifier: { - type: 'dns', - value: identifierValue.replace(/^\*\./, '') - }, - challenges: challenges, - expires: new Date(Date.now() + 60 * 1000).toISOString(), - wildcard: identifierValue.includes('*.') || undefined - } - }; - - // The dry-run comes first in the spirit of "fail fast" - // (and protecting against challenge failure rate limits) - var dryrun = true; - return ACME._computeAuths( - me, - noopts, - accountKeyThumb, - resp.body, - zonenames, - dryrun - ).then(function (auths) { - resp.body.challenges = auths; - return resp.body; - }); - }); - }) - ).then(function (claims) { - var selected = []; - noopts.order._claims = claims.slice(0); - noopts.notify = function (ev, params) { - if ('_challenge_select' === ev) { - selected.push(params.challenge); - } - }; - - function clear() { - selected.forEach(function (ch) { - ACME._notify(me, noopts, 'challenge_remove', { - altname: ch.altname, - type: ch.type - //challenge: ch - }); - // ignore promise return - noopts.challenges[ch.type] - .remove({ challenge: ch }) - .catch(function (err) { - err.action = 'challenge_remove'; - err.altname = ch.altname; - err.type = ch.type; - ACME._notify(me, noopts, 'error', err); - }); - }); - } - - return ACME._setChallenges(me, noopts, noopts.order) - .catch(function (err) { - clear(); - throw err; - }) - .then(clear); - }); -}; - -// Get the list of challenge types we can validate, -// which is already ordered by preference. -// Select the first matching offered challenge type -ACME._chooseChallenge = function (options, results) { - // For each of the challenge types that we support - var challenge; - options._presenterTypes.some(function (chType) { - // And for each of the challenge types that are allowed - return results.challenges.some(function (ch) { - // Check to see if there are any matches - if (ch.type === chType) { - challenge = ch; - return true; - } - }); - }); - - return challenge; -}; - -ACME._getZones = function (me, challenges, domains) { - var presenter = challenges['dns-01']; - if (!presenter) { - return Promise.resolve([]); - } - if ('function' !== typeof presenter.zones) { - return Promise.resolve([]); - } - - // a little bit of random to ensure that getZones() - // actually returns the zones and not the hosts as zones - var dnsHosts = domains.map(function (d) { - var rnd = ACME._prnd(2); - return rnd + '.' + d; - }); - - var authChallenge = { - type: 'dns-01', - dnsHosts: dnsHosts - }; - - return presenter.zones({ challenge: authChallenge }); -}; - -ACME._challengesMap = { 'http-01': 0, 'dns-01': 0, 'tls-alpn-01': 0 }; -ACME._computeAuths = function (me, options, thumb, authz, zonenames, dryrun) { - // we don't poison the dns cache with our dummy request - var dnsPrefix = ACME.challengePrefixes['dns-01']; - if (dryrun) { - dnsPrefix = dnsPrefix.replace( - 'acme-challenge', - 'greenlock-dryrun-' + ACME._prnd(4) - ); - } - - var getThumbprint = ACME._thumber(options, thumb); - - return Promise.all( - authz.challenges.map(function (challenge) { - // Don't do extra work for challenges that we can't satisfy - var _types = options._presenterTypes; - if (_types && !_types.includes(challenge.type)) { - return null; - } - - var auth = {}; - - // straight copy from the new order response - // { identifier, status, expires, challenges, wildcard } - Object.keys(authz).forEach(function (key) { - auth[key] = authz[key]; - }); - - // copy from the challenge we've chosen - // { type, status, url, token } - // (note the duplicate status overwrites the one above, but they should be the same) - Object.keys(challenge).forEach(function (key) { - // don't confused devs with the id url - auth[key] = challenge[key]; - }); - - // batteries-included helpers - auth.hostname = auth.identifier.value; - // because I'm not 100% clear if the wildcard identifier does or doesn't - // have the leading *. in all cases - auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); - - var zone = pluckZone(zonenames || [], auth.identifier.value); - - return ACME.computeChallenge({ - accountKey: options.accountKey, - _getThumbprint: getThumbprint, - challenge: auth, - zone: zone, - dnsPrefix: dnsPrefix - }).then(function (resp) { - Object.keys(resp).forEach(function (k) { - auth[k] = resp[k]; - }); - return auth; - }); - }) - ).then(function (auths) { - return auths.filter(Boolean); - }); -}; - -ACME.computeChallenge = function (opts) { - var auth = opts.challenge; - var hostname = auth.hostname || opts.hostname; - var zone = opts.zone; - var thumb = opts.thumbprint || ''; - var accountKey = opts.accountKey; - var getThumbprint = opts._getThumbprint || ACME._thumber(opts, thumb); - var dnsPrefix = opts.dnsPrefix || ACME.challengePrefixes['dns-01']; - - return getThumbprint(accountKey).then(function (thumb) { - var resp = {}; - resp.thumbprint = thumb; - // keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey)) - resp.keyAuthorization = auth.token + '.' + thumb; - - if ('http-01' === auth.type) { - // conflicts with ACME challenge id url is already in use, - // so we call this challengeUrl instead - // TODO auth.http01Url ? - resp.challengeUrl = - 'http://' + - // `hostname` is an alias of `auth.indentifier.value` - hostname + - ACME.challengePrefixes['http-01'] + - '/' + - auth.token; - } - - if ('dns-01' !== auth.type) { - return resp; - } - - // Always calculate dnsAuthorization because we - // may need to present to the user for confirmation / instruction - // _as part of_ the decision making process - return sha2 - .sum(256, resp.keyAuthorization) - .then(function (hash) { - return Enc.bufToUrlBase64(Uint8Array.from(hash)); - }) - .then(function (hash64) { - resp.dnsHost = dnsPrefix + '.' + hostname; // .replace('*.', ''); - - // deprecated - resp.dnsAuthorization = hash64; - // should use this instead - resp.keyAuthorizationDigest = hash64; - - if (zone) { - resp.dnsZone = zone; - resp.dnsPrefix = resp.dnsHost - .replace(newZoneRegExp(zone), '') - .replace(/\.$/, ''); - } - - return resp; - }); - }); -}; - -ACME._untame = function (name, wild) { - if (wild) { - name = '*.' + name.replace('*.', ''); - } - return name; -}; - -// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 -ACME._postChallenge = function (me, options, kid, auth) { - var RETRY_INTERVAL = me.retryInterval || 1000; - var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; - var MAX_POLL = me.retryPoll || 8; - var MAX_PEND = me.retryPending || 4; - var count = 0; - - var altname = ACME._untame(auth.identifier.value, auth.wildcard); - - /* - POST /acme/authz/1234 HTTP/1.1 - Host: example.com - Content-Type: application/jose+json - - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "xWCM9lGbIyCgue8di6ueWQ", - "url": "https://example.com/acme/authz/1234" - }), - "payload": base64url({ - "status": "deactivated" - }), - "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" - } - */ - function deactivate() { - //#console.debug('[ACME.js] deactivate:'); - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: auth.url, - protected: { kid: kid }, - payload: Enc.strToBuf(JSON.stringify({ status: 'deactivated' })) - }).then(function (/*#resp*/) { - //#console.debug('deactivate challenge: resp.body:'); - //#console.debug(resp.body); - return ACME._wait(DEAUTH_INTERVAL); - }); - } - - function pollStatus() { - if (count >= MAX_POLL) { - var err = new Error( - "[ACME.js] stuck in bad pending/processing state for '" + - altname + - "'" - ); - err.context = 'present_challenge'; - return Promise.reject(err); - } - - count += 1; - - //#console.debug('\n[DEBUG] statusChallenge\n'); - // POST-as-GET - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: auth.url, - protected: { kid: kid }, - payload: Enc.binToBuf('') - }) - .then(checkResult) - .catch(transformError); - } - - function checkResult(resp) { - ACME._notify(me, options, 'challenge_status', { - // API-locked - status: resp.body.status, - type: auth.type, - altname: altname - }); - - if ('processing' === resp.body.status) { - //#console.debug('poll: again', auth.url); - return ACME._wait(RETRY_INTERVAL).then(pollStatus); - } - - // This state should never occur - if ('pending' === resp.body.status) { - if (count >= MAX_PEND) { - return ACME._wait(RETRY_INTERVAL) - .then(deactivate) - .then(respondToChallenge); - } - //#console.debug('poll: again', auth.url); - return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); - } - - // REMOVE DNS records as soon as the state is non-processing - // (valid or invalid or other) - try { - options.challenges[auth.type] - .remove({ challenge: auth }) - .catch(function (err) { - err.action = 'challenge_remove'; - err.altname = auth.altname; - err.type = auth.type; - ACME._notify(me, options, 'error', err); - }); - } catch (e) {} - - if ('valid' === resp.body.status) { - if (me.debug) { - console.debug('poll: valid'); - } - - return resp.body; - } - - var errmsg; - if (!resp.body.status) { - errmsg = - "[ACME.js] (E_STATE_EMPTY) empty challenge state for '" + - altname + - "':" + - JSON.stringify(resp.body); - } else if ('invalid' === resp.body.status) { - errmsg = - "[ACME.js] (E_STATE_INVALID) challenge state for '" + - altname + - "': '" + - //resp.body.status + - JSON.stringify(resp.body) + - "'"; - } else { - errmsg = - "[ACME.js] (E_STATE_UKN) challenge state for '" + - altname + - "': '" + - resp.body.status + - "'"; - } - - return Promise.reject(new Error(errmsg)); - } - - function transformError(e) { - var err = e; - if (err.urn) { - err = new Error( - '[acme-v2] ' + - auth.altname + - ' status:' + - e.status + - ' ' + - e.detail - ); - err.auth = auth; - err.altname = auth.altname; - err.type = auth.type; - err.code = - 'invalid' === e.status ? 'E_ACME_CHALLENGE' : 'E_ACME_UNKNOWN'; - } - - throw err; - } - - function respondToChallenge() { - //#console.debug('[ACME.js] responding to accept challenge:'); - // POST-as-POST (empty JSON object) - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: auth.url, - protected: { kid: kid }, - payload: Enc.strToBuf(JSON.stringify({})) - }) - .then(checkResult) - .catch(transformError); - } - - return respondToChallenge(); -}; - -// options = { domains, claims, challenges } -ACME._setChallenges = function (me, options, order) { - var claims = order._claims.slice(0); - var valids = []; - var auths = []; - var placed = []; - var USE_DNS = false; - var DNS_DELAY = 0; - - // Set any challenges, excpting ones that have already been validated - function setNext() { - var claim = claims.shift(); - // check false for testing - if (!claim || false === options.challenges) { - return Promise.resolve(); - } - - return Promise.resolve() - .then(function () { - // For any challenges that are already valid, - // add to the list and skip any checks. - if ( - claim.challenges.some(function (ch) { - if ('valid' === ch.status) { - valids.push(ch); - return true; - } - }) - ) { - return; - } - - var selected = ACME._chooseChallenge(options, claim); - if (!selected) { - throw E.NO_SUITABLE_CHALLENGE( - claim.altname, - claim.challenges, - options._presenterTypes - ); - } - auths.push(selected); - placed.push(selected); - ACME._notify(me, options, 'challenge_select', { - // API-locked - altname: ACME._untame( - claim.identifier.value, - claim.wildcard - ), - type: selected.type, - dnsHost: selected.dnsHost, - keyAuthorization: selected.keyAuthorization - }); - ACME._notify(me, options, '_challenge_select', { - altname: ACME._untame( - claim.identifier.value, - claim.wildcard - ), - type: selected.type, - challenge: selected - }); - - // Set a delay for nameservers a moment to propagate - if ('dns-01' === selected.type) { - if (options.challenges['dns-01'] && !USE_DNS) { - USE_DNS = true; - DNS_DELAY = parseInt( - options.challenges['dns-01'].propagationDelay, - 10 - ); - } - } - - var ch = options.challenges[selected.type] || {}; - if (!ch.set) { - throw new Error('no handler for setting challenge'); - } - return ch.set({ challenge: selected }); - }) - .then(setNext); - } - - function waitAll() { - //#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); - if (!DNS_DELAY || DNS_DELAY <= 0) { - DNS_DELAY = 5000; - } - return ACME._wait(DNS_DELAY); - } - - function checkNext() { - var auth = auths.shift(); - if (!auth) { - return Promise.resolve(valids); - } - - // These are not as much "valids" as they are "not invalids" - if (!me._canCheck[auth.type] || me.skipChallengeTest) { - valids.push(auth); - return checkNext(); - } - - return ACME.challengeTests[auth.type](me, { challenge: auth }) - .then(function () { - valids.push(auth); - }) - .then(checkNext); - } - - function removeAll(ch) { - options.challenges[ch.type] - .remove({ challenge: ch }) - .catch(function (err) { - err.action = 'challenge_remove'; - err.altname = ch.altname; - err.type = ch.type; - ACME._notify(me, options, 'error', err); - }); - } - - // The reason we set every challenge in a batch first before checking any - // is so that we don't poison our own DNS cache with misses. - return setNext() - .then(waitAll) - .then(checkNext) - .catch(function (err) { - if (!options.debug) { - placed.forEach(removeAll); - } - throw err; - }); -}; - -ACME._presentChallenges = function (me, options, kid, readyToPresent) { - // Actually sets the challenge via ACME - function challengeNext() { - // First set, First presented - var auth = readyToPresent.shift(); - if (!auth) { - return Promise.resolve(); - } - return ACME._postChallenge(me, options, kid, auth).then(challengeNext); - } - - // BTW, these are done serially rather than parallel on purpose - // (rate limits, propagation delays, etc) - return challengeNext().then(function () { - return readyToPresent; - }); -}; - -ACME._pollOrderStatus = function (me, options, kid, order, verifieds) { - var csr64 = ACME._csrToUrlBase64(options.csr); - var body = { csr: csr64 }; - var payload = JSON.stringify(body); - - function pollCert() { - //#console.debug('[ACME.js] pollCert:', order._finalizeUrl); - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: order._finalizeUrl, - protected: { kid: kid }, - payload: Enc.strToBuf(payload) - }).then(function (resp) { - ACME._notify(me, options, 'certificate_status', { - subject: options.domains[0], - status: resp.body.status - }); - - // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 - // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" - if ('valid' === resp.body.status) { - var voucher = resp.body; - voucher._certificateUrl = resp.body.certificate; - - return voucher; - } - - if ('processing' === resp.body.status) { - return ACME._wait().then(pollCert); - } - - if (me.debug) { - console.debug( - 'Error: bad status:\n' + JSON.stringify(resp.body, null, 2) - ); - } - - if ('pending' === resp.body.status) { - return Promise.reject( - new Error( - "Did not finalize order: status 'pending'." + - ' Best guess: You have not accepted at least one challenge for each domain:\n' + - "Requested: '" + - options.domains.join(', ') + - "'\n" + - "Validated: '" + - verifieds.join(', ') + - "'\n" + - JSON.stringify(resp.body, null, 2) - ) - ); - } - - if ('invalid' === resp.body.status) { - return Promise.reject( - E.ORDER_INVALID(options, verifieds, resp) - ); - } - - if ('ready' === resp.body.status) { - return Promise.reject( - E.DOUBLE_READY_ORDER(options, verifieds, resp) - ); - } - - return Promise.reject( - E.UNHANDLED_ORDER_STATUS(options, verifieds, resp) - ); - }); - } - - return pollCert(); -}; - -ACME._redeemCert = function (me, options, kid, voucher) { - //#console.debug('ACME.js: order was finalized'); - - // POST-as-GET - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: voucher._certificateUrl, - protected: { kid: kid }, - payload: Enc.binToBuf(''), - json: true - }).then(function (resp) { - //#console.debug('ACME.js: csr submitted and cert received:'); - - // https://github.com/certbot/certbot/issues/5721 - var certsarr = ACME.splitPemChain(ACME.formatPemChain(resp.body || '')); - // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ - var certs = { - expires: voucher.expires, - identifiers: voucher.identifiers, - //, authorizations: order.authorizations - cert: certsarr.shift(), - //, privkey: privkeyPem - chain: certsarr.join('\n') - }; - //#console.debug(certs); - return certs; - }); -}; - -ACME._finalizeOrder = function (me, options, kid, order) { - //#console.debug('[ACME.js] finalizeOrder:'); - var readyToPresent; - return A._getAccountKid(me, options).then(function (kid) { - return ACME._setChallenges(me, options, order) - .then(function (_readyToPresent) { - readyToPresent = _readyToPresent; - return ACME._presentChallenges( - me, - options, - kid, - readyToPresent - ); - }) - .then(function () { - return ACME._pollOrderStatus( - me, - options, - kid, - order, - readyToPresent.map(function (ch) { - return ACME._untame(ch.identifier.value, ch.wildcard); - }) - ); - }) - .then(function (voucher) { - return ACME._redeemCert(me, options, kid, voucher); - }); - }); -}; - -// Order a certificate request with all domains -ACME._orderCert = function (me, options, kid) { - var certificateRequest = { - // raw wildcard syntax MUST be used here - identifiers: options.domains.map(function (hostname) { - return { type: 'dns', value: hostname }; - }) - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" - }; - - return ACME._prepRequest(me, options) - .then(function () { - return ACME._getZones(me, options.challenges, options.domains); - }) - .then(function (zonenames) { - var p; - // Do a little dry-run / self-test - if (!me.skipDryRun && !options.skipDryRun) { - p = ACME._dryRun(me, options, zonenames); - } else { - p = Promise.resolve(null); - } - - return p.then(function () { - return A._getAccountKid(me, options) - .then(function (kid) { - ACME._notify(me, options, 'certificate_order', { - // API-locked - account: { key: { kid: kid } }, - subject: options.domains[0], - altnames: options.domains, - challengeTypes: options._presenterTypes - }); - - var payload = JSON.stringify(certificateRequest); - //#console.debug('\n[DEBUG] newOrder\n'); - return U._jwsRequest(me, { - accountKey: options.accountKey, - url: me._directoryUrls.newOrder, - protected: { kid: kid }, - payload: Enc.binToBuf(payload) - }); - }) - .then(function (resp) { - var order = resp.body; - order._orderUrl = resp.headers.location; - order._finalizeUrl = resp.body.finalize; - order._identifiers = certificateRequest.identifiers; - //#console.debug('[ordered]', location); // the account id url - //#console.debug(resp); - - if (!order.authorizations) { - return Promise.reject( - E.NO_AUTHORIZATIONS(options, resp) - ); - } - - return order; - }) - .then(function (order) { - return ACME._getAllChallenges( - me, - options, - kid, - zonenames, - order - ).then(function (claims) { - order._claims = claims; - return order; - }); - }); - }); - }); -}; - -ACME._prepRequest = function (me, options) { - return Promise.resolve().then(function () { - // TODO check that all presenterTypes are represented in challenges - if (!options._presenterTypes.length) { - return Promise.reject( - new Error('options.challenges must be specified') - ); - } - - if (!options.csr) { - throw new Error( - 'no `csr` option given (should be in DER or PEM format)' - ); - } - // TODO validate csr signature? - var _csr = CSR._info(options.csr); - options.domains = options.domains || _csr.altnames; - _csr.altnames = _csr.altnames || []; - if ( - options.domains.slice(0).sort().join(' ') !== - _csr.altnames.slice(0).sort().join(' ') - ) { - return Promise.reject( - new Error('certificate altnames do not match requested domains') - ); - } - if (_csr.subject !== options.domains[0]) { - return Promise.reject( - new Error( - 'certificate subject (commonName) does not match first altname (SAN)' - ) - ); - } - if (!(options.domains && options.domains.length)) { - return Promise.reject( - new Error( - 'options.domains must be a list of string domain names,' + - ' with the first being the subject of the certificate' - ) - ); - } - - // a cheap check to see if there are non-ascii characters in any of the domains - var nonAsciiDomains = options.domains.some(function (d) { - // IDN / unicode / utf-8 / punycode - return Enc.strToBin(d) !== d; - }); - if (nonAsciiDomains) { - throw new Error( - "please use the 'punycode' module to convert unicode domain names to punycode" - ); - } - - // TODO Promise.all()? - (options._presenterTypes || []).forEach(function (key) { - var presenter = options.challenges[key]; - if ( - 'function' === typeof presenter.init && - !presenter._acme_initialized - ) { - presenter._acme_initialized = true; - return presenter.init({ type: '*', request: me.request }); - } - }); - }); -}; - -// Request a challenge for each authorization in the order -ACME._getAllChallenges = function (me, options, kid, zonenames, order) { - var claims = []; - //#console.debug("[acme-v2] POST newOrder has authorizations"); - var challengeAuths = order.authorizations.slice(0); - - function getNext() { - var authUrl = challengeAuths.shift(); - if (!authUrl) { - return claims; - } - - return ACME._getAuthorization( - me, - options, - kid, - zonenames, - authUrl - ).then(function (claim) { - // var domain = options.domains[i]; // claim.identifier.value - claims.push(claim); - return getNext(); - }); - } - - return getNext().then(function () { - return claims; - }); -}; - -ACME.formatPemChain = function formatPemChain(str) { - return ( - str - .trim() - .replace(/[\r\n]+/g, '\n') - .replace(/\-\n\-/g, '-\n\n-') + '\n' - ); -}; - -ACME.splitPemChain = function splitPemChain(str) { - return str - .trim() - .split(/[\r\n]{2,}/g) - .map(function (str) { - return str + '\n'; - }); -}; - -ACME._csrToUrlBase64 = function (csr) { - // if der, convert to base64 - if ('string' !== typeof csr) { - csr = Enc.bufToUrlBase64(csr); - } - - // TODO use PEM.parseBlock() - // nix PEM headers, if any - if ('-' === csr[0]) { - csr = csr.split(/\n+/).slice(1, -1).join(''); - } - return Enc.base64ToUrlBase64(csr.trim().replace(/\s+/g, '')); -}; - -// In v8 this is crypto random, but we're just using it for pseudorandom -ACME._prnd = function (n) { - var rnd = ''; - while (rnd.length / 2 < n) { - var i = Math.random().toString().substr(2); - var h = parseInt(i, 10).toString(16); - if (h.length % 2) { - h = '0' + h; - } - rnd += h; - } - return rnd.substr(0, n * 2); -}; - -ACME._notify = function (me, options, ev, params) { - if (!options.notify && !me.notify) { - //console.info(ev, params); - return; - } - try { - (options.notify || me.notify)(ev, params); - } catch (e) { - console.error('`acme.notify(ev, params)` Error:'); - console.error(e); - } -}; - -ACME._wait = function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, ms || 1100); - }); -}; - -function newZoneRegExp(zonename) { - // (^|\.)example\.com$ - // which matches: - // foo.example.com - // example.com - // but not: - // fooexample.com - return new RegExp('(^|\\.)' + zonename.replace(/\./g, '\\.') + '$'); -} - -function pluckZone(zonenames, dnsHost) { - return zonenames - .filter(function (zonename) { - // the only character that needs to be escaped for regex - // and is allowed in a domain name is '.' - return newZoneRegExp(zonename).test(dnsHost); - }) - .sort(function (a, b) { - // longest match first - return b.length - a.length; - })[0]; -} diff --git a/bin/bundle.js b/bin/bundle.js deleted file mode 100644 index ac2b88e..0000000 --- a/bin/bundle.js +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env node -(async function () { - 'use strict'; - - var UglifyJS = require('uglify-js'); - var path = require('path'); - var fs = require('fs'); - var promisify = require('util').promisify; - var readFile = promisify(fs.readFile); - var writeFile = promisify(fs.writeFile); - var gzip = promisify(require('zlib').gzip); - - // The order is specific, and it matters - var files = await Promise.all( - [ - '../lib/encoding.js', - '../lib/asn1-packer.js', - '../lib/x509.js', - '../lib/ecdsa.js', - '../lib/rsa.js', - '../lib/keypairs.js', - '../lib/asn1-parser.js', - '../lib/csr.js', - '../lib/acme.js' - ].map(async function (file) { - return (await readFile(path.join(__dirname, file), 'utf8')).trim(); - }) - ); - - var header = - [ - '// Copyright 2015-2019 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/. */' - ].join('\n') + '\n'; - - var file = header + files.join('\n') + '\n'; - await writeFile(path.join(__dirname, '../dist', 'acme.js'), file); - await writeFile( - path.join(__dirname, '../dist', 'acme.js.gz'), - await gzip(file) - ); - - // TODO source maps? - var result = UglifyJS.minify(file, { - compress: true, - // mangling doesn't save significant - mangle: false - }); - if (result.error) { - throw result.error; - } - file = header + result.code; - await writeFile(path.join(__dirname, '../dist', 'acme.min.js'), file); - await writeFile( - path.join(__dirname, '../dist', 'acme.min.js.gz'), - await gzip(file) - ); -})(); diff --git a/compat.js b/compat.js new file mode 100644 index 0000000..9654475 --- /dev/null +++ b/compat.js @@ -0,0 +1,94 @@ +// 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 */ + +var ACME2 = require('./').ACME; + +function resolveFn(cb) { + return function(val) { + // nextTick to get out of Promise chain + process.nextTick(function() { + cb(null, val); + }); + }; +} +function rejectFn(cb) { + return function(err) { + console.error('[acme-v2] handled(?) rejection as errback:'); + console.error(err.stack); + + // nextTick to get out of Promise chain + process.nextTick(function() { + cb(err); + }); + + // do not resolve promise further + return new Promise(function() {}); + }; +} + +function create(deps) { + deps.LeCore = {}; + var acme2 = ACME2.create(deps); + acme2.registerNewAccount = function(options, cb) { + acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getCertificate = function(options, cb) { + options.agreeToTerms = + options.agreeToTerms || + function(tos) { + return Promise.resolve(tos); + }; + acme2.certificates.create(options).then(function(certs) { + var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); + certs.privkey = privkeyPem; + resolveFn(cb)(certs); + }, rejectFn(cb)); + }; + acme2.getAcmeUrls = function(options, cb) { + acme2.init(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getOptions = function() { + var defs = {}; + + Object.keys(module.exports.defaults).forEach(function(key) { + defs[key] = defs[deps] || module.exports.defaults[key]; + }); + + return defs; + }; + acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; + acme2.productionServerUrl = module.exports.defaults.productionServerUrl; + acme2.acmeChallengePrefix = module.exports.defaults.acmeChallengePrefix; + return acme2; +} + +module.exports.ACME = {}; +module.exports.defaults = { + productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory', + stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory', + knownEndpoints: [ + 'keyChange', + 'meta', + 'newAccount', + 'newNonce', + 'newOrder', + 'revokeCert' + ], + challengeTypes: ['http-01', 'dns-01'], + challengeType: 'http-01', + //, keyType: 'rsa' // ecdsa + //, keySize: 2048 // 256 + rsaKeySize: 2048, // 256 + acmeChallengePrefix: '/.well-known/acme-challenge/' +}; +Object.keys(module.exports.defaults).forEach(function(key) { + module.exports.ACME[key] = module.exports.defaults[key]; +}); +Object.keys(ACME2).forEach(function(key) { + module.exports.ACME[key] = ACME2[key]; +}); +module.exports.ACME.create = create; diff --git a/errors.js b/errors.js deleted file mode 100644 index 0437034..0000000 --- a/errors.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -var E = module.exports; - -E.NO_SUITABLE_CHALLENGE = function (domain, challenges, presenters) { - // Bail with a descriptive message if no usable challenge could be selected - // For example, wildcards require dns-01 and, if we don't have that, we have to bail - var enabled = presenters.join(', ') || 'none'; - var suitable = - challenges - .map(function (r) { - return r.type; - }) - .join(', ') || 'none'; - return new Error( - "None of the challenge types that you've enabled ( " + - enabled + - ' )' + - " are suitable for validating the domain you've selected (" + - domain + - ').' + - ' You must enable one of ( ' + - suitable + - ' ).' - ); -}; -E.UNHANDLED_ORDER_STATUS = function (options, domains, resp) { - return new Error( - "Didn't finalize order: Unhandled status '" + - resp.body.status + - "'." + - ' This is not one of the known statuses...\n' + - "Requested: '" + - options.domains.join(', ') + - "'\n" + - "Validated: '" + - domains.join(', ') + - "'\n" + - JSON.stringify(resp.body, null, 2) + - '\n\n' + - 'Please open an issue at https://git.rootprojects.org/root/acme.js' - ); -}; -E.DOUBLE_READY_ORDER = function (options, domains, resp) { - return new Error( - "Did not finalize order: status 'ready'." + - " Hmmm... this state shouldn't be possible here. That was the last state." + - " This one should at least be 'processing'.\n" + - "Requested: '" + - options.domains.join(', ') + - "'\n" + - "Validated: '" + - domains.join(', ') + - "'\n" + - JSON.stringify(resp.body, null, 2) + - '\n\n' + - 'Please open an issue at https://git.rootprojects.org/root/acme.js' - ); -}; -E.ORDER_INVALID = function (options, domains, resp) { - return new Error( - "Did not finalize order: status 'invalid'." + - ' Best guess: One or more of the domain challenges could not be verified' + - ' (or the order was canceled).\n' + - "Requested: '" + - options.domains.join(', ') + - "'\n" + - "Validated: '" + - domains.join(', ') + - "'\n" + - JSON.stringify(resp.body, null, 2) - ); -}; -E.NO_AUTHORIZATIONS = function (options, resp) { - return new Error( - "[acme-v2.js] authorizations were not fetched for '" + - options.domains.join() + - "':\n" + - JSON.stringify(resp.body) - ); -}; diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 4bed0fc..0000000 --- a/examples/README.md +++ /dev/null @@ -1,317 +0,0 @@ -# Example [ACME.js](https://git.rootprojects.org/root/acme.js) Usage - -| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub) - -ACME.js is a _low-level_ client for Let's Encrypt. - -Looking for an **easy**, _high-level_ client? Check out [Greenlock.js](https://git.rootprojects.org/root/greenlock.js). - -# Overview - -A basic example includes the following: - -1. Initialization - - maintainer contact - - package user-agent - - log events -2. Discover API - - retrieves Terms of Service and API endpoints -3. Get Subscriber Account - - create an ECDSA (or RSA) Account key in JWK format - - agree to terms - - register account by the key -4. Prepare a Certificate Signing Request - - create a RSA (or ECDSA) Server key in PEM format - - select domains (as punycode) - - choose challenges - - sign CSR - - order certificate - -# Code - -The tested-working code for this is in [examples/get-certificate-full.js](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/get-certificate-full.js) - -# Walkthrough - -Whereas [Greenlock.js](https://git.rootprojects.org/root/greenlock.js) is very much "batteries included", -the goal of ACME.js is to be lightweight and over more control. - -## 1. Create an `acme` instance - -The maintainer contact is used by Root to notify you of security notices and -bugfixes to ACME.js. - -The subscriber contact is used by Let's Encrypt to manage your account and -notify you of renewal failures. In the future we plan to enable some of that, -but allowing for your own branding. - -The customer email is provided as an example of what NOT to use as either of the other two. -Typically your customers are NOT directly Let's Encrypt subscribers. - -```js -// In many cases all three of these are the same (your email) -// However, this is what they may look like when different: - -var maintainerEmail = 'security@devshop.com'; -var subscriberEmail = 'support@hostingcompany.com'; -var customerEmail = 'jane.doe@gmail.com'; -``` - -The ACME spec requires clients to have RFC 7231 style User Agent. -This will be contstructed automatically using your package name. - -```js -var pkg = require('../package.json'); -var packageAgent = 'test-' + pkg.name + '/' + pkg.version; -``` - -Set up your logging facility. It's fine to ignore the logs, -but you'll probably want to log `warning` and `error` at least. - -```js -// This is intended to get at important messages without -// having to use even lower-level APIs in the code - -function notify(ev, msg) { - if ('error' === ev || 'warning' === ev) { - errors.push(ev.toUpperCase() + ' ' + msg.message); - return; - } - // be brief on all others - console.log(ev, msg.altname || '', msg.status || '''); -} -``` - -```js -var ACME = require('acme'); -var acme = ACME.create({ maintainerEmail, packageAgent, notify }); -``` - -## 2. Fetch the API Directory - -ACME defines an API discovery mechanism. - -For Let's Encrypt specifically, these are the _production_ and _staging_ URLs: - -```js -// Choose either the production or staging URL - -var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; -//var directoryUrl = 'https://acme-v02.api.letsencrypt.org/directory' -``` - -The init function will fetch the API and set internal urls and such accordingly. - -```js -await acme.init(directoryUrl); -``` - -## 3. Create (or import) an Account Keypair - -You must create a Subscriber Account using a public/private keypair. - -The Account key MUST be different from the server key. - -Keypairs.js will use native node crypto or WebCrypto to generate the key, and a lightweight parser and packer to translate between formats. - -```js -var Keypairs = require('@root/keypairs'); -``` - -Unless you're multi-tenanted, you only ever need ONE account key. Save it. - -```js -// You only need ONE account key, ever, in most cases -// save this and keep it safe. ECDSA is preferred. - -var accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' }); -var accountKey = accountKeypair.private; -``` - -If you already have a key you would like to use, you can import it (as shown in the server key section below). - -## 4. Create an ACME Subscriber Account - -In order to use Let's Encrypt and ACME.js, you must agree to the respective Subscriber Agreement and Terms. - -```js -// This can be `true` or an async function which presents the terms of use - -var agreeToTerms = true; - -// If you are multi-tenanted or white-labled and need to present the terms of -// use to the Subscriber running the service, you can do so with a function. - -var agreeToTerms = async function() { - return true; -}; -``` - -You create an account with a signed JWS message including your public key, which ACME.js handles for you with your account key. - -All messages must be signed with your account key. - -```js -console.info('registering new ACME account...'); - -var account = await acme.accounts.create({ - subscriberEmail, - agreeToTerms, - accountKey -}); -console.info('created account with id', account.key.kid); -``` - -## 5. Create (or import) a Server Keypair - -You must have a SERVER keypair, which is different from your account keypair. - -This isn't part of the ACME protocol, but rather something your Web Server uses and which you must use to sign the request for an SSL certificate, the same as with paid issuers in the days of yore. - -In many situations you only ever need ONE of these. - -```js -// This is the key used by your WEBSERVER, typically named `privkey.pem`, -// `key.crt`, or `bundle.pem`. RSA may be preferrable for legacy compatibility. - -// You can generate it fresh -var serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' }); -var serverKey = serverKeypair.private; -var serverPem = await Keypairs.export({ jwk: serverKey }); -await fs.promises.writeFile('./privkey.pem', serverPem, 'ascii'); - -// Or you can load it from a file -var serverPem = await fs.promises.readFile('./privkey.pem', 'ascii'); -console.info('wrote ./privkey.pem'); - -var serverKey = await Keypairs.import({ pem: serverPem }); -``` - -## 6. Create a Signed Certificate Request (CSR) - -Your domains must be `punycode`-encoded: - -```js -var punycode = require('punycode'); - -var domains = ['example.com', '*.example.com', '你好.example.com']; -domains = domains.map(function(name) { - return punycode.toASCII(name); -}); -``` - -```js -var CSR = require('@root/csr'); -var PEM = require('@root/pem'); - -var encoding = 'der'; -var typ = 'CERTIFICATE REQUEST'; - -var csrDer = await CSR.csr({ jwk: serverKey, domains, encoding }); -var csr = PEM.packBlock({ type: typ, bytes: csrDer }); -``` - -## 7. Choose Domain Validation Strategies - -You can use one of the existing http-01 or dns-01 plugins, or you can build your own. - -There's a test suite that makes this very easy to do: - -- [acme-dns-01-test](https://git.rootprojects.org/root/acme-dns-01-test.js) -- [acme-http-01-test](https://git.rootprojects.org/root/acme-http-01-test.js) - -```js -// You can pick from existing challenge modules -// which integrate with a variety of popular services -// or you can create your own. -// -// The order of priority will be http-01, tls-alpn-01, dns-01 -// dns-01 will always be used for wildcards -// dns-01 should be the only option given for local/private domains - -var webroot = require('acme-http-01-webroot').create({}); -var challenges = { - 'http-01': webroot, - 'dns-01': { - init: async function(deps) { - // includes the http request object to use - }, - zones: async function(args) { - // return a list of zones - }, - set: async function(args) { - // set a TXT record with the lowest allowable TTL - }, - get: async function(args) { - // check the TXT record exists - }, - remove: async function(args) { - // remove the TXT record - }, - // how long to wait after *all* TXTs are set - // before presenting them for validation - // (for most this is seconds, for some it may be minutes) - propagationDelay: 5000 - } -}; -``` - -## 8. Verify Domains & Get an SSL Certificate - -```js -console.info('validating domain authorization for ' + domains.join(' ')); -var pems = await acme.certificates.create({ - account, - accountKey, - csr, - domains, - challenges -}); -``` - -## 9. Save the Certificate - -```js -var fullchain = pems.cert + '\n' + pems.chain + '\n'; - -await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii'); -console.info('wrote ./fullchain.pem'); -``` - -## 10. Test Drive Your Cert - -```js -'use strict'; - -var https = require('http2'); -var fs = require('fs'); - -var key = fs.readFileSync('./privkey.pem'); -var cert = fs.readFileSync('./fullchain.pem'); - -var server = https.createSecureServer({ key, cert }, function(req, res) { - res.end('Hello, Encrypted World!'); -}); - -server.listen(443, function() { - console.info('Listening on', server.address()); -}); -``` - -Note: You can allow non-root `node` processes to bind to port 443 using `setcap`: - -```bash -sudo setcap 'cap_net_bind_service=+ep' $(which node) -``` - -You can also set your domain to localhost by editing your `/etc/hosts`: - -`/etc/hosts`: - -```txt -127.0.0.1 test.example.com - -127.0.0.1 localhost -255.255.255.255 broadcasthost -::1 localhost -``` diff --git a/examples/app.js b/examples/app.js deleted file mode 100644 index 2389569..0000000 --- a/examples/app.js +++ /dev/null @@ -1,340 +0,0 @@ -/*global Promise*/ -(function () { - 'use strict'; - - var Keypairs = require('@root/keypairs'); - var Rasha = require('@root/acme/rsa'); - var Eckles = require('@root/acme/ecdsa'); - var x509 = require('@root/acme/x509'); - var CSR = require('@root/csr'); - var ACME = require('@root/acme'); - var accountStuff = {}; - - function $(sel) { - return document.querySelector(sel); - } - function $$(sel) { - return Array.prototype.slice.call(document.querySelectorAll(sel)); - } - - function checkTos(tos) { - if ($('input[name="tos"]:checked')) { - return tos; - } else { - return ''; - } - } - - function run() { - console.log('hello'); - - // Show different options for ECDSA vs RSA - $$('input[name="kty"]').forEach(function ($el) { - $el.addEventListener('change', function (ev) { - console.log(this); - console.log(ev); - if ('RSA' === ev.target.value) { - $('.js-rsa-opts').hidden = false; - $('.js-ec-opts').hidden = true; - } else { - $('.js-rsa-opts').hidden = true; - $('.js-ec-opts').hidden = false; - } - }); - }); - - // Generate a key on submit - $('form.js-keygen').addEventListener('submit', function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - $('.js-loading').hidden = false; - $('.js-jwk').hidden = true; - $('.js-toc-der-public').hidden = true; - $('.js-toc-der-private').hidden = true; - $$('.js-toc-pem').forEach(function ($el) { - $el.hidden = true; - }); - $$('input').map(function ($el) { - $el.disabled = true; - }); - $$('button').map(function ($el) { - $el.disabled = true; - }); - var opts = { - kty: $('input[name="kty"]:checked').value, - namedCurve: $('input[name="ec-crv"]:checked').value, - modulusLength: $('input[name="rsa-len"]:checked').value - }; - var then = Date.now(); - console.log('opts', opts); - Keypairs.generate(opts).then(function (results) { - console.log('Key generation time:', Date.now() - then + 'ms'); - var pubDer; - var privDer; - if (/EC/i.test(opts.kty)) { - privDer = x509.packPkcs8(results.private); - pubDer = x509.packSpki(results.public); - Eckles.export({ - jwk: results.private, - format: 'sec1' - }).then(function (pem) { - $('.js-input-pem-sec1-private').innerText = pem; - $('.js-toc-pem-sec1-private').hidden = false; - }); - Eckles.export({ - jwk: results.private, - format: 'pkcs8' - }).then(function (pem) { - $('.js-input-pem-pkcs8-private').innerText = pem; - $('.js-toc-pem-pkcs8-private').hidden = false; - }); - Eckles.export({ jwk: results.public, public: true }).then( - function (pem) { - $('.js-input-pem-spki-public').innerText = pem; - $('.js-toc-pem-spki-public').hidden = false; - } - ); - } else { - privDer = x509.packPkcs8(results.private); - pubDer = x509.packSpki(results.public); - Rasha.export({ - jwk: results.private, - format: 'pkcs1' - }).then(function (pem) { - $('.js-input-pem-pkcs1-private').innerText = pem; - $('.js-toc-pem-pkcs1-private').hidden = false; - }); - Rasha.export({ - jwk: results.private, - format: 'pkcs8' - }).then(function (pem) { - $('.js-input-pem-pkcs8-private').innerText = pem; - $('.js-toc-pem-pkcs8-private').hidden = false; - }); - Rasha.export({ jwk: results.public, format: 'pkcs1' }).then( - function (pem) { - $('.js-input-pem-pkcs1-public').innerText = pem; - $('.js-toc-pem-pkcs1-public').hidden = false; - } - ); - Rasha.export({ jwk: results.public, format: 'spki' }).then( - function (pem) { - $('.js-input-pem-spki-public').innerText = pem; - $('.js-toc-pem-spki-public').hidden = false; - } - ); - } - - $('.js-der-public').innerText = pubDer; - $('.js-toc-der-public').hidden = false; - $('.js-der-private').innerText = privDer; - $('.js-toc-der-private').hidden = false; - $('.js-jwk').innerText = JSON.stringify(results, null, 2); - $('.js-loading').hidden = true; - $('.js-jwk').hidden = false; - $$('input').map(function ($el) { - $el.disabled = false; - }); - $$('button').map(function ($el) { - $el.disabled = false; - }); - $('.js-toc-jwk').hidden = false; - - $('.js-create-account').hidden = false; - $('.js-create-csr').hidden = false; - }); - }); - - $('form.js-acme-account').addEventListener('submit', function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - $('.js-loading').hidden = false; - var acme = ACME.create({ - Keypairs: Keypairs, - CSR: CSR - }); - acme.init( - 'https://acme-staging-v02.api.letsencrypt.org/directory' - ).then(function (result) { - console.log('acme result', result); - var privJwk = JSON.parse($('.js-jwk').innerText).private; - var email = $('.js-email').value; - return acme.accounts - .create({ - email: email, - agreeToTerms: checkTos, - accountKeypair: { privateKeyJwk: privJwk } - }) - .then(function (account) { - console.log('account created result:', account); - accountStuff.account = account; - accountStuff.privateJwk = privJwk; - accountStuff.email = email; - accountStuff.acme = acme; - $('.js-create-order').hidden = false; - $('.js-toc-acme-account-response').hidden = false; - $( - '.js-acme-account-response' - ).innerText = JSON.stringify(account, null, 2); - }) - .catch(function (err) { - console.error('A bad thing happened:'); - console.error(err); - window.alert( - err.message || JSON.stringify(err, null, 2) - ); - }); - }); - }); - - $('form.js-csr').addEventListener('submit', function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - generateCsr(); - }); - - $('form.js-acme-order').addEventListener('submit', function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - var account = accountStuff.account; - var privJwk = accountStuff.privateJwk; - var email = accountStuff.email; - var acme = accountStuff.acme; - - var domains = ($('.js-domains').value || 'example.com').split( - /[, ]+/g - ); - return getDomainPrivkey().then(function (domainPrivJwk) { - console.log('Has CSR already?'); - console.log(accountStuff.csr); - return acme.certificates - .create({ - accountKeypair: { privateKeyJwk: privJwk }, - account: account, - serverKeypair: { privateKeyJwk: domainPrivJwk }, - csr: accountStuff.csr, - domains: domains, - skipDryRun: - $('input[name="skip-dryrun"]:checked') && true, - agreeToTerms: checkTos, - challenges: { - 'dns-01': { - set: function (opts) { - console.info('dns-01 set challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while ( - !window.confirm( - 'Did you set the challenge?' - ) - ) {} - resolve(); - }); - }, - remove: function (opts) { - console.log('dns-01 remove challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while ( - !window.confirm( - 'Did you delete the challenge?' - ) - ) {} - resolve(); - }); - } - }, - 'http-01': { - set: function (opts) { - console.info('http-01 set challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while ( - !window.confirm( - 'Did you set the challenge?' - ) - ) {} - resolve(); - }); - }, - remove: function (opts) { - console.log('http-01 remove challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while ( - !window.confirm( - 'Did you delete the challenge?' - ) - ) {} - resolve(); - }); - } - } - }, - challengeTypes: [ - $('input[name="acme-challenge-type"]:checked').value - ] - }) - .then(function (results) { - console.log('Got Certificates:'); - console.log(results); - $('.js-toc-acme-order-response').hidden = false; - $('.js-acme-order-response').innerText = JSON.stringify( - results, - null, - 2 - ); - }) - .catch(function (err) { - console.error('challenge failed:'); - console.error(err); - window.alert( - 'failed! ' + err.message || JSON.stringify(err) - ); - }); - }); - }); - - $('.js-generate').hidden = false; - } - - function getDomainPrivkey() { - if (accountStuff.domainPrivateJwk) { - return Promise.resolve(accountStuff.domainPrivateJwk); - } - return Keypairs.generate({ - kty: $('input[name="kty"]:checked').value, - namedCurve: $('input[name="ec-crv"]:checked').value, - modulusLength: $('input[name="rsa-len"]:checked').value - }).then(function (pair) { - console.log('domain keypair:', pair); - accountStuff.domainPrivateJwk = pair.private; - return pair.private; - }); - } - - function generateCsr() { - var domains = ($('.js-domains').value || 'example.com').split(/[, ]+/g); - //var privJwk = JSON.parse($('.js-jwk').innerText).private; - return getDomainPrivkey().then(function (privJwk) { - accountStuff.domainPrivateJwk = privJwk; - return CSR({ jwk: privJwk, domains: domains }).then(function (pem) { - // Verify with https://www.sslshopper.com/csr-decoder.html - accountStuff.csr = pem; - console.log('Created CSR:'); - console.log(pem); - - console.log('CSR info:'); - console.log(CSR._info(pem)); - - return pem; - }); - }); - } - - window.addEventListener('load', run); -})(); diff --git a/examples/cli.js b/examples/cli.js new file mode 100644 index 0000000..e453439 --- /dev/null +++ b/examples/cli.js @@ -0,0 +1,131 @@ +// 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'; + +var RSA = require('rsa-compat').RSA; +var readline = require('readline'); +var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +require('./genkeypair.js'); + +function getWeb() { + rl.question( + 'What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', + function(web) { + web = (web || '').trim().split(/,/g); + if (!web[0]) { + getWeb(); + return; + } + + if ( + web.some(function(w) { + return '*' === w[0]; + }) + ) { + console.log('Wildcard domains must use dns-01'); + getEmail(web, 'dns-01'); + } else { + getChallengeType(web); + } + } + ); +} + +function getChallengeType(web) { + rl.question( + 'What challenge will you be testing today? http-01 or dns-01? [http-01] ', + function(chType) { + chType = (chType || '').trim(); + if (!chType) { + chType = 'http-01'; + } + + getEmail(web, chType); + } + ); +} + +function getEmail(web, chType) { + rl.question('What email should we use? (optional) ', function(email) { + email = (email || '').trim(); + if (!email) { + email = null; + } + + getApiStyle(web, chType, email); + }); +} + +function getApiStyle(web, chType, email) { + var defaultStyle = 'compat'; + rl.question( + 'What API style would you like to test? v1-compat or promise? [v1-compat] ', + function(apiStyle) { + apiStyle = (apiStyle || '').trim(); + if (!apiStyle) { + apiStyle = 'v1-compat'; + } + + rl.close(); + + var RSA = require('rsa-compat').RSA; + var accountKeypair = RSA.import({ + privateKeyPem: require('fs').readFileSync( + __dirname + '/../tests/account.privkey.pem' + ) + }); + var domainKeypair = RSA.import({ + privateKeyPem: require('fs').readFileSync( + __dirname + '/../tests/privkey.pem' + ) + }); + var directoryUrl = + 'https://acme-staging-v02.api.letsencrypt.org/directory'; + + if ('promise' === apiStyle) { + require('../tests/promise.js').run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair + ); + } else if ('cb' === apiStyle) { + require('../tests/cb.js').run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair + ); + } else { + if ('v1-compat' !== apiStyle) { + console.warn( + "Didn't understand '" + apiStyle + "', using 'v1-compat' instead..." + ); + } + require('../tests/compat.js').run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair + ); + } + } + ); +} + +getWeb(); diff --git a/examples/dns-01-digitalocean.js b/examples/dns-01-digitalocean.js new file mode 100644 index 0000000..02112ce --- /dev/null +++ b/examples/dns-01-digitalocean.js @@ -0,0 +1,69 @@ +(function(exports) { + 'use strict'; + + // node[0] ./test.js[1] jon.doe@gmail.com[2] example.com,*.example.com[3] xxxxxx[4] + var email = process.argv[2] || process.env.ACME_EMAIL; + var domains = (process.argv[3] || process.env.ACME_DOMAINS).split(/[,\s]+/); + var token = process.argv[4] || process.env.DIGITALOCEAN_API_KEY; + + // git clone https://git.rootprojects.org/root/acme-dns-01-digitalocean.js node_modules/acme-dns-01-digitalocean + var dns01 = require('acme-dns-01-digitalocean').create({ + //baseUrl: 'https://api.digitalocean.com/v2/domains', + token: token + }); + + // This will be replaced with Keypairs.js in the next version + var promisify = require('util').promisify; + var generateKeypair = promisify(require('rsa-compat').RSA.generateKeypair); + + //var ACME = exports.ACME || require('acme').ACME; + var ACME = exports.ACME || require('../').ACME; + var acme = ACME.create({}); + acme + .init({ + //directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' + }) + .then(function() { + return generateKeypair(null).then(function(accountPair) { + return generateKeypair(null).then(function(serverPair) { + return acme.accounts + .create({ + // valid email (server checks MX records) + email: email, + accountKeypair: accountPair, + agreeToTerms: function(tosUrl) { + // ask user (if user is the host) + return tosUrl; + } + }) + .then(function(account) { + console.info('Created Account:'); + console.info(account); + + return acme.certificates + .create({ + domains: domains, + challenges: { 'dns-01': dns01 }, + domainKeypair: serverPair, + accountKeypair: accountPair, + + // v2 will be directly compatible with the new ACME modules, + // whereas this version needs a shim + getZones: dns01.zones, + setChallenge: dns01.set, + removeChallenge: dns01.remove + }) + .then(function(certs) { + console.info('Secured SSL Certificates'); + console.info(certs); + }); + }); + }); + }); + }) + .catch(function(e) { + console.error('Something went wrong:'); + console.error(e); + process.exit(500); + }); +})('undefined' === typeof module ? window : module.exports); diff --git a/examples/example.env b/examples/example.env index 68796b3..2df1d9a 100644 --- a/examples/example.env +++ b/examples/example.env @@ -1,13 +1,3 @@ -ENV=DEV - -MAINTAINER_EMAIL=letsencrypt+staging@example.com -SUBSCRIBER_EMAIL=letsencrypt+staging@example.com - -# for example -DOMAINS=test.example.com,www.test.example.com -# for tests -BASE_DOMAIN=test.example.com - -CHALLENGE_TYPE=dns-01 -CHALLENGE_PLUGIN=acme-dns-01-digitalocean -CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}' +ACME_EMAIL=jon.doe@gmail.com +ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com +DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/examples/genkeypair.js b/examples/genkeypair.js new file mode 100644 index 0000000..c40e187 --- /dev/null +++ b/examples/genkeypair.js @@ -0,0 +1,26 @@ +// 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/. */ +var RSA = require('rsa-compat').RSA; +var fs = require('fs'); + +if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function(err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair); + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem); + }); +} + +if (!fs.existsSync(__dirname + '/../tests/privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function(err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair); + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/privkey.pem', privkeyPem); + }); +} diff --git a/examples/get-certificate-full.js b/examples/get-certificate-full.js deleted file mode 100644 index 0d66d14..0000000 --- a/examples/get-certificate-full.js +++ /dev/null @@ -1,151 +0,0 @@ -async function main() { - 'use strict'; - - require('dotenv').config(); - - var fs = require('fs'); - // just to trigger the warning message out of the way - await fs.promises.readFile().catch(function () {}); - console.warn('\n'); - var MY_DOMAINS = process.env.DOMAINS.split(/[,\s]+/); - - // In many cases all three of these are the same (your email) - // However, this is what they may look like when different: - - var maintainerEmail = process.env.MAINTAINER_EMAIL; - var subscriberEmail = process.env.SUBSCRIBER_EMAIL; - //var customerEmail = 'jane.doe@gmail.com'; - - var pkg = require('../package.json'); - var packageAgent = 'test-' + pkg.name + '/' + pkg.version; - - // Choose either the production or staging URL - - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - //var directoryUrl = 'https://acme-v02.api.letsencrypt.org/directory' - - // This is intended to get at important messages without - // having to use even lower-level APIs in the code - - var errors = []; - function notify(ev, msg) { - if ('error' === ev || 'warning' === ev) { - errors.push(ev.toUpperCase() + ' ' + msg.message); - return; - } - // ignore all for now - console.log(ev, msg.altname || '', msg.status || ''); - } - - var Keypairs = require('@root/keypairs'); - - var ACME = require('../'); - var acme = ACME.create({ maintainerEmail, packageAgent, notify }); - await acme.init(directoryUrl); - - // You only need ONE account key, ever, in most cases - // save this and keep it safe. ECDSA is preferred. - - var accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' }); - var accountKey = accountKeypair.private; - - // This can be `true` or an async function which presents the terms of use - - var agreeToTerms = true; - - // If you are multi-tenanted or white-labled and need to present the terms of - // use to the Subscriber running the service, you can do so with a function. - var agreeToTerms = async function () { - return true; - }; - - console.info('registering new ACME account...'); - var account = await acme.accounts.create({ - subscriberEmail, - agreeToTerms, - accountKey - }); - console.info('created account with id', account.key.kid); - - // This is the key used by your WEBSERVER, typically named `privkey.pem`, - // `key.crt`, or `bundle.pem`. RSA may be preferrable for legacy compatibility. - - // You can generate it fresh - var serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' }); - var serverKey = serverKeypair.private; - var serverPem = await Keypairs.export({ jwk: serverKey }); - await fs.promises.writeFile('./privkey.pem', serverPem, 'ascii'); - console.info('wrote ./privkey.pem'); - - // Or you can load it from a file - var serverPem = await fs.promises.readFile('./privkey.pem', 'ascii'); - - var serverKey = await Keypairs.import({ pem: serverPem }); - - var CSR = require('@root/csr'); - var PEM = require('@root/pem'); - var Enc = require('@root/encoding/base64'); - - var encoding = 'der'; - var typ = 'CERTIFICATE REQUEST'; - - var domains = MY_DOMAINS; - var csrDer = await CSR.csr({ jwk: serverKey, domains, encoding }); - //var csr64 = Enc.bufToBase64(csrDer); - var csr = PEM.packBlock({ type: typ, bytes: csrDer }); - - // You can pick from existing challenge modules - // which integrate with a variety of popular services - // or you can create your own. - // - // The order of priority will be http-01, tls-alpn-01, dns-01 - // dns-01 will always be used for wildcards - // dns-01 should be the only option given for local/private domains - - var challenges = { - 'dns-01': loadDns01() - }; - - console.info('validating domain authorization for ' + domains.join(' ')); - var pems = await acme.certificates.create({ - account, - accountKey, - csr, - domains, - challenges - }); - var fullchain = pems.cert + '\n' + pems.chain + '\n'; - - await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii'); - console.info('wrote ./fullchain.pem'); - if (errors.length) { - console.warn(); - console.warn('[Warning]'); - console.warn('The following warnings and/or errors were encountered:'); - console.warn(errors.join('\n')); - } -} - -main().catch(function (e) { - console.error(e.stack); -}); - -function loadDns01() { - var pluginName = process.env.CHALLENGE_PLUGIN; - var pluginOptions = process.env.CHALLENGE_OPTIONS; - var plugin; - if (!pluginOptions) { - console.error( - 'Please create a .env in the format of examples/example.env to run the tests' - ); - process.exit(1); - } - try { - plugin = require(pluginName); - } catch (err) { - console.error("Couldn't find '" + pluginName + "'. Is it installed?"); - console.error("\tnpm install --save-dev '" + pluginName + "'"); - process.exit(1); - } - return plugin.create(JSON.parse(pluginOptions)); -} diff --git a/examples/http-server.js b/examples/http-server.js new file mode 100644 index 0000000..26b6cab --- /dev/null +++ b/examples/http-server.js @@ -0,0 +1,13 @@ +// 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'; + +var http = require('http'); +var express = require('express'); +var server = http + .createServer(express.static('../tests')) + .listen(80, function() { + console.log('Listening on', this.address()); + }); diff --git a/examples/https-server.js b/examples/https-server.js index e7b20bd..4369ebb 100644 --- a/examples/https-server.js +++ b/examples/https-server.js @@ -1,15 +1,20 @@ +// 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'; -var https = require('http2'); -var fs = require('fs'); - -var key = fs.readFileSync('./privkey.pem'); -var cert = fs.readFileSync('./fullchain.pem'); - +var https = require('https'); var server = https - .createSecureServer({ key, cert }, function (req, res) { - res.end('Hello, Encrypted World!'); - }) - .listen(443, function () { - console.info('Listening on', server.address()); + .createServer( + { + key: require('fs').readFileSync('../tests/privkey.pem'), + cert: require('fs').readFileSync('../tests/fullchain.pem') + }, + function(req, res) { + res.end('Hello, World!'); + } + ) + .listen(443, function() { + console.log('Listening on', this.address()); }); diff --git a/examples/https-sni-server.js b/examples/https-sni-server.js deleted file mode 100644 index 20f02ad..0000000 --- a/examples/https-sni-server.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -var https = require('http2'); -var tls = require('tls'); -var fs = require('fs'); - -var key = fs.readFileSync('./privkey.pem'); -var cert = fs.readFileSync('./fullchain.pem'); - -function SNICallback(servername, cb) { - console.log('sni:', servername); - cb(null, tls.createSecureContext({ key, cert })); -} - -var server = https - .createSecureServer({ SNICallback: SNICallback }, function (req, res) { - res.end('Hello, Encrypted World!'); - }) - .listen(443, function () { - console.info('Listening on', server.address()); - }); diff --git a/examples/index.html b/examples/index.html deleted file mode 100644 index 254382f..0000000 --- a/examples/index.html +++ /dev/null @@ -1,228 +0,0 @@ - - - ACME.js - A Root Project - - - - -

- @root/acme: Let's Encrypt for the Browser -

- -

- This is intended to be explored with your JavaScript console open. -

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

1. Keypair Generation

-
-

Key Type:

-
- - - - -
-
-

EC Options:

- - - -
- - -
- -

2. ACME Account

- - -

3. (optional) Certificate Signing Request

-
- - -
- -
- -

4. ACME Certificate Order

-
- Challenge type: - - -
- -
- -
- - - - - - - - - - - - - - -
-

- [Root](https://rootprojects.org) has built a collection of - lightweight, zero-dependency, libraries written in VanillaJS. They - are fast, tiny, and secure, using the native features of modern - browsers where possible. This means it's easy-to-use crypto in - kilobytes, not megabytes. -

-
- - - - - - - diff --git a/examples/server.js b/examples/server.js deleted file mode 100644 index 2050187..0000000 --- a/examples/server.js +++ /dev/null @@ -1,174 +0,0 @@ -'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 deleted file mode 100644 index 507063c..0000000 --- a/fixtures/account.jwk.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "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.headers.json b/fixtures/account.registration.headers.json deleted file mode 100644 index f9b6776..0000000 --- a/fixtures/account.registration.headers.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:19:57 GMT", - "content-type": "application/json", - "content-length": "341", - "connection": "close", - "boulder-requester": "11407977", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\", ;rel=\"terms-of-service\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11407977", - "replay-nonce": "0001pgbsovQitzg1gDmvpxu18MOh_lsxRyV8cDC19YozinE", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/account.registration.json b/fixtures/account.registration.json deleted file mode 100644 index d2a42e2..0000000 --- a/fixtures/account.registration.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "key": { - "kty": "EC", - "crv": "P-256", - "x": "9JZE7ZMAAQ-26oP-_pzd9gy2CbuEvgvrB42R1rP2Pb0", - "y": "8yvSYK5sAx30upYpqVknnPPQlK1T3zGTLbJRC-DH_qw" - }, - "contact": ["mailto:letsencrypt+staging@therootcompany.com"], - "initialIp": "66.219.236.169", - "createdAt": "2019-10-24T23:19:57.480171297Z", - "status": "valid" -} diff --git a/fixtures/account.request.json b/fixtures/account.request.json deleted file mode 100644 index 711ba4b..0000000 --- a/fixtures/account.request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct", - "json": { - "protected": "eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJCa0ZsYVBQUi1JTmJfcHVvWHNlbkpGUWJLcFJQM2RraUJuQ0Y4TlNNX3lZIiwieSI6IlZCMEhjM2JoYXlJS2s4QlFiRGJSTDBJZC1LS1hoVkFhRFhLd0RENk1EMjgifSwibm9uY2UiOiIwMDAxSVBlQzN0YV91S29lLTVHanBxUVlGUjFDLVFjS0pzVFVac0daTVFPSzY5ZyIsInVybCI6Imh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvbmV3LWFjY3QiLCJhbGciOiJFUzI1NiJ9", - "payload": "eyJ0ZXJtc09mU2VydmljZUFncmVlZCI6dHJ1ZSwib25seVJldHVybkV4aXN0aW5nIjpmYWxzZSwiY29udGFjdCI6WyJtYWlsdG86bGV0c2VuY3J5cHQrc3RhZ2luZ0B0aGVyb290Y29tcGFueS5jb20iXX0", - "signature": "nuwft1-d349OZoQOH5lsgWCCFYsbciUFrGspiYkd630z_AZU_z0BdNXU5oT2NdaFJJXdqOJkePvEtmTFhAPCEg" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJCa0ZsYVBQUi1JTmJfcHVvWHNlbkpGUWJLcFJQM2RraUJuQ0Y4TlNNX3lZIiwieSI6IlZCMEhjM2JoYXlJS2s4QlFiRGJSTDBJZC1LS1hoVkFhRFhLd0RENk1EMjgifSwibm9uY2UiOiIwMDAxSVBlQzN0YV91S29lLTVHanBxUVlGUjFDLVFjS0pzVFVac0daTVFPSzY5ZyIsInVybCI6Imh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvbmV3LWFjY3QiLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"eyJ0ZXJtc09mU2VydmljZUFncmVlZCI6dHJ1ZSwib25seVJldHVybkV4aXN0aW5nIjpmYWxzZSwiY29udGFjdCI6WyJtYWlsdG86bGV0c2VuY3J5cHQrc3RhZ2luZ0B0aGVyb290Y29tcGFueS5jb20iXX0\",\"signature\":\"nuwft1-d349OZoQOH5lsgWCCFYsbciUFrGspiYkd630z_AZU_z0BdNXU5oT2NdaFJJXdqOJkePvEtmTFhAPCEg\"}", - "method": "POST" -} diff --git a/fixtures/account.response.headers.json b/fixtures/account.response.headers.json deleted file mode 100644 index 2888dde..0000000 --- a/fixtures/account.response.headers.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:24 GMT", - "content-type": "application/json", - "content-length": "340", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\", ;rel=\"terms-of-service\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11408075", - "replay-nonce": "0002O1dowqaEQWEHtP2Cz9BYJuOU91uRvRM1uPFbcdwaj-0", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/account.response.json b/fixtures/account.response.json deleted file mode 100644 index 4c37f73..0000000 --- a/fixtures/account.response.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "key": { - "kty": "EC", - "crv": "P-256", - "x": "BkFlaPPR-INb_puoXsenJFQbKpRP3dkiBnCF8NSM_yY", - "y": "VB0Hc3bhayIKk8BQbDbRL0Id-KKXhVAaDXKwDD6MD28" - }, - "contact": ["mailto:letsencrypt+staging@therootcompany.com"], - "initialIp": "66.219.236.169", - "createdAt": "2019-10-24T23:41:24.38248946Z", - "status": "valid" -} diff --git a/fixtures/authorization.other.json b/fixtures/authorization.other.json deleted file mode 100644 index 73c32dd..0000000 --- a/fixtures/authorization.other.json +++ /dev/null @@ -1,177 +0,0 @@ -[ - [ - { - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603342", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFvU05Bd25ZVjJRWlB0cGNCZHlNUWd1cXB4MFI1SzhFd0txYzJPeWxVYm5vIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MiIsImFsZyI6IkVTMjU2In0", - "payload": "", - "signature": "mgxpomAxc-a2zEbVuyDxncZvoJTbEWwSRb3aE9W-d8TU_9iIK7jKo6RTL6jTZfgM4ToUET7F19NIqWMnQmoREw" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFvU05Bd25ZVjJRWlB0cGNCZHlNUWd1cXB4MFI1SzhFd0txYzJPeWxVYm5vIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MiIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"mgxpomAxc-a2zEbVuyDxncZvoJTbEWwSRb3aE9W-d8TU_9iIK7jKo6RTL6jTZfgM4ToUET7F19NIqWMnQmoREw\"}", - "method": "POST" - }, - - { - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:32 GMT", - "content-type": "application/json", - "content-length": "838", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "replay-nonce": "0002t2JSKyWPm0PEBFrttckiXqIrSEf0PoLdhv24P_QGbrw", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" - }, - { - "identifier": { - "type": "dns", - "value": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com" - }, - "status": "pending", - "expires": "2019-10-31T23:41:32Z", - "challenges": [ - { - "type": "http-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/SX06Rw", - "token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA" - }, - { - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ", - "token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA" - }, - { - "type": "tls-alpn-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/E-EFfg", - "token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA" - } - ] - } - ], - [ - { - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603343", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJ0MkpTS3lXUG0wUEVCRnJ0dGNraVhxSXJTRWYwUG9MZGh2MjRQX1FHYnJ3IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MyIsImFsZyI6IkVTMjU2In0", - "payload": "", - "signature": "equGw3S_17IjiavHk25D3l3g48nE6kIhcN6bvgUdBofh1kfsc-kpPVwkZrBMndqWTh-_WHmQtfg01fkP3xzVGg" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJ0MkpTS3lXUG0wUEVCRnJ0dGNraVhxSXJTRWYwUG9MZGh2MjRQX1FHYnJ3IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MyIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"equGw3S_17IjiavHk25D3l3g48nE6kIhcN6bvgUdBofh1kfsc-kpPVwkZrBMndqWTh-_WHmQtfg01fkP3xzVGg\"}", - "method": "POST" - }, - { - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:32 GMT", - "content-type": "application/json", - "content-length": "838", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "replay-nonce": "0002quWdcKvS2smvRV2Dl98tTHjPUS9sRC4ZDzjXpuyeGhc", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" - }, - { - "identifier": { - "type": "dns", - "value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" - }, - "status": "pending", - "expires": "2019-10-31T23:41:32Z", - "challenges": [ - { - "type": "http-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/bSRwrg", - "token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8" - }, - { - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w", - "token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8" - }, - { - "type": "tls-alpn-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/awV7qQ", - "token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8" - } - ] - } - ], - [ - { - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603344", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxdVdkY0t2UzJzbXZSVjJEbDk4dFRIalBVUzlzUkM0WkR6alhwdXllR2hjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0NCIsImFsZyI6IkVTMjU2In0", - "payload": "", - "signature": "UzOSs2HvxN_mErU-wjrffbFp3JZOu6Earsq3ssj49Qcw3Bf5uyXPKO5DF7iseuL2Qammqofvh70pCka6tD_knQ" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxdVdkY0t2UzJzbXZSVjJEbDk4dFRIalBVUzlzUkM0WkR6alhwdXllR2hjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0NCIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"UzOSs2HvxN_mErU-wjrffbFp3JZOu6Earsq3ssj49Qcw3Bf5uyXPKO5DF7iseuL2Qammqofvh70pCka6tD_knQ\"}", - "method": "POST" - }, - { - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:32 GMT", - "content-type": "application/json", - "content-length": "838", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "replay-nonce": "0001kREyyuaaIacPhD7-j73BHzyQnhfPiBM3PEwnXDFVgTc", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" - }, - { - "identifier": { - "type": "dns", - "value": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com" - }, - "status": "pending", - "expires": "2019-10-31T23:41:32Z", - "challenges": [ - { - "type": "http-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/usH89w", - "token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs" - }, - { - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw", - "token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs" - }, - { - "type": "tls-alpn-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/6C26qQ", - "token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs" - } - ] - } - ] -] diff --git a/fixtures/authorization.post.json b/fixtures/authorization.post.json deleted file mode 100644 index 3871138..0000000 --- a/fixtures/authorization.post.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFqNEF6c2Qwa2s2aTYwTlN6Um9aY3ZMaWRtTG81QjBzRzFsTUtUcVdyMzg4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MSIsImFsZyI6IkVTMjU2In0", - "payload": "", - "signature": "qjrQyqKRskdhF7DVUymZdHhm9neC9vgH9UUc6D-vtXtS8T2QW9C82qsyghZdGGJLWeKeZLRsADjmZSh5XCAa4g" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFqNEF6c2Qwa2s2aTYwTlN6Um9aY3ZMaWRtTG81QjBzRzFsTUtUcVdyMzg4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MSIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"qjrQyqKRskdhF7DVUymZdHhm9neC9vgH9UUc6D-vtXtS8T2QW9C82qsyghZdGGJLWeKeZLRsADjmZSh5XCAa4g\"}", - "method": "POST" -} diff --git a/fixtures/authorization.response.headers.json b/fixtures/authorization.response.headers.json deleted file mode 100644 index ebe20b1..0000000 --- a/fixtures/authorization.response.headers.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:32 GMT", - "content-type": "application/json", - "content-length": "420", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "replay-nonce": "0001oSNAwnYV2QZPtpcBdyMQguqpx0R5K8EwKqc2OylUbno", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/authorization.response.json b/fixtures/authorization.response.json deleted file mode 100644 index 1afd36b..0000000 --- a/fixtures/authorization.response.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "identifier": { - "type": "dns", - "value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" - }, - "status": "pending", - "expires": "2019-10-31T23:41:32Z", - "challenges": [ - { - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw", - "token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI" - } - ], - "wildcard": true -} diff --git a/fixtures/cert.request.json b/fixtures/cert.request.json deleted file mode 100644 index bbdbc5a..0000000 --- a/fixtures/cert.request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/cert/fa78326c21c0c7f06c03931900bead4fe3ee", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDExLW5qUV91MWp4N1dqVEdfY1Blam05UUxLZWxFcUVFdEpEa3JlVHJ5OVI4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jZXJ0L2ZhNzgzMjZjMjFjMGM3ZjA2YzAzOTMxOTAwYmVhZDRmZTNlZSIsImFsZyI6IkVTMjU2In0", - "payload": "", - "signature": "639Q5Eo2_xWh3ylRy3olXJVXz_4JTrpVFkUmz9-h1l8Hrsmg47I0HFgMrHslfKEJfj86zGUh9XY-VtBF2IFcIQ" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDExLW5qUV91MWp4N1dqVEdfY1Blam05UUxLZWxFcUVFdEpEa3JlVHJ5OVI4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jZXJ0L2ZhNzgzMjZjMjFjMGM3ZjA2YzAzOTMxOTAwYmVhZDRmZTNlZSIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"639Q5Eo2_xWh3ylRy3olXJVXz_4JTrpVFkUmz9-h1l8Hrsmg47I0HFgMrHslfKEJfj86zGUh9XY-VtBF2IFcIQ\"}", - "method": "POST" -} diff --git a/fixtures/cert.response.headers.json b/fixtures/cert.response.headers.json deleted file mode 100644 index d131dcc..0000000 --- a/fixtures/cert.response.headers.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:44 GMT", - "content-type": "application/pem-certificate-chain", - "content-length": "3806", - "connection": "close", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "replay-nonce": "0002vmpuKxQvokCGu5-cbVhsXkBHweBkdFnNrIpufnVn8mc", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/cert.response.txt b/fixtures/cert.response.txt deleted file mode 100644 index 02dc023..0000000 --- a/fixtures/cert.response.txt +++ /dev/null @@ -1,64 +0,0 @@ -// Note: I may have added or truncated a beginning or ending -// newline here in the process of copy/paste ------BEGIN CERTIFICATE----- -MIIF9TCCBN2gAwIBAgITAPp4MmwhwMfwbAOTGQC+rU/j7jANBgkqhkiG9w0BAQsF -ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xOTEwMjQy -MjQxNDRaFw0yMDAxMjIyMjQxNDRaMDUxMzAxBgNVBAMTKnhuLS1mb28tYWNtZWpz -LTJlYTQtems4eC50ZXN0LnV0YWhydXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAOXgIzVvJzQRuGkomoKQzswNyMaFB7MmCHNOW98yYxfHpLqj -KKddplJpvHQ/R8I15+38QfqT9kvj9vQ7i3gU6AUya56Sg6TSSmUE5PBP7WfEn/2O -+iHzZ/Devq/Oq0fHQoF+TtEFgnMVZZL4gnEyciSzQs5ftn+HejLGYmBH5uJlPGCp -9lMOe+ziweWKbmZYDu4Qrqf3TEHbFOpBPgJUna4tz0xmISdxzuR9Q/tie3a+cCjV -4xtxCblN9W37KC1VnEkLtQwgm6zjZAVSUWOLZUqMVL2H+/jR5Z9r1XYevEDlAl35 -sW0kaEf/FdLfr8tfbbnPUsVvRL5I5gdLmyonJccCAwEAAaOCAw8wggMLMA4GA1Ud -DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T -AQH/BAIwADAdBgNVHQ4EFgQUJqGfhDoxM99m3HZUhlME4JMg+zQwHwYDVR0jBBgw -FoAUwMwDRrlYIMxccnDz4S7LIKb1aDowdwYIKwYBBQUHAQEEazBpMDIGCCsGAQUF -BzABhiZodHRwOi8vb2NzcC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZzAzBggr -BgEFBQcwAoYnaHR0cDovL2NlcnQuc3RnLWludC14MS5sZXRzZW5jcnlwdC5vcmcv -MIG9BgNVHREEgbUwgbKCLCoueG4tLWJhei1hY21lanMtMmVhNC16azh4LnRlc3Qu -dXRhaHJ1c3QuY29tgip4bi0tYmFyLWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFo -cnVzdC5jb22CKnhuLS1iYXotYWNtZWpzLTJlYTQtems4eC50ZXN0LnV0YWhydXN0 -LmNvbYIqeG4tLWZvby1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29t -MEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUH -AgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB -9ASB8QDvAHYAxj8iGMN9VqaqBrWW2o5T1NcVbR6brI5E0iAt5k1p2dwAAAFuACW/ -/QAABAMARzBFAiB/xTPuBFV2+yfovKBiru29WQ+j3wjTGE1Urcn1Rn+5nQIhALH+ -5N4A0TiK04romA8Nb/R5X0sNM68HGK/KRCICdYOxAHUAsMyD5aX5fWuvfAnMKEkE -hyrH6IsTLGNQt8b9JuFsbHcAAAFuACW//gAABAMARjBEAiAcL3cjhbwAOV34v3vK -svbb9yIK36vRucq3hu/Vs1B3ZAIgfTwjAHDE6GqfZEW2e9MjuULEvMdF2QHVh7WB -Bp5A48wwDQYJKoZIhvcNAQELBQADggEBAFxbkUt0QOZNAKnTqdYnBP2FlxezjFPq -P4pD/G2/JFKi86VDg2vLVfPMGd7jv+e8Ao0+G9rgC3vtQE817T5d9XFlJ8p7dMjK -TbTmSlKHxM9Dal8fqC7kbqqx/gdpzzPyBoDYlKWvhr3qXsxB/hGI3OX+d42R1wsr -zcQKaG2HpJcerZ1au2Jm/YOCJPpDHMAFKK5wuCmOIBfNQ+ULyStPZLQWPdMI04S2 -Y8eIQgS6q9OX1CtvuehVFwyO8TNi53do88wFDdHF7lNZEjz7NvpNqi3qeZgSRuAb -/fTMCULMjDghh+xpTLRzSROB6YJbU8uXtSZ6Xn04SZ6ZSuvbCYmHlsU= ------END CERTIFICATE----- - ------BEGIN CERTIFICATE----- -MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw -GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 -MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 -8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym -oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 -ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN -xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 -dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 -AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw -HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 -BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu -b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu -Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq -hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF -UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 -AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp -DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 -IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf -zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI -PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w -SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em -2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 -WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt -n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= ------END CERTIFICATE----- diff --git a/fixtures/challenge.others.json b/fixtures/challenge.others.json deleted file mode 100644 index c3b9aae..0000000 --- a/fixtures/challenge.others.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - [ - { - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDIybXdZUUhpR0NMMVRacUViYkNBZ1N1djJYMXctSGhkMWR0TV9zRllXRGlNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0Mi9JNHVoR1EiLCJhbGciOiJFUzI1NiJ9", - "payload": "e30", - "signature": "90XygqCrKMhqsoFD4-J56yYgEKuevnw7V-4MaP_lZKzMn9vnhK_CtWh0k5kRuePhJzopTRrWkRzXz9OExlt9WQ" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDIybXdZUUhpR0NMMVRacUViYkNBZ1N1djJYMXctSGhkMWR0TV9zRllXRGlNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0Mi9JNHVoR1EiLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"90XygqCrKMhqsoFD4-J56yYgEKuevnw7V-4MaP_lZKzMn9vnhK_CtWh0k5kRuePhJzopTRrWkRzXz9OExlt9WQ\"}", - "method": "POST" - }, - { - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:42 GMT", - "content-type": "application/json", - "content-length": "292", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\", ;rel=\"up\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ", - "replay-nonce": "0001XZufnGiSHfABU10B8FWCxHzvqPN991zSEO3-uQnNZqI", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" - }, - { - "type": "dns-01", - "status": "valid", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ", - "token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA", - "validationRecord": [ - { "hostname": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com" } - ] - } - ], - - [ - { - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFYWnVmbkdpU0hmQUJVMTBCOEZXQ3hIenZxUE45OTF6U0VPMy11UW5OWnFJIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0My9VbXZfNXciLCJhbGciOiJFUzI1NiJ9", - "payload": "e30", - "signature": "I5p1OLU52W7m-oHeRWAuZQyf5saBlm1Mv5UV8kqRLVxxt-kMEJLXwKgP0kgfz-rXjnZheYnrKiKERZX1wt7RdA" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFYWnVmbkdpU0hmQUJVMTBCOEZXQ3hIenZxUE45OTF6U0VPMy11UW5OWnFJIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0My9VbXZfNXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"I5p1OLU52W7m-oHeRWAuZQyf5saBlm1Mv5UV8kqRLVxxt-kMEJLXwKgP0kgfz-rXjnZheYnrKiKERZX1wt7RdA\"}", - "method": "POST" - }, - { - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:43 GMT", - "content-type": "application/json", - "content-length": "292", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\", ;rel=\"up\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w", - "replay-nonce": "00012YkSGH0-3llPNZT_hV8Ovw11jJU9YyppuJ--gJldLTo", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" - }, - { - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w", - "token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8" - } - ], - [ - { - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxT0s4WGhNcWtCVWgzYk1LUV9ZMUo2QXJUbEVOR01BTUQ4bHc3WjNtT2JvIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0NC9IcXZsaXciLCJhbGciOiJFUzI1NiJ9", - "payload": "e30", - "signature": "ltAp1E52XSMMZpleycguLlo4Hii0FxAbiXcmZBdA-vTjqJb8S1X4CVYQ-qebmYFlCipRhe9Juaj6zpvX7UbTnQ" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxT0s4WGhNcWtCVWgzYk1LUV9ZMUo2QXJUbEVOR01BTUQ4bHc3WjNtT2JvIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0NC9IcXZsaXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"ltAp1E52XSMMZpleycguLlo4Hii0FxAbiXcmZBdA-vTjqJb8S1X4CVYQ-qebmYFlCipRhe9Juaj6zpvX7UbTnQ\"}", - "method": "POST" - }, - - { - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:44 GMT", - "content-type": "application/json", - "content-length": "292", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\", ;rel=\"up\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw", - "replay-nonce": "0001RZo7OXhCjsG_9mtrLylmz443TVc9FOsyhfergGWmkDM", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" - }, - { - "type": "dns-01", - "status": "valid", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw", - "token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs", - "validationRecord": [ - { "hostname": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com" } - ] - } - ] -] diff --git a/fixtures/challenge.pending.json b/fixtures/challenge.pending.json deleted file mode 100644 index 9f5f960..0000000 --- a/fixtures/challenge.pending.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFrUkV5eXVhYUlhY1BoRDctajczQkh6eVFuaGZQaUJNM1BFd25YREZWZ1RjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9", - "payload": "e30", - "signature": "QZKdMroSf-qrno2UBHf_L2nL9VrvDtDEb0uLL2fp1yKkwX8u0sELLOYfIu8YqeSwcmPZ1LQHWbXLx5SQ0Lv3Pw" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFrUkV5eXVhYUlhY1BoRDctajczQkh6eVFuaGZQaUJNM1BFd25YREZWZ1RjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"QZKdMroSf-qrno2UBHf_L2nL9VrvDtDEb0uLL2fp1yKkwX8u0sELLOYfIu8YqeSwcmPZ1LQHWbXLx5SQ0Lv3Pw\"}", - "method": "POST" -} diff --git a/fixtures/challenge.pending.response.headers.json b/fixtures/challenge.pending.response.headers.json deleted file mode 100644 index 4d2edcd..0000000 --- a/fixtures/challenge.pending.response.headers.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:39 GMT", - "content-type": "application/json", - "content-length": "190", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\", ;rel=\"up\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw", - "replay-nonce": "0001In5LKCnj27k3uNTzl19vqQ5oHlroIJJI-U1daaxNd-Y", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/challenge.pending.response.json b/fixtures/challenge.pending.response.json deleted file mode 100644 index ae9012d..0000000 --- a/fixtures/challenge.pending.response.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw", - "token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI" -} diff --git a/fixtures/challenge.valid.json b/fixtures/challenge.valid.json deleted file mode 100644 index 0eca62e..0000000 --- a/fixtures/challenge.valid.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFJbjVMS0NuajI3azN1TlR6bDE5dnFRNW9IbHJvSUpKSS1VMWRhYXhOZC1ZIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9", - "payload": "e30", - "signature": "3SVtWvRXGFirW198sM4bWErA5M_GplWkI_duSKLHtdGLe-R2D2r0VK1_Xn4exfk6MGIBSkaeeYV6RJfnsLgYLg" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFJbjVMS0NuajI3azN1TlR6bDE5dnFRNW9IbHJvSUpKSS1VMWRhYXhOZC1ZIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"3SVtWvRXGFirW198sM4bWErA5M_GplWkI_duSKLHtdGLe-R2D2r0VK1_Xn4exfk6MGIBSkaeeYV6RJfnsLgYLg\"}", - "method": "POST" -} diff --git a/fixtures/challenge.valid.response.headers.json b/fixtures/challenge.valid.response.headers.json deleted file mode 100644 index daa5dcb..0000000 --- a/fixtures/challenge.valid.response.headers.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "dns-01", - "status": "valid", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw", - "token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI", - "validationRecord": [ - { "hostname": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" } - ] -} diff --git a/fixtures/challenge.valid.response.json b/fixtures/challenge.valid.response.json deleted file mode 100644 index 1d530e5..0000000 --- a/fixtures/challenge.valid.response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:40 GMT", - "content-type": "application/json", - "content-length": "292", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\", ;rel=\"up\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw", - "replay-nonce": "0001P9ksMrD-4xaHyRPUVR2pq6PMQSG7T-ELjWBWXsLROv0", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/directory.request.json b/fixtures/directory.request.json deleted file mode 100644 index e84fd00..0000000 --- a/fixtures/directory.request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "method": "GET", - "url": "https://acme-staging-v02.api.letsencrypt.org/directory", - "json": true, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Accept": "application/json" - } -} diff --git a/fixtures/directory.response.headers.json b/fixtures/directory.response.headers.json deleted file mode 100644 index 7f4a3c1..0000000 --- a/fixtures/directory.response.headers.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:24 GMT", - "content-type": "application/json", - "content-length": "724", - "connection": "close", - "cache-control": "public, max-age=0, no-cache", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/directory.response.json b/fixtures/directory.response.json deleted file mode 100644 index d8d8663..0000000 --- a/fixtures/directory.response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Uw5jwSdQL_Q": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417", - "keyChange": "https://acme-staging-v02.api.letsencrypt.org/acme/key-change", - "meta": { - "caaIdentities": ["letsencrypt.org"], - "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf", - "website": "https://letsencrypt.org/docs/staging-environment/" - }, - "newAccount": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct", - "newNonce": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce", - "newOrder": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order", - "revokeCert": "https://acme-staging-v02.api.letsencrypt.org/acme/revoke-cert" -} diff --git a/fixtures/finalize.valid.json b/fixtures/finalize.valid.json deleted file mode 100644 index 5a008af..0000000 --- a/fixtures/finalize.valid.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/11408075/57799471", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFSWm83T1hoQ2pzR185bXRyTHlsbXo0NDNUVmM5Rk9zeWhmZXJnR1dta0RNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9maW5hbGl6ZS8xMTQwODA3NS81Nzc5OTQ3MSIsImFsZyI6IkVTMjU2In0", - "payload": "eyJjc3IiOiJNSUlEVHpDQ0FqY0NBUUF3TlRFek1ERUdBMVVFQXd3cWVHNHRMV1p2YnkxaFkyMWxhbk10TW1WaE5DMTZhemg0TG5SbGMzUXVkWFJoYUhKMWMzUXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE1ZUFqTlc4bk5CRzRhU2lhZ3BET3pBM0l4b1VIc3lZSWMwNWIzekpqRjhla3VxTW9wMTJtVW1tOGREOUh3alhuN2Z4Qi1wUDJTLVAyOUR1TGVCVG9CVEpybnBLRHBOSktaUVRrOEVfdFo4U2ZfWTc2SWZObjhONi1yODZyUjhkQ2dYNU8wUVdDY3hWbGt2aUNjVEp5SkxOQ3psLTJmNGQ2TXNaaVlFZm00bVU4WUtuMlV3NTc3T0xCNVlwdVpsZ083aEN1cF9kTVFkc1U2a0UtQWxTZHJpM1BUR1loSjNITzVIMUQtMko3ZHI1d0tOWGpHM0VKdVUzMWJmc29MVldjU1F1MURDQ2JyT05rQlZKUlk0dGxTb3hVdllmNy1OSGxuMnZWZGg2OFFPVUNYZm14YlNSb1JfOFYwdC12eTE5dHVjOVN4VzlFdmtqbUIwdWJLaWNseHdJREFRQUJvSUhVTUlIUkJna3Foa2lHOXcwQkNRNHhnY013Z2NBd2diMEdBMVVkRVFTQnRUQ0Jzb0lxZUc0dExXWnZieTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRnlMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMkNMQ291ZUc0dExXSmhlaTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRjZMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBTV9idU84N0YtMkd2aThKZmlaZ3ZYNGNvUnllLVhSSDVnbTJ6enRjNW1KS0ZxRmRBdkV5Z0IxbE82NmJaTG5uZjk5bWRROFk5UnQ0R1RiU3N5N1djQ1NMVF91MVNGX0h1REU5SnZ2ek43MnU1VmtlLW1KelB0cG1OcTlRODZpRWNVQnVEMmNfVVVCQ0Y2ZEFsTHhUZmRQRkJWdXBPSnVCRmQ4azdBNlhhbTl0UjFKV3p4RGdrSHM1cTdmSWo1dXVLcmdjSlhWc19lWHA0QkNONEcyM2hKX01YR1RidDhqeHU1MTFOaDE0Z18wT3JlWkw1bHd5MWR5ZE9mN0pLdGpUdmtyQWE1YjJDVXlLa293NHlaLTNoUmVRcHZjVnIzcnRaTWtKdndMMHI5WjcxcENHRjViUVEweDBIVk04VzYtVkotTWJpLVlhTC04TjNyNEpTbWdDN09VIn0", - "signature": "_X0X-Wg86dr5mF0eS0GOYNSmO0HCenlIGQeMygRVoH7BpYO0AMK_mgRQlNR3MWNMULC_aQ-oEMtsXGMXrTa7VA" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFSWm83T1hoQ2pzR185bXRyTHlsbXo0NDNUVmM5Rk9zeWhmZXJnR1dta0RNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9maW5hbGl6ZS8xMTQwODA3NS81Nzc5OTQ3MSIsImFsZyI6IkVTMjU2In0\",\"payload\":\"eyJjc3IiOiJNSUlEVHpDQ0FqY0NBUUF3TlRFek1ERUdBMVVFQXd3cWVHNHRMV1p2YnkxaFkyMWxhbk10TW1WaE5DMTZhemg0TG5SbGMzUXVkWFJoYUhKMWMzUXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE1ZUFqTlc4bk5CRzRhU2lhZ3BET3pBM0l4b1VIc3lZSWMwNWIzekpqRjhla3VxTW9wMTJtVW1tOGREOUh3alhuN2Z4Qi1wUDJTLVAyOUR1TGVCVG9CVEpybnBLRHBOSktaUVRrOEVfdFo4U2ZfWTc2SWZObjhONi1yODZyUjhkQ2dYNU8wUVdDY3hWbGt2aUNjVEp5SkxOQ3psLTJmNGQ2TXNaaVlFZm00bVU4WUtuMlV3NTc3T0xCNVlwdVpsZ083aEN1cF9kTVFkc1U2a0UtQWxTZHJpM1BUR1loSjNITzVIMUQtMko3ZHI1d0tOWGpHM0VKdVUzMWJmc29MVldjU1F1MURDQ2JyT05rQlZKUlk0dGxTb3hVdllmNy1OSGxuMnZWZGg2OFFPVUNYZm14YlNSb1JfOFYwdC12eTE5dHVjOVN4VzlFdmtqbUIwdWJLaWNseHdJREFRQUJvSUhVTUlIUkJna3Foa2lHOXcwQkNRNHhnY013Z2NBd2diMEdBMVVkRVFTQnRUQ0Jzb0lxZUc0dExXWnZieTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRnlMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMkNMQ291ZUc0dExXSmhlaTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRjZMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBTV9idU84N0YtMkd2aThKZmlaZ3ZYNGNvUnllLVhSSDVnbTJ6enRjNW1KS0ZxRmRBdkV5Z0IxbE82NmJaTG5uZjk5bWRROFk5UnQ0R1RiU3N5N1djQ1NMVF91MVNGX0h1REU5SnZ2ek43MnU1VmtlLW1KelB0cG1OcTlRODZpRWNVQnVEMmNfVVVCQ0Y2ZEFsTHhUZmRQRkJWdXBPSnVCRmQ4azdBNlhhbTl0UjFKV3p4RGdrSHM1cTdmSWo1dXVLcmdjSlhWc19lWHA0QkNONEcyM2hKX01YR1RidDhqeHU1MTFOaDE0Z18wT3JlWkw1bHd5MWR5ZE9mN0pLdGpUdmtyQWE1YjJDVXlLa293NHlaLTNoUmVRcHZjVnIzcnRaTWtKdndMMHI5WjcxcENHRjViUVEweDBIVk04VzYtVkotTWJpLVlhTC04TjNyNEpTbWdDN09VIn0\",\"signature\":\"_X0X-Wg86dr5mF0eS0GOYNSmO0HCenlIGQeMygRVoH7BpYO0AMK_mgRQlNR3MWNMULC_aQ-oEMtsXGMXrTa7VA\"}", - "method": "POST" -} diff --git a/fixtures/finalize.valid.response.headers.json b/fixtures/finalize.valid.response.headers.json deleted file mode 100644 index e721ecd..0000000 --- a/fixtures/finalize.valid.response.headers.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:44 GMT", - "content-type": "application/json", - "content-length": "993", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/order/11408075/57799471", - "replay-nonce": "00011-njQ_u1jx7WjTG_cPejm9QLKelEqEEtJDkreTry9R8", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/finalize.valid.response.json b/fixtures/finalize.valid.response.json deleted file mode 100644 index d314bd7..0000000 --- a/fixtures/finalize.valid.response.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "status": "valid", - "expires": "2019-10-31T23:41:32Z", - "identifiers": [ - { - "type": "dns", - "value": "*.xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" - }, - { - "type": "dns", - "value": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com" - }, - { - "type": "dns", - "value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" - }, - { "type": "dns", "value": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com" } - ], - "authorizations": [ - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341", - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603342", - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603343", - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603344" - ], - "finalize": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/11408075/57799471", - "certificate": "https://acme-staging-v02.api.letsencrypt.org/acme/cert/fa78326c21c0c7f06c03931900bead4fe3ee" -} diff --git a/fixtures/nonce.request.json b/fixtures/nonce.request.json deleted file mode 100644 index 485a9d7..0000000 --- a/fixtures/nonce.request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "method": "HEAD", - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce", - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 Darwin darwin/17.7.0 Darwin/x64" - } -} diff --git a/fixtures/nonce.response.headers.json b/fixtures/nonce.response.headers.json deleted file mode 100644 index f13454b..0000000 --- a/fixtures/nonce.response.headers.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:24 GMT", - "connection": "close", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "replay-nonce": "0001IPeC3ta_uKoe-5GjpqQYFR1C-QcKJsTUZsGZMQOK69g", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/nonce.response.txt b/fixtures/nonce.response.txt deleted file mode 100644 index a6b4c21..0000000 --- a/fixtures/nonce.response.txt +++ /dev/null @@ -1 +0,0 @@ -// there is no nonce response body, see the headers diff --git a/fixtures/order.request.json b/fixtures/order.request.json deleted file mode 100644 index 255f5d7..0000000 --- a/fixtures/order.request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order", - "json": { - "protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJPMWRvd3FhRVFXRUh0UDJDejlCWUp1T1U5MXVSdlJNMXVQRmJjZHdhai0wIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctb3JkZXIiLCJhbGciOiJFUzI1NiJ9", - "payload": "eyJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoieG4tLWZvby1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmFyLWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifSx7InR5cGUiOiJkbnMiLCJ2YWx1ZSI6IioueG4tLWJhei1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmF6LWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifV19", - "signature": "Bw8cjSwQj_rFooUFL61gqiuLXec-8x4anHNF1ueVt_LvoCO70bYt0fM26W4hOJ9Es6fibmYazFKSTPwdgnLm2Q" - }, - "headers": { - "User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64", - "Content-Type": "application/jose+json", - "Accept": "application/json" - }, - "body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJPMWRvd3FhRVFXRUh0UDJDejlCWUp1T1U5MXVSdlJNMXVQRmJjZHdhai0wIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctb3JkZXIiLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"eyJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoieG4tLWZvby1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmFyLWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifSx7InR5cGUiOiJkbnMiLCJ2YWx1ZSI6IioueG4tLWJhei1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmF6LWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifV19\",\"signature\":\"Bw8cjSwQj_rFooUFL61gqiuLXec-8x4anHNF1ueVt_LvoCO70bYt0fM26W4hOJ9Es6fibmYazFKSTPwdgnLm2Q\"}", - "method": "POST" -} diff --git a/fixtures/order.response.headers.json b/fixtures/order.response.headers.json deleted file mode 100644 index 5757b23..0000000 --- a/fixtures/order.response.headers.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "server": "nginx", - "date": "Thu, 24 Oct 2019 23:41:32 GMT", - "content-type": "application/json", - "content-length": "893", - "connection": "close", - "boulder-requester": "11408075", - "cache-control": "public, max-age=0, no-cache", - "link": ";rel=\"index\"", - "location": "https://acme-staging-v02.api.letsencrypt.org/acme/order/11408075/57799471", - "replay-nonce": "0001j4Azsd0kk6i60NSzRoZcvLidmLo5B0sG1lMKTqWr388", - "x-frame-options": "DENY", - "strict-transport-security": "max-age=604800" -} diff --git a/fixtures/order.response.json b/fixtures/order.response.json deleted file mode 100644 index a03bbd8..0000000 --- a/fixtures/order.response.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "status": "pending", - "expires": "2019-10-31T23:41:32.669736375Z", - "identifiers": [ - { - "type": "dns", - "value": "*.xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" - }, - { - "type": "dns", - "value": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com" - }, - { - "type": "dns", - "value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" - }, - { "type": "dns", "value": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com" } - ], - "authorizations": [ - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341", - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603342", - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603343", - "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603344" - ], - "finalize": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/11408075/57799471" -} diff --git a/fixtures/server.jwk.json b/fixtures/server.jwk.json deleted file mode 100644 index ca65589..0000000 --- a/fixtures/server.jwk.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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.js b/lib/browser.js deleted file mode 100644 index 6cbed73..0000000 --- a/lib/browser.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -var native = module.exports; - -native._canCheck = function (me) { - me._canCheck = {}; - return me - .request({ url: me._baseUrl + '/api/_acme_api_/' }) - .then(function (resp) { - if (resp.body.success) { - me._canCheck['http-01'] = true; - me._canCheck['dns-01'] = true; - } - }) - .catch(function () { - // ignore - }); -}; - -native._dns01 = function (me, ch) { - return me - .request({ - url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT' - }) - .then(function (resp) { - var err; - if (!resp.body || !Array.isArray(resp.body.answer)) { - err = new Error('failed to get DNS response'); - console.error(err); - throw err; - } - if (!resp.body.answer.length) { - err = new Error('failed to get DNS answer record in response'); - console.error(err); - throw err; - } - return { - answer: resp.body.answer.map(function (ans) { - return { data: ans.data, ttl: ans.ttl }; - }) - }; - }); -}; - -native._http01 = function (me, ch) { - var url = encodeURIComponent(ch.challengeUrl); - return me - .request({ - url: me._baseUrl + '/api/http?url=' + url - }) - .then(function (resp) { - return resp.body; - }); -}; diff --git a/lib/browser/client-user-agent.js b/lib/browser/client-user-agent.js deleted file mode 100644 index a5acaa7..0000000 --- a/lib/browser/client-user-agent.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -var UserAgent = module.exports; -UserAgent.get = function () { - return false; -}; diff --git a/lib/browser/http.js b/lib/browser/http.js deleted file mode 100644 index fb71f65..0000000 --- a/lib/browser/http.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -var http = module.exports; - -http.request = function (opts) { - opts.cors = true; - 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 deleted file mode 100644 index 84a2e84..0000000 --- a/lib/browser/sha2.js +++ /dev/null @@ -1,13 +0,0 @@ -'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/native.js b/lib/native.js deleted file mode 100644 index 36983d2..0000000 --- a/lib/native.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -var native = module.exports; -var promisify = require('util').promisify; -var resolveTxt = promisify(require('dns').resolveTxt); -var crypto = require('crypto'); - -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; - }); -}; - -// the hashcash here is for browser parity only -// basically we ask the client to find a needle in a haystack -// (very similar to CloudFlare's api protection) -native._hashcash = function (ch) { - if (!ch || !ch.nonce) { - ch = { nonce: 'xxx' }; - } - return Promise.resolve() - .then(function () { - // only get easy answers - var len = ch.needle.length; - var start = ch.start || 0; - var end = ch.end || Math.ceil(len / 2); - var window = parseInt(end - start, 10) || 0; - - var maxLen = 6; - var maxTries = Math.pow(2, maxLen * 8); - if ( - len > maxLen || - window < Math.ceil(len / 2) || - ch.needle.toLowerCase() !== ch.needle || - ch.alg !== 'SHA-256' - ) { - // bail unless the server is issuing very easy challenges - throw new Error('possible and easy answers only, please'); - } - - var haystack; - var i; - var answer; - var needle = Buffer.from(ch.needle, 'hex'); - for (i = 0; i < maxTries; i += 1) { - answer = i.toString(16); - if (answer.length % 2) { - answer = '0' + answer; - } - haystack = crypto - .createHash('sha256') - .update(Buffer.from(ch.nonce + answer, 'hex')) - .digest() - .slice(ch.start, ch.end); - if (-1 !== haystack.indexOf(needle)) { - return ch.nonce + ':' + answer; - } - } - return ch.nonce + ':xxx'; - }) - .catch(function () { - //console.log('[debug]', err); - // ignore any error - return ch.nonce + ':xxx'; - }); -}; diff --git a/lib/node/client-user-agent.js b/lib/node/client-user-agent.js deleted file mode 100644 index f852906..0000000 --- a/lib/node/client-user-agent.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -var os = require('os'); -var ver = require('../../package.json').version; - -var UserAgent = module.exports; -UserAgent.get = function (me) { - // ACME clients MUST have an RFC7231-compliant User-Agent - // ex: Greenlock/v3 ACME.js/v3 node/v12.0.0 darwin/17.7.0 Darwin/x64 - // - // See https://tools.ietf.org/html/rfc8555#section-6.1 - // And https://tools.ietf.org/html/rfc7231#section-5.5.3 - // And https://community.letsencrypt.org/t/user-agent-flag-explained/3843/2 - - var ua = - 'ACME.js/' + - ver + - ' ' + - process.release.name + - '/' + - process.version + - ' ' + - os.platform() + - '/' + - os.release() + - ' ' + - os.type() + - '/' + - process.arch; - - var pkg = me.packageAgent; - if (pkg) { - ua = pkg + ' ' + ua; - } - - return ua; -}; diff --git a/lib/node/http.js b/lib/node/http.js deleted file mode 100644 index 245fd73..0000000 --- a/lib/node/http.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -var http = module.exports; -var promisify = require('util').promisify; -var request = promisify(require('@root/request')); - -http.request = function (opts) { - return request(opts); -}; diff --git a/lib/node/sha2.js b/lib/node/sha2.js deleted file mode 100644 index e41d83f..0000000 --- a/lib/node/sha2.js +++ /dev/null @@ -1,14 +0,0 @@ -/* 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/maintainers.js b/maintainers.js deleted file mode 100644 index 26d3876..0000000 --- a/maintainers.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -var M = module.exports; -var native = require('./lib/native.js'); - -// Keep track of active maintainers so that we know who to inform if -// something breaks or has a serious bug or flaw. - -var oldCollegeTries = {}; -M.init = function (me) { - if (oldCollegeTries[me.maintainerEmail]) { - return; - } - - var tz = ''; - try { - // Use timezone to stagger messages to maintainers - tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - } catch (e) { - // ignore node versions with no or incomplete Intl - } - - // Use locale to know what language to use - var env = process.env; - var locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE; - - try { - M._init(me, tz, locale); - } catch (e) { - //console.log(e); - // ignore - } -}; - -M._init = function (me, tz, locale) { - setTimeout(function () { - // prevent a stampede from misconfigured clients in an eternal loop - me.request({ - timeout: 3000, - method: 'GET', - url: 'https://api.rootprojects.org/api/nonce', - json: true - }) - .then(function (resp) { - // in the browser this will work until solved, but in - // node this will bail unless the challenge is trivial - return native._hashcash(resp.body || {}); - }) - .then(function (hashcash) { - var req = { - timeout: 3000, - headers: { - 'x-root-nonce-v1': hashcash - }, - method: 'POST', - url: - 'https://api.rootprojects.org/api/projects/ACME.js/dependents', - json: { - maintainer: me.maintainerEmail, - package: me.packageAgent, - tz: tz, - locale: locale - } - }; - return me.request(req); - }) - .catch(function (err) { - if (me.debug) { - console.error( - 'error adding maintainer to support notices:' - ); - console.error(err); - } - }) - .then(function (/*resp*/) { - oldCollegeTries[me.maintainerEmail] = true; - //console.log(resp); - }); - }, me.__timeout || 3000); -}; - -if (require.main === module) { - var ACME = require('./'); - var acme = ACME.create({ - maintainerEmail: 'aj+acme-test@rootprojects.org', - packageAgent: 'test/v0', - __timeout: 100 - }); - M.init(acme); -} diff --git a/node.js b/node.js new file mode 100644 index 0000000..fa00c1b --- /dev/null +++ b/node.js @@ -0,0 +1,1431 @@ +// 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'; +/* globals Promise */ + +var ACME = (module.exports.ACME = {}); + +ACME.formatPemChain = function formatPemChain(str) { + return ( + str + .trim() + .replace(/[\r\n]+/g, '\n') + .replace(/\-\n\-/g, '-\n\n-') + '\n' + ); +}; +ACME.splitPemChain = function splitPemChain(str) { + return str + .trim() + .split(/[\r\n]{2,}/g) + .map(function(str) { + return str + '\n'; + }); +}; + +// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} +// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" +ACME.challengePrefixes = { + 'http-01': '/.well-known/acme-challenge', + 'dns-01': '_acme-challenge' +}; +ACME.challengeTests = { + 'http-01': function(me, auth) { + var url = + 'http://' + + auth.hostname + + ACME.challengePrefixes['http-01'] + + '/' + + auth.token; + return me._request({ url: url }).then(function(resp) { + var err; + + // TODO limit the number of bytes that are allowed to be downloaded + if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { + return true; + } + + err = new Error( + 'Error: Failed HTTP-01 Pre-Flight / Dry Run.\n' + + "curl '" + + url + + "'\n" + + "Expected: '" + + auth.keyAuthorization + + "'\n" + + "Got: '" + + resp.body + + "'\n" + + 'See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4' + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; + return Promise.reject(err); + }); + }, + 'dns-01': function(me, auth) { + // remove leading *. on wildcard domains + return me + ._dig({ + type: 'TXT', + name: auth.dnsHost + }) + .then(function(ans) { + var err; + + if ( + ans.answer.some(function(txt) { + return auth.dnsAuthorization === txt.data[0]; + }) + ) { + return true; + } + + err = new Error( + 'Error: Failed DNS-01 Pre-Flight Dry Run.\n' + + "dig TXT '" + + auth.dnsHost + + "' does not return '" + + auth.dnsAuthorization + + "'\n" + + 'See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4' + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; + return Promise.reject(err); + }); + } +}; + +ACME._getUserAgentString = function(deps) { + var uaDefaults = { + pkg: 'Greenlock/' + deps.pkg.version, + os: + '(' + + deps.os.type() + + '; ' + + deps.process.arch + + ' ' + + deps.os.platform() + + ' ' + + deps.os.release() + + ')', + node: 'Node.js/' + deps.process.version, + user: '' + }; + + var userAgent = []; + + //Object.keys(currentUAProps) + Object.keys(uaDefaults).forEach(function(key) { + if (uaDefaults[key]) { + userAgent.push(uaDefaults[key]); + } + }); + + return userAgent.join(' ').trim(); +}; +ACME._directory = function(me) { + return me._request({ url: me.directoryUrl, json: true }); +}; +ACME._getNonce = function(me) { + if (me._nonce) { + return new Promise(function(resolve) { + resolve(me._nonce); + return; + }); + } + return me + ._request({ method: 'HEAD', url: me._directoryUrls.newNonce }) + .then(function(resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); +}; +// ACME RFC Section 7.3 Account Creation +/* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } +*/ +ACME._registerAccount = function(me, options) { + if (me.debug) { + console.debug('[acme-v2] accounts.create'); + } + + return ACME._getNonce(me).then(function() { + return new Promise(function(resolve, reject) { + function agree(tosUrl) { + var err; + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = 'E_AGREE_TOS'; + reject(err); + return; + } + + var jwk = me.RSA.exportPublicJwk(options.accountKeypair); + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = ['mailto:' + options.email]; + } + var body = { + termsOfServiceAgreed: tosUrl === me._tos, + onlyReturnExisting: false, + contact: contact + }; + if (options.externalAccount) { + // TODO is this really done by HMAC or is it arbitrary? + body.externalAccountBinding = me.RSA.signJws( + options.externalAccount.secret, + undefined, + { + alg: 'HS256', + kid: options.externalAccount.id, + url: me._directoryUrls.newAccount + }, + Buffer.from(JSON.stringify(jwk)) + ); + } + var payload = JSON.stringify(body); + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: me._directoryUrls.newAccount, + jwk: jwk + }, + Buffer.from(payload) + ); + + delete jws.header; + if (me.debug) { + console.debug('[acme-v2] accounts.create JSON body:'); + } + if (me.debug) { + console.debug(jws); + } + me._nonce = null; + return me + ._request({ + method: 'POST', + url: me._directoryUrls.newAccount, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + var account = resp.body; + + if (2 !== Math.floor(resp.statusCode / 100)) { + throw new Error('account error: ' + JSON.stringify(body)); + } + + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + // the account id url + me._kid = location; + if (me.debug) { + console.debug('[DEBUG] new account location:'); + } + if (me.debug) { + console.debug(location); + } + if (me.debug) { + console.debug(resp.toJSON()); + } + + /* + { + contact: ["mailto:jon@example.com"], + orders: "https://some-url", + status: 'valid' + } + */ + if (!account) { + account = { _emptyResponse: true, key: {} }; + } + // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 + if (!account.key) { + account.key = {}; + } + account.key.kid = me._kid; + return account; + }) + .then(resolve, reject); + } + + if (me.debug) { + console.debug('[acme-v2] agreeToTerms'); + } + if (1 === options.agreeToTerms.length) { + // newer promise API + return Promise.resolve(options.agreeToTerms(me._tos)).then( + agree, + reject + ); + } else if (2 === options.agreeToTerms.length) { + // backwards compat cb API + return options.agreeToTerms(me._tos, function(err, tosUrl) { + if (!err) { + agree(tosUrl); + return; + } + reject(err); + }); + } else { + reject( + new Error( + 'agreeToTerms has incorrect function signature.' + + ' Should be fn(tos) { return Promise; }' + ) + ); + } + }); + }); +}; +/* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } +*/ +ACME._getChallenges = function(me, options, auth) { + if (me.debug) { + console.debug('\n[DEBUG] getChallenges\n'); + } + return me + ._request({ method: 'GET', url: auth, json: true }) + .then(function(resp) { + return resp.body; + }); +}; +ACME._wait = function wait(ms) { + return new Promise(function(resolve) { + setTimeout(resolve, ms || 1100); + }); +}; + +ACME._testChallengeOptions = function() { + var chToken = require('crypto') + .randomBytes(16) + .toString('hex'); + return [ + { + type: 'http-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/0', + token: 'test-' + chToken + '-0' + }, + { + type: 'dns-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/1', + token: 'test-' + chToken + '-1', + _wildcard: true + }, + { + type: 'tls-sni-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/2', + token: 'test-' + chToken + '-2' + }, + { + type: 'tls-alpn-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/3', + token: 'test-' + chToken + '-3' + } + ]; +}; +ACME._testChallenges = function(me, options) { + if (me.skipChallengeTest) { + return Promise.resolve(); + } + + var CHECK_DELAY = 0; + return Promise.all( + options.domains.map(function(identifierValue) { + // TODO we really only need one to pass, not all to pass + var challenges = ACME._testChallengeOptions(); + if (identifierValue.includes('*')) { + challenges = challenges.filter(function(ch) { + return ch._wildcard; + }); + } + + var challenge = ACME._chooseChallenge(options, { + challenges: challenges + }); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + var enabled = options.challengeTypes.join(', ') || 'none'; + var suitable = + challenges + .map(function(r) { + return r.type; + }) + .join(', ') || 'none'; + return Promise.reject( + new Error( + "None of the challenge types that you've enabled ( " + + enabled + + ' )' + + " are suitable for validating the domain you've selected (" + + identifierValue + + ').' + + ' You must enable one of ( ' + + suitable + + ' ).' + ) + ); + } + if ('dns-01' === challenge.type) { + // Give the nameservers a moment to propagate + CHECK_DELAY = 1.5 * 1000; + } + + return Promise.resolve().then(function() { + var results = { + identifier: { + type: 'dns', + value: identifierValue.replace(/^\*\./, '') + }, + challenges: [challenge], + expires: new Date(Date.now() + 60 * 1000).toISOString(), + wildcard: identifierValue.includes('*.') || undefined + }; + var dryrun = true; + var auth = ACME._challengeToAuth( + me, + options, + results, + challenge, + dryrun + ); + return ACME._setChallenge(me, options, auth).then(function() { + return auth; + }); + }); + }) + ).then(function(auths) { + return ACME._wait(CHECK_DELAY).then(function() { + return Promise.all( + auths.map(function(auth) { + return ACME.challengeTests[auth.type](me, auth); + }) + ); + }); + }); +}; +ACME._chooseChallenge = function(options, results) { + // For each of the challenge types that we support + var challenge; + options.challengeTypes.some(function(chType) { + // And for each of the challenge types that are allowed + return results.challenges.some(function(ch) { + // Check to see if there are any matches + if (ch.type === chType) { + challenge = ch; + return true; + } + }); + }); + + return challenge; +}; +ACME._depInit = function(me, options) { + if ('function' !== typeof options.init) { + options.init = function() { + return Promise.resolve(null); + }; + } + // back/forwards-compat + return ACME._wrapCb( + me, + options, + 'init', + { type: '*', request: me._request }, + 'null' + ); +}; +ACME._getZones = function(me, options, dnsHosts) { + if ('function' !== typeof options.getZones) { + options.getZones = function() { + return Promise.resolve([]); + }; + } + var challenge = { type: 'dns-01', dnsHosts: dnsHosts, request: me._request }; + // back/forwards-compat + challenge.challenge = challenge; + return ACME._wrapCb( + me, + options, + 'getZones', + challenge, + 'an array of zone names' + ); +}; + +ACME._wrapCb = function(me, options, _name, stuff, _desc) { + return new Promise(function(resolve, reject) { + try { + if (options[_name].length <= 1) { + return Promise.resolve(options[_name](stuff)) + .then(resolve) + .catch(reject); + } else if (2 === options[_name].length) { + options[_name](stuff, function(err, zonenames) { + if (err) { + reject(err); + } else { + resolve(zonenames); + } + }); + } else { + throw new Error( + 'options.' + _name + ' should accept opts and Promise ' + _desc + ); + } + } catch (e) { + reject(e); + } + }); +}; + +function newZoneRegExp(zonename) { + // (^|\.)example\.com$ + // which matches: + // foo.example.com + // example.com + // but not: + // fooexample.com + return new RegExp('(^|\\.)' + zonename.replace(/\./g, '\\.') + '$'); +} +function pluckZone(zonenames, dnsHost) { + return zonenames + .filter(function(zonename) { + // the only character that needs to be escaped for regex + // and is allowed in a domain name is '.' + return newZoneRegExp(zonename).test(dnsHost); + }) + .sort(function(a, b) { + // longest match first + return b.length - a.length; + })[0]; +} +ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { + // we don't poison the dns cache with our dummy request + var dnsPrefix = ACME.challengePrefixes['dns-01']; + if (dryrun) { + dnsPrefix = dnsPrefix.replace( + 'acme-challenge', + 'greenlock-dryrun-' + + Math.random() + .toString() + .slice(2, 6) + ); + } + + var auth = {}; + + // straight copy from the new order response + // { identifier, status, expires, challenges, wildcard } + Object.keys(request).forEach(function(key) { + auth[key] = request[key]; + }); + + // copy from the challenge we've chosen + // { type, status, url, token } + // (note the duplicate status overwrites the one above, but they should be the same) + Object.keys(challenge).forEach(function(key) { + // don't confused devs with the id url + auth[key] = challenge[key]; + }); + + var zone = pluckZone(options.zonenames || [], auth.identifier.value); + // batteries-included helpers + auth.hostname = auth.identifier.value; + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); + auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; + // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead + auth.challengeUrl = + 'http://' + + auth.identifier.value + + ACME.challengePrefixes['http-01'] + + '/' + + auth.token; + auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); + auth.dnsAuthorization = ACME._toWebsafeBase64( + require('crypto') + .createHash('sha256') + .update(auth.keyAuthorization) + .digest('base64') + ); + if (zone) { + auth.dnsZone = zone; + auth.dnsPrefix = auth.dnsHost + .replace(newZoneRegExp(zone), '') + .replace(/\.$/, ''); + } + + // for backwards/forwards compat + auth.challenge = auth; + auth.request = me._request; + return auth; +}; + +ACME._untame = function(name, wild) { + if (wild) { + name = '*.' + name.replace('*.', ''); + } + return name; +}; + +// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 +ACME._postChallenge = function(me, options, auth) { + var RETRY_INTERVAL = me.retryInterval || 1000; + var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; + var MAX_POLL = me.retryPoll || 8; + var MAX_PEND = me.retryPending || 4; + var count = 0; + + var altname = ACME._untame(auth.identifier.value, auth.wildcard); + + /* + POST /acme/authz/1234 HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "xWCM9lGbIyCgue8di6ueWQ", + "url": "https://example.com/acme/authz/1234" + }), + "payload": base64url({ + "status": "deactivated" + }), + "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" + } + */ + function deactivate() { + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: auth.url, + kid: me._kid + }, + Buffer.from(JSON.stringify({ status: 'deactivated' })) + ); + me._nonce = null; + return me + ._request({ + method: 'POST', + url: auth.url, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + if (me.debug) { + console.debug('[acme-v2.js] deactivate:'); + } + if (me.debug) { + console.debug(resp.headers); + } + if (me.debug) { + console.debug(resp.body); + } + if (me.debug) { + console.debug(); + } + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { + console.debug('deactivate challenge: resp.body:'); + } + if (me.debug) { + console.debug(resp.body); + } + return ACME._wait(DEAUTH_INTERVAL); + }); + } + + function pollStatus() { + if (count >= MAX_POLL) { + return Promise.reject( + new Error( + "[acme-v2] stuck in bad pending/processing state for '" + + altname + + "'" + ) + ); + } + + count += 1; + + if (me.debug) { + console.debug('\n[DEBUG] statusChallenge\n'); + } + return me + ._request({ method: 'GET', url: auth.url, json: true }) + .then(function(resp) { + if ('processing' === resp.body.status) { + if (me.debug) { + console.debug('poll: again'); + } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + } + + // This state should never occur + if ('pending' === resp.body.status) { + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL) + .then(deactivate) + .then(respondToChallenge); + } + if (me.debug) { + console.debug('poll: again'); + } + return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); + } + + if ('valid' === resp.body.status) { + if (me.debug) { + console.debug('poll: valid'); + } + + try { + if (1 === options.removeChallenge.length) { + options.removeChallenge(auth).then(function() {}, function() {}); + } else if (2 === options.removeChallenge.length) { + options.removeChallenge(auth, function(err) { + return err; + }); + } else { + if (!ACME._removeChallengeWarn) { + console.warn( + 'Please update to acme-v2 removeChallenge(options) or removeChallenge(options, cb).' + ); + console.warn( + "The API has been changed for compatibility with all ACME / Let's Encrypt challenge types." + ); + ACME._removeChallengeWarn = true; + } + options.removeChallenge( + auth.request.identifier, + auth.token, + function() {} + ); + } + } catch (e) {} + return resp.body; + } + + var errmsg; + if (!resp.body.status) { + errmsg = + "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + + altname + + "':"; + } else if ('invalid' === resp.body.status) { + errmsg = + "[acme-v2] (E_STATE_INVALID) challenge state for '" + + altname + + "': '" + + resp.body.status + + "'"; + } else { + errmsg = + "[acme-v2] (E_STATE_UKN) challenge state for '" + + altname + + "': '" + + resp.body.status + + "'"; + } + + return Promise.reject(new Error(errmsg)); + }); + } + + function respondToChallenge() { + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: auth.url, + kid: me._kid + }, + Buffer.from(JSON.stringify({})) + ); + me._nonce = null; + return me + ._request({ + method: 'POST', + url: auth.url, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + if (me.debug) { + console.debug('[acme-v2.js] challenge accepted!'); + } + if (me.debug) { + console.debug(resp.headers); + } + if (me.debug) { + console.debug(resp.body); + } + if (me.debug) { + console.debug(); + } + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { + console.debug('respond to challenge: resp.body:'); + } + if (me.debug) { + console.debug(resp.body); + } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + }); + } + + return respondToChallenge(); +}; +ACME._setChallenge = function(me, options, auth) { + return new Promise(function(resolve, reject) { + try { + if (1 === options.setChallenge.length) { + options + .setChallenge(auth) + .then(resolve) + .catch(reject); + } else if (2 === options.setChallenge.length) { + options.setChallenge(auth, function(err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } else { + var challengeCb = function(err) { + if (err) { + reject(err); + } else { + resolve(); + } + }; + // for backwards compat adding extra keys without changing params length + Object.keys(auth).forEach(function(key) { + challengeCb[key] = auth[key]; + }); + if (!ACME._setChallengeWarn) { + console.warn( + 'Please update to acme-v2 setChallenge(options) or setChallenge(options, cb).' + ); + console.warn( + "The API has been changed for compatibility with all ACME / Let's Encrypt challenge types." + ); + ACME._setChallengeWarn = true; + } + options.setChallenge( + auth.identifier.value, + auth.token, + auth.keyAuthorization, + challengeCb + ); + } + } catch (e) { + reject(e); + } + }).then(function() { + // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? + var DELAY = me.setChallengeWait || 500; + if (me.debug) { + console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); + } + return ACME._wait(DELAY); + }); +}; +ACME._finalizeOrder = function(me, options, validatedDomains) { + if (me.debug) { + console.debug('finalizeOrder:'); + } + var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function pollCert() { + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: me._finalize, + kid: me._kid + }, + Buffer.from(payload) + ); + + if (me.debug) { + console.debug('finalize:', me._finalize); + } + me._nonce = null; + return me + ._request({ + method: 'POST', + url: me._finalize, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 + // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" + me._nonce = resp.toJSON().headers['replay-nonce']; + + if (me.debug) { + console.debug('order finalized: resp.body:'); + } + if (me.debug) { + console.debug(resp.body); + } + + if ('valid' === resp.body.status) { + me._expires = resp.body.expires; + me._certificate = resp.body.certificate; + + return resp.body; // return order + } + + if ('processing' === resp.body.status) { + return ACME._wait().then(pollCert); + } + + if (me.debug) { + console.debug( + 'Error: bad status:\n' + JSON.stringify(resp.body, null, 2) + ); + } + + if ('pending' === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'pending'." + + ' Best guess: You have not accepted at least one challenge for each domain:\n' + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + ) + ); + } + + if ('invalid' === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'invalid'." + + ' Best guess: One or more of the domain challenges could not be verified' + + ' (or the order was canceled).\n' + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + ) + ); + } + + if ('ready' === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'ready'." + + " Hmmm... this state shouldn't be possible here. That was the last state." + + " This one should at least be 'processing'.\n" + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + + '\n\n' + + 'Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js' + ) + ); + } + + // The documented ACME error code is "urn:ietf:params:acme:error:badNonce" + // however some servers historically have done their variations, so just check `:badnonce`. + // https://tools.ietf.org/html/rfc8555#section-6.5 + if (resp.body.type.toLowerCase().endsWith(':badnonce')) { + if (me.debug) { + console.debug('[acme-v2] Nonce value unacceptable; retrying with a new nonce'); + } + + // `me._nonce` has been reset earlier in this function, so just call `poll` again. + return pollCert(); + } + + return Promise.reject( + new Error( + "Didn't finalize order: Unhandled status '" + + resp.body.status + + "'." + + ' This is not one of the known statuses...\n' + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + + '\n\n' + + 'Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js' + ) + ); + }); + } + + return pollCert(); +}; +ACME._getCertificate = function(me, options) { + if (me.debug) { + console.debug('[acme-v2] DEBUG get cert 1'); + } + + // Lot's of error checking to inform the user of mistakes + if (!(options.challengeTypes || []).length) { + options.challengeTypes = Object.keys(options.challenges || {}); + } + if (!options.challengeTypes.length) { + options.challengeTypes = [options.challengeType].filter(Boolean); + } + if (options.challengeType) { + options.challengeTypes.sort(function(a, b) { + if (a === options.challengeType) { + return -1; + } + if (b === options.challengeType) { + return 1; + } + return 0; + }); + if (options.challengeType !== options.challengeTypes[0]) { + return Promise.reject( + new Error( + "options.challengeType is '" + + options.challengeType + + "'," + + " which does not exist in the supplied types '" + + options.challengeTypes.join(',') + + "'" + ) + ); + } + } + // TODO check that all challengeTypes are represented in challenges + if (!options.challengeTypes.length) { + return Promise.reject( + new Error( + 'options.challengeTypes (string array) must be specified' + + ' (and in order of preferential priority).' + ) + ); + } + if (!(options.domains && options.domains.length)) { + return Promise.reject( + new Error( + 'options.domains must be a list of string domain names,' + + ' with the first being the subject of the domain (or options.subject must specified).' + ) + ); + } + + // It's just fine if there's no account, we'll go get the key id we need via the public key + if (!me._kid) { + if (options.accountKid || (options.account && options.account.kid)) { + me._kid = options.accountKid || options.account.kid; + } else { + //return Promise.reject(new Error("must include KeyID")); + // This is an idempotent request. It'll return the same account for the same public key. + return ACME._registerAccount(me, options).then(function() { + // start back from the top + return ACME._getCertificate(me, options); + }); + } + } + + var dnsHosts = options.domains.map(function(d) { + return ( + require('crypto') + .randomBytes(2) + .toString('hex') + d + ); + }); + return ACME._depInit(me, options, dnsHosts).then(function(zonenames) { + return ACME._getZones(me, options, dnsHosts).then(function(zonenames) { + options.zonenames = zonenames; + // Do a little dry-run / self-test + return ACME._testChallenges(me, options).then(function() { + if (me.debug) { + console.debug('[acme-v2] certificates.create'); + } + return ACME._getNonce(me).then(function() { + var body = { + // raw wildcard syntax MUST be used here + identifiers: options.domains + .sort(function(a, b) { + // the first in the list will be the subject of the certificate, I believe (and hope) + if (!options.subject) { + return 0; + } + if (options.subject === a) { + return -1; + } + if (options.subject === b) { + return 1; + } + return 0; + }) + .map(function(hostname) { + return { type: 'dns', value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer? + me._kty = + (options.accountKeypair.privateKeyJwk && + options.accountKeypair.privateKeyJwk.kty) || + 'RSA'; + me._alg = 'EC' === me._kty ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled) + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg, + url: me._directoryUrls.newOrder, + kid: me._kid + }, + Buffer.from(payload, 'utf8') + ); + + if (me.debug) { + console.debug('\n[DEBUG] newOrder\n'); + } + me._nonce = null; + return me + ._request({ + method: 'POST', + url: me._directoryUrls.newOrder, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + var setAuths; + var auths = []; + if (me.debug) { + console.debug(location); + } // the account id url + if (me.debug) { + console.debug(resp.toJSON()); + } + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; + + if (!me._authorizations) { + // The documented ACME error code is "urn:ietf:params:acme:error:badNonce" + // however some servers historically have done their variations, so just check `:badnonce`. + // https://tools.ietf.org/html/rfc8555#section-6.5 + if (resp.body.type.toLowerCase().endsWith(':badnonce')) { + if (me.debug) { + console.debug('[acme-v2] Nonce value unacceptable; retrying with a new nonce'); + } + + // `me._nonce` has been reset earlier in this function, so just call `get` again. + return ACME._getCertificate(me, options); + } + + return Promise.reject( + new Error( + "[acme-v2.js] authorizations were not fetched for '" + + options.domains.join() + + "':\n" + + JSON.stringify(resp.body) + ) + ); + } + if (me.debug) { + console.debug('[acme-v2] POST newOrder has authorizations'); + } + setAuths = me._authorizations.slice(0); + + function setNext() { + var authUrl = setAuths.shift(); + if (!authUrl) { + return; + } + + return ACME._getChallenges(me, options, authUrl).then(function( + results + ) { + // var domain = options.domains[i]; // results.identifier.value + + // If it's already valid, we're golden it regardless + if ( + results.challenges.some(function(ch) { + return 'valid' === ch.status; + }) + ) { + return setNext(); + } + + var challenge = ACME._chooseChallenge(options, results); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + return Promise.reject( + new Error( + "Server didn't offer any challenge we can handle for '" + + options.domains.join() + + "'." + ) + ); + } + + var auth = ACME._challengeToAuth( + me, + options, + results, + challenge + ); + auths.push(auth); + return ACME._setChallenge(me, options, auth).then(setNext); + }); + } + + function challengeNext() { + var auth = auths.shift(); + if (!auth) { + return; + } + return ACME._postChallenge(me, options, auth).then( + challengeNext + ); + } + + // First we set every challenge + // Then we ask for each challenge to be checked + // Doing otherwise would potentially cause us to poison our own DNS cache with misses + return setNext() + .then(challengeNext) + .then(function() { + if (me.debug) { + console.debug('[getCertificate] next.then'); + } + var validatedDomains = body.identifiers.map(function(ident) { + return ident.value; + }); + + return ACME._finalizeOrder(me, options, validatedDomains); + }) + .then(function(order) { + if (me.debug) { + console.debug('acme-v2: order was finalized'); + } + return me + ._request({ + method: 'GET', + url: me._certificate, + json: true + }) + .then(function(resp) { + if (me.debug) { + console.debug( + 'acme-v2: csr submitted and cert received:' + ); + } + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain( + ACME.formatPemChain(resp.body || '') + ); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ + var certs = { + expires: order.expires, + identifiers: order.identifiers, + //, authorizations: order.authorizations + cert: certsarr.shift(), + //, privkey: privkeyPem + chain: certsarr.join('\n') + }; + if (me.debug) { + console.debug(certs); + } + return certs; + }); + }); + }); + }); + }); + }); + }); +}; + +ACME.create = function create(me) { + if (!me) { + me = {}; + } + // me.debug = true; + me.challengePrefixes = ACME.challengePrefixes; + me.RSA = me.RSA || require('rsa-compat').RSA; + //me.Keypairs = me.Keypairs || require('keypairs'); + me.request = me.request || require('@root/request'); + me._dig = function(query) { + // TODO use digd.js + return new Promise(function(resolve, reject) { + var dns = require('dns'); + dns.resolveTxt(query.name, function(err, records) { + if (err) { + reject(err); + return; + } + + resolve({ + answer: records.map(function(rr) { + return { + data: rr + }; + }) + }); + }); + }); + }; + me.promisify = + me.promisify || + require('util').promisify /*node v8+*/ || + require('bluebird').promisify /*node v6*/; + + if ('function' !== typeof me.getUserAgentString) { + me.pkg = me.pkg || require('./package.json'); + me.os = me.os || require('os'); + me.process = me.process || require('process'); + me.userAgent = ACME._getUserAgentString(me); + } + + function getRequest(opts) { + if (!opts) { + opts = {}; + } + + return me.request.defaults({ + headers: { + 'User-Agent': + opts.userAgent || me.userAgent || me.getUserAgentString(me) + } + }); + } + + if ('function' !== typeof me._request) { + me._request = me.promisify(getRequest({})); + } + + me.init = function(_directoryUrl) { + if (_directoryUrl) { + _directoryUrl = _directoryUrl.directoryUrl || _directoryUrl; + } + if ('string' === typeof _directoryUrl) { + me.directoryUrl = _directoryUrl; + } + if (!me.directoryUrl) { + me.directoryUrl = + 'https://acme-staging-v02.api.letsencrypt.org/directory'; + console.warn(); + console.warn( + "No ACME `directoryUrl` was specified. Using Let's Encrypt's staging environment as the default, which will issue invalid certs." + ); + console.warn('\t' + me.directoryUrl); + console.warn(); + console.warn( + "To get valid certificates you will need to switch to a production URL. You might like Let's Encrypt v2:" + ); + console.warn('\t' + me.directoryUrl.replace('-staging', '')); + console.warn(); + } + return ACME._directory(me).then(function(resp) { + me._directoryUrls = resp.body; + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + }; + me.accounts = { + create: function(options) { + return ACME._registerAccount(me, options); + } + }; + me.certificates = { + create: function(options) { + return ACME._getCertificate(me, options); + } + }; + return me; +}; + +ACME._toWebsafeBase64 = function(b64) { + return b64 + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +}; diff --git a/package-lock.json b/package-lock.json index 8726a68..fef8f40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,226 +1,46 @@ { - "name": "@root/acme", - "version": "3.1.0", + "name": "acme-v2", + "version": "1.8.0", "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==", - "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.10.0", - "resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.10.0.tgz", - "integrity": "sha512-t8VocY46Mtb0NTsxzyLLf5tsgfw0BXLYVADAyiRdEdqHcvPFGJdjkXNtHVQuSV/FMaC65iTOHVP4E6X8iT3Ikg==", - "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.6.1", - "resolved": "https://registry.npmjs.org/@root/request/-/request-1.6.1.tgz", - "integrity": "sha512-8wrWyeBLRp7T8J36GkT3RODJ6zYmL0/maWlAUD5LOXT28D3TDquUepyYDKYANNA3Gc8R5ZCgf+AXvSTYpJEWwQ==" - }, - "@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 - }, - "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 - } - } + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz", + "integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw==" }, "dotenv": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", + "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==", "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.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "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": { + "eckles": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", + "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "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" + } } } } diff --git a/package.json b/package.json index 5b95704..51b3b9b 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,35 @@ { - "name": "@root/acme", - "version": "3.1.0", - "description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt", - "homepage": "https://rootprojects.org/acme/", - "main": "acme.js", - "browser": { - "./lib/native.js": "./lib/browser.js", - "./lib/node/sha2.js": "./lib/browser/sha2.js", - "./lib/node/http.js": "./lib/browser/http.js", - "./lib/node/client-user-agent.js": "./lib/browser/client-user-agent.js" - }, - "files": [ - "*.js", - "lib", - "bin", - "scripts", - "dist" - ], + "name": "acme-v2", + "version": "1.8.2", + "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": "node.js", "scripts": { - "build": "node_xxx bin/bundle.js", - "lint": "jshint lib bin", - "postinstall": "node scripts/postinstall", - "test": "node server.js", - "start": "node server.js" + "test": "node ./test.js" }, "repository": { "type": "git", - "url": "https://git.rootprojects.org/root/acme.js.git" + "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" }, "keywords": [ - "ACME", "Let's Encrypt", - "EC", - "RSA", - "CSR", - "browser", - "greenlock", - "VanillaJS", - "ZeroSSL" + "ACME", + "v02", + "v2", + "draft-11", + "draft-12", + "free ssl", + "tls", + "automated https", + "letsencrypt" ], - "author": "AJ ONeal (https://coolaj86.com/)", + "author": "AJ ONeal (https://solderjs.com/)", "license": "MPL-2.0", "dependencies": { - "@root/csr": "^0.8.1", - "@root/encoding": "^1.0.1", - "@root/keypairs": "^0.10.0", - "@root/pem": "^1.0.4", - "@root/request": "^1.6.1", - "@root/x509": "^0.7.2" + "@root/request": "^1.3.11", + "rsa-compat": "^2.0.8" }, "devDependencies": { - "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" + "dotenv": "^8.0.0" } } diff --git a/scripts/postinstall b/scripts/postinstall deleted file mode 100755 index ef5e79a..0000000 --- a/scripts/postinstall +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// TODO put postinstall message diff --git a/test.js b/test.js new file mode 100644 index 0000000..b8ff270 --- /dev/null +++ b/test.js @@ -0,0 +1,3 @@ +'use strict'; +require('dotenv').config(); +require('./examples/dns-01-digitalocean.js'); diff --git a/tests/cb.js b/tests/cb.js new file mode 100644 index 0000000..68b343b --- /dev/null +++ b/tests/cb.js @@ -0,0 +1,118 @@ +// 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 new file mode 100644 index 0000000..363dfba --- /dev/null +++ b/tests/compat.js @@ -0,0 +1,106 @@ +// 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/compute-authorization-response.js b/tests/compute-authorization-response.js deleted file mode 100644 index 11263be..0000000 --- a/tests/compute-authorization-response.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -var ACME = require('../'); -var accountKey = require('../fixtures/account.jwk.json').private; - -var authorization = { - identifier: { - type: 'dns', - value: 'example.com' - }, - status: 'pending', - expires: '2018-04-25T00:23:57Z', - challenges: [ - { - type: 'dns-01', - status: 'pending', - url: - 'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755342', - token: 'LZdlUiZ-kWPs6q5WTmQFYQHZKpz9szn2vxEUu0XhyyM' - }, - { - type: 'http-01', - status: 'pending', - url: - 'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755343', - token: '1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU' - } - ] -}; -var expectedChallengeUrl = - 'http://example.com/.well-known/acme-challenge/1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU'; -var expectedKeyAuth = - '1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU.UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs'; -var expectedKeyAuthDigest = 'iQiMcQUDiAeD0TJV1RHJuGnI5D2-PuSpxKz9JqUaZ2M'; -var expectedDnsHost = '_test-challenge.example.com'; - -async function main() { - console.info('\n[Test] computing challenge authorizatin responses'); - var challenges = authorization.challenges.slice(0); - - function next() { - var ch = challenges.shift(); - if (!ch) { - return null; - } - - var hostname = authorization.identifier.value; - return ACME.computeChallenge({ - accountKey: accountKey, - hostname: hostname, - challenge: ch, - dnsPrefix: '_test-challenge' - }) - .then(function (auth) { - if ('dns-01' === ch.type) { - if (auth.keyAuthorizationDigest !== expectedKeyAuthDigest) { - console.error('[keyAuthorizationDigest]'); - console.error(auth.keyAuthorizationDigest); - console.error(expectedKeyAuthDigest); - throw new Error('bad keyAuthDigest'); - } - if (auth.dnsHost !== expectedDnsHost) { - console.error('[dnsHost]'); - console.error(auth.dnsHost); - console.error(expectedDnsHost); - throw new Error('bad dnsHost'); - } - } else if ('http-01' === ch.type) { - if (auth.challengeUrl !== expectedChallengeUrl) { - console.error('[challengeUrl]'); - console.error(auth.challengeUrl); - console.error(expectedChallengeUrl); - throw new Error('bad challengeUrl'); - } - if (auth.challengeUrl !== expectedChallengeUrl) { - console.error('[keyAuthorization]'); - console.error(auth.keyAuthorization); - console.error(expectedKeyAuth); - throw new Error('bad keyAuth'); - } - } else { - throw new Error('bad authorization inputs'); - } - console.info('PASS', hostname, ch.type); - return next(); - }) - .catch(function (err) { - err.message = - 'Error computing ' + - ch.type + - ' for ' + - hostname + - ':' + - err.message; - throw err; - }); - } - - return next(); -} - -module.exports = function () { - return main(authorization) - .then(function () { - console.info('PASS'); - }) - .catch(function (err) { - console.error(err.stack); - process.exit(1); - }); -}; diff --git a/tests/format-pem-chains.js b/tests/fullchain-formats.js similarity index 50% rename from tests/format-pem-chains.js rename to tests/fullchain-formats.js index 64387ea..b265b11 100644 --- a/tests/format-pem-chains.js +++ b/tests/fullchain-formats.js @@ -35,46 +35,55 @@ var tests = [ '----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n' ]; -var ACME = require('../'); +function formatPemChain(str) { + return ( + str + .trim() + .replace(/[\r\n]+/g, '\n') + .replace(/\-\n\-/g, '-\n\n-') + '\n' + ); +} +function splitPemChain(str) { + return str + .trim() + .split(/[\r\n]{2,}/g) + .map(function(str) { + return str + '\n'; + }); +} -module.exports = function () { - console.info('\n[Test] can split and format PEM chain properly'); - - tests.forEach(function (str) { - var actual = ACME.formatPemChain(str); - if (expected !== actual) { - console.error('input: ', JSON.stringify(str)); - console.error('expected:', JSON.stringify(expected)); - console.error('actual: ', JSON.stringify(actual)); - throw new Error('did not pass'); - } - }); - - if ( - '----\nxxxx\nyyyy\n----\n' !== - ACME.formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n') - ) { - throw new Error('Not proper for single cert in chain'); +tests.forEach(function(str) { + var actual = formatPemChain(str); + if (expected !== actual) { + console.error('input: ', JSON.stringify(str)); + console.error('expected:', JSON.stringify(expected)); + console.error('actual: ', JSON.stringify(actual)); + throw new Error('did not pass'); } +}); - if ( - '--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !== - ACME.formatPemChain( - '\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n' - ) - ) { - throw new Error('Not proper for three certs in chain'); +if ( + '----\nxxxx\nyyyy\n----\n' !== + formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n') +) { + throw new Error('Not proper for single cert in chain'); +} + +if ( + '--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !== + formatPemChain( + '\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n' + ) +) { + throw new Error('Not proper for three certs in chain'); +} + +splitPemChain( + '--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' +).forEach(function(str) { + if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) { + throw new Error('bad thingy'); } +}); - ACME.splitPemChain( - '--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' - ).forEach(function (str) { - if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) { - throw new Error('bad thingy'); - } - }); - - console.info('PASS'); - - return Promise.resolve(); -}; +console.info('PASS'); diff --git a/tests/generate-cert-key.js b/tests/generate-cert-key.js deleted file mode 100644 index bc13882..0000000 --- a/tests/generate-cert-key.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -module.exports = async function () { - console.log('[Test] can generate, export, and import key'); - 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' - }); - var jwk = await Keypairs.import({ - pem: pem - }); - ['kty', 'd', 'n', 'e'].forEach(function (k) { - if (!jwk[k] || jwk[k] !== certKeypair.private[k]) { - throw new Error('bad export/import'); - } - }); - //console.log(pem); - console.log('PASS'); -}; - -if (require.main === module) { - module.exports(); -} diff --git a/tests/index.js b/tests/index.js deleted file mode 100644 index 9554809..0000000 --- a/tests/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -async function main() { - await require('./generate-cert-key.js')(); - await require('./format-pem-chains.js')(); - await require('./compute-authorization-response.js')(); - await require('./issue-certificates.js')(); -} - -main(); diff --git a/tests/issue-certificates.js b/tests/issue-certificates.js deleted file mode 100644 index 9dce4c0..0000000 --- a/tests/issue-certificates.js +++ /dev/null @@ -1,255 +0,0 @@ -'use strict'; - -require('dotenv').config(); - -var pkg = require('../package.json'); -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 ecJwk = require('../fixtures/account.jwk.json'); - -// TODO exec npm install --save-dev CHALLENGE_MODULE -if (!process.env.CHALLENGE_OPTIONS) { - console.error( - 'Please create a .env in the format of examples/example.env to run the tests' - ); - process.exit(1); -} - -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; - -module.exports = function () { - console.info('\n[Test] end-to-end issue certificates'); - - var acme = ACME.create({ - // debug: true - maintainerEmail: config.email, - packageAgent: 'test-' + pkg.name + '/' + pkg.version, - notify: function (ev, params) { - console.info( - '\t' + ev, - params.subject || params.altname || params.domain || '', - params.status || '' - ); - if ('error' === ev) { - console.error(params.action || params.type || ''); - console.error(params); - } - } - }); - - 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 (/EC/i.test(accKty)) { - // to test that an existing account gets back data - accountKeypair = ecJwk; - } - var accountKey = accountKeypair.private; - if (config.debug) { - console.info('Account Key Created'); - console.info(JSON.stringify(accountKey, null, 2)); - console.info(); - console.info(); - } - - var account = await acme.accounts.create({ - agreeToTerms: agree, - // TODO detect jwk/pem/der? - accountKey: accountKey, - 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(agreed); - } - 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, - accountKey: accountKey, - 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 () { - console.info('PASS: ECDSA account key with RSA server key'); - // Now try RSA + EC - rnd = random(); - return happyPath('RSA', 'EC', rnd).then(function () { - console.info('PASS: RSA account key with ECDSA server key'); - }); - }) - .then(function () { - console.info('PASS'); - }) - .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/maintainer.js b/tests/maintainer.js deleted file mode 100644 index 7eff010..0000000 --- a/tests/maintainer.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -var native = require('../lib/native.js'); -var crypto = require('crypto'); - -native - ._hashcash({ - alg: 'SHA-256', - nonce: '00', - needle: '0000', - start: 0, - end: 2 - }) - .then(function (hashcash) { - if ('00:76de' !== hashcash) { - throw new Error('hashcash algorthim changed'); - } - console.info('PASS: known hash solves correctly'); - - return native - ._hashcash({ - alg: 'SHA-256', - nonce: '10', - needle: '', - start: 0, - end: 2 - }) - .then(function (hashcash) { - if ('10:00' !== hashcash) { - throw new Error('hashcash algorthim changed'); - } - console.info('PASS: empty hash solves correctly'); - - var now = Date.now(); - var nonce = '20'; - var needle = crypto.randomBytes(3).toString('hex').slice(0, 5); - native - ._hashcash({ - alg: 'SHA-256', - nonce: nonce, - needle: needle, - start: 0, - end: Math.ceil(needle.length / 2) - }) - .then(function (hashcash) { - var later = Date.now(); - var parts = hashcash.split(':'); - var answer = parts[1]; - if (parts[0] !== nonce) { - throw new Error('incorrect nonce'); - } - var haystack = crypto - .createHash('sha256') - .update(Buffer.from(nonce + answer, 'hex')) - .digest() - .slice(0, Math.ceil(needle.length / 2)); - if ( - -1 === haystack.indexOf(Buffer.from(needle, 'hex')) - ) { - throw new Error('incorrect solution'); - } - if (later - now > 2000) { - throw new Error('took too long to solve'); - } - console.info( - 'PASS: rando hash solves correctly (and in good time - %dms)', - later - now - ); - }); - }); - }); diff --git a/tests/promise.js b/tests/promise.js new file mode 100644 index 0000000..90107a5 --- /dev/null +++ b/tests/promise.js @@ -0,0 +1,124 @@ +// 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/utils.js b/utils.js deleted file mode 100644 index 5c0d1bc..0000000 --- a/utils.js +++ /dev/null @@ -1,174 +0,0 @@ -'use strict'; - -var U = module.exports; - -var Keypairs = require('@root/keypairs'); -var UserAgent = require('./lib/node/client-user-agent.js'); - -// Handle nonce, signing, and request altogether -U._jwsRequest = function (me, bigopts) { - return U._getNonce(me).then(function (nonce) { - bigopts.protected.nonce = nonce; - bigopts.protected.url = bigopts.url; - // protected.alg: added by Keypairs.signJws - if (bigopts.protected.jwk) { - bigopts.protected.kid = false; - } else if (!('kid' in bigopts.protected)) { - // protected.kid must be provided according to ACME's interpretation of the spec - // (using the provided URL rather than the Key's Thumbprint as Key ID) - bigopts.protected.kid = bigopts.kid; - } - - // this will shasum the thumbprint the 2nd time - return Keypairs.signJws({ - jwk: bigopts.accountKey, - protected: bigopts.protected, - payload: bigopts.payload - }) - .then(function (jws) { - //#console.debug('[ACME.js] url: ' + bigopts.url + ':'); - //#console.debug(jws); - return U._request(me, { url: bigopts.url, json: jws }); - }) - .catch(function (e) { - if (/badNonce$/.test(e.urn)) { - // retry badNonces - var retryable = bigopts._retries >= 2; - if (!retryable) { - bigopts._retries = (bigopts._retries || 0) + 1; - return U._jwsRequest(me, bigopts); - } - } - throw e; - }); - }); -}; - -U._getNonce = function (me) { - var nonce; - while (true) { - nonce = me._nonces.shift(); - if (!nonce) { - break; - } - if (Date.now() - nonce.createdAt > 15 * 60 * 1000) { - nonce = null; - } else { - break; - } - } - if (nonce) { - return Promise.resolve(nonce.nonce); - } - - // HEAD-as-HEAD ok - return U._request(me, { - method: 'HEAD', - url: me._directoryUrls.newNonce - }).then(function (resp) { - return resp.headers['replay-nonce']; - }); -}; - -// Handle some ACME-specific defaults -U._request = function (me, opts) { - // no-op on browser - var ua = UserAgent.get(me, opts); - - // Note: the required User-Agent string will be set in node, but not browsers - if (!opts.headers) { - opts.headers = {}; - } - - if (ua && !opts.headers['User-Agent']) { - opts.headers['User-Agent'] = ua; - } - if (opts.json) { - opts.headers.Accept = 'application/json'; - if (true !== opts.json) { - opts.body = JSON.stringify(opts.json); - } - if (/*opts.jose ||*/ opts.json.protected) { - opts.headers['Content-Type'] = 'application/jose+json'; - } - } - if (!opts.method) { - opts.method = 'GET'; - if (opts.body) { - opts.method = 'POST'; - } - } - - //console.log('\n[debug] REQUEST'); - //console.log(opts); - return me.__request(opts).then(function (resp) { - if (resp.toJSON) { - resp = resp.toJSON(); - } - if (resp.headers['replay-nonce']) { - U._setNonce(me, resp.headers['replay-nonce']); - } - //console.log('[debug] RESPONSE:'); - //console.log(resp.headers); - //console.log(resp.body); - - var e; - var err; - if (resp.body) { - err = resp.body.error; - e = new Error(''); - if (400 === resp.body.status) { - err = { type: resp.body.type, detail: resp.body.detail }; - } - if (err) { - e.status = resp.body.status; - e.code = 'E_ACME'; - if (e.status) { - e.message = '[' + e.status + '] '; - } - e.detail = err.detail; - e.message += err.detail || JSON.stringify(err); - e.urn = err.type; - e.uri = resp.body.url; - e._rawError = err; - e._rawBody = resp.body; - throw e; - } - } - - return resp; - }); -}; - -U._setNonce = function (me, nonce) { - me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); -}; - -U._importKeypair = function (key) { - var p; - var pub; - - if (key && key.kty) { - // nix the browser jwk extras - key.key_ops = undefined; - key.ext = undefined; - pub = Keypairs.neuter({ jwk: key }); - p = Promise.resolve({ - private: key, - public: pub - }); - } else if ('string' === typeof key) { - p = Keypairs.import({ pem: key }); - } else { - throw new Error('no private key given'); - } - - return p.then(function (pair) { - if (pair.public.kid) { - pair = JSON.parse(JSON.stringify(pair)); - delete pair.public.kid; - delete pair.private.kid; - } - return pair; - }); -}; diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index d3d2b12..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,20 +0,0 @@ -'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'] - } -};