Browse Source

merge unrelated v2 (historical) and v3 (new from scratch)

v3
AJ ONeal 5 years ago
parent
commit
ad42d34587
  1. 4
      .gitignore
  2. 2
      .prettierrc
  3. 53
      CHANGELOG.md
  4. 2
      LICENSE
  5. 460
      README.md
  6. 1810
      acme.js
  7. 60
      bin/bundle.js
  8. 50
      browser.js
  9. 94
      compat.js
  10. 340
      examples/app.js
  11. 69
      examples/dns-01-digitalocean.js
  12. 9
      examples/example.env
  13. 5
      examples/genkeypair.js
  14. 13
      examples/http-server.js
  15. 20
      examples/https-server.js
  16. 231
      examples/index.html
  17. 174
      examples/server.js
  18. 17
      fixtures/account.jwk.json
  19. 13
      fixtures/account.registration.json
  20. 20
      fixtures/server.jwk.json
  21. 32
      lib/browser/http.js
  22. 13
      lib/browser/sha2.js
  23. 19
      lib/node/http.js
  24. 17
      lib/node/sha2.js
  25. 33
      native.js
  26. 229
      package-lock.json
  27. 61
      package.json
  28. 22
      scripts/postinstall
  29. 3
      test.js
  30. 118
      tests/cb.js
  31. 106
      tests/compat.js
  32. 15
      tests/generate-cert-key.js
  33. 225
      tests/index.js
  34. 124
      tests/promise.js
  35. 20
      webpack.config.js

4
.gitignore

@ -1,4 +1,7 @@
.env
*.gz
.*.sw*
.ignore
*.pem
@ -14,4 +17,5 @@ coverage
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

2
.prettierrc

@ -2,7 +2,7 @@
"bracketSpacing": true,
"printWidth": 80,
"singleQuote": true,
"tabWidth": 2,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": true
}

53
CHANGELOG.md

@ -0,0 +1,53 @@
# Changelog
- v3 (Oct 2019)
- Add POST-as-GET for Let's Encrypt v2 release 2 (ACME / RFC 8555)
- Jump to v3 for parity with Greenlock
- Merge browser and node.js versions in one
- Drop all backwards-compat complexity
- Move to zero-external deps, using @root packages only
- v1.8
- more transitional prepwork for new v2 API
- support newer (simpler) dns-01 and http-01 libraries
- v1.5
- perform full test challenge first (even before nonce)
- v1.3
- Use node RSA keygen by default
- No non-optional external deps!
- v1.2
- fix some API out-of-specness
- doc some magic numbers (status)
- updated deps
- v1.1.0
- reduce dependencies (use lightweight @coolaj86/request instead of request)
- v1.0.5 - cleanup logging
- v1.0.4 - v6- compat use `promisify` from node's util or bluebird
- v1.0.3 - documentation cleanup
- v1.0.2
- use `options.contact` to provide raw contact array
- made `options.email` optional
- file cleanup
- v1.0.1
- Compat API is ready for use
- Eliminate debug logging
- Apr 10, 2018 - tested backwards-compatibility using greenlock.js
- Apr 5, 2018 - export http and dns challenge tests
- Apr 5, 2018 - test http and dns challenges (success and failure)
- Apr 5, 2018 - test subdomains and its wildcard
- Apr 5, 2018 - test two subdomains
- Apr 5, 2018 - test wildcard
- Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js)
- Mar 21, 2018 - _mostly_ matches le-acme-core.js API
- Mar 21, 2018 - can now accept values (not hard coded)
- Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded)
- Mar 20, 2018 - download certificate
- Mar 20, 2018 - poll for status
- Mar 20, 2018 - finalize order (submit csr)
- Mar 20, 2018 - generate domain keypair
- Mar 20, 2018 - respond to challenges
- Mar 16, 2018 - get challenges
- Mar 16, 2018 - new order
- Mar 15, 2018 - create account
- Mar 15, 2018 - generate account keypair
- Mar 15, 2018 - get nonce
- Mar 15, 2018 - get directory

2
LICENSE

@ -1,4 +1,4 @@
Copyright 2018 AJ ONeal
Copyright 2015-2019 AJ ONeal
Mozilla Public License Version 2.0
==================================

460
README.md

@ -1,239 +1,355 @@
# ACME.js v3 on its way (Nov 1st, 2019)
# [ACME.js](https://git.rootprojects.org/root/acme.js) v3
ACME.js v3 is in private beta and will be available by Nov 1st.
| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains)
Follow the updates on the [campaign page](https://indiegogo.com/at/greenlock),
and contribute to support the project and get beta access now.
Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers
| **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2))
| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js)
| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js)
Lightweight. Fast. Modern Crypto. Zero external dependecies.
# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project
# Features
A **Zero (External) Dependency**\* library for building
Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates.
| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |
The primary goal of this library is to make it easy to
get Accounts and Certificates through Let's Encrypt.
# Features
- [x] Let's Encrypt v2 / ACME RFC 8555 (November 2019)
- [x] POST-as-GET support
- [x] Secure support for EC and RSA for account and server keys
- [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations
- [ ] (in-progress) StartTLS Everywhere™
- [x] Supports International Domain Names (i.e. `.中国`)
- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js)
- [x] **http-01** for single or multiple domains per certificate
- [x] **dns-01** for wildcards, localhost, private networks, etc
- [x] VanillaJS, Zero External Dependencies
- [x] Safe, Efficient, Maintained
- [x] Node.js\* (v6+)
- [x] WebPack
- [x] Online Demo
- See https://greenlock.domains
- [x] Let's Encrypt™ v2 / ACME Draft 12
- [ ] (in-progress) Let's Encrypt™ v2.1 / ACME Draft 18
- [ ] (in-progress) StartTLS Everywhere™
- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js)
- [x] **http-01** for single or multiple domains per certificate
- [x] **dns-01** for wildcards, localhost, private networks, etc
- [x] VanillaJS
- [x] Zero External Dependencies
- [x] Safe, Efficient, Maintained
- [x] Works in Node v6+
- [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains))
\* Although we use `async/await` in the examples, the code is written in CommonJS,
with Promises, so you can use it in Node.js and Browsers without transpiling.
\* <small>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.</small>
# Want Quick and Easy?
## Looking for Quick 'n' Easy&trade;?
ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers.
If you want something that's more "batteries included" give
[greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
a try.
If you're looking for maximum convenience, try
[Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js).
- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
- <https://git.rootprojects.org/root/greenlock-express.js>
## v1.7+: Transitional v2 Support
# Online Demos
By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18).
- Greenlock for the Web <https://greenlock.domains>
- ACME.js Demo <https://rootprojects.org/acme/>
Although the draft 18 changes themselves don't requiring breaking the API,
we've been keeping backwards compatibility for a long time and the API has become messy.
We expect that our hosted versions will meet all of yours needs.
If they don't, please open an issue to let us know why.
We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify**
the code with a fresh new release.
We'd much rather improve the app than have a hundred different versions running in the wild.
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
As of **v1.7** we started adding **transitional support** for the **next major version**, v2.0 of acme-v2.js.
We've been really good about backwards compatibility for
# QuickStart
## Recommended Example
To make it easy to generate, encode, and decode keys and certificates,
ACME.js uses [Keypairs.js](https://git.rootprojects.org/root/keypairs.js)
and [CSR.js](https://git.rootprojects.org/root/csr.js)
Due to the upcoming changes we've removed the old documentation.
## Node.js
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)
```js
var ACME = require('@root/acme');
```
- [examples/dns-01-digitalocean.js](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
## WebPack
That's not exactly the new API, but it's close.
```html
<meta charset="UTF-8" />
```
## Let's Encrypt v02 Directory URLs
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
```js
var ACME = require('@root/acme');
```
# Production URL
https://acme-v02.api.letsencrypt.org/directory
## Vanilla JS
```html
<meta charset="UTF-8" />
```
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
`acme.js`
```html
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.js"></script>
```
# Staging URL
https://acme-staging-v02.api.letsencrypt.org/directory
`acme.min.js`
```html
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.min.js"></script>
```
<!--
## How to build ACME clients
Use
As this is intended to build ACME clients, there is not a simple 2-line example
(and if you want that, see [greenlock-express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js)).
```js
var ACME = window['@root/acme'];
```
I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example:
## Examples
```bash
node examples/cli.js
You can see `tests/index.js`, `examples/index.html`, `examples/app.js` in the repo for full example usage.
### Emails: Maintainer vs Subscriber vs Customer
- `maintainerEmail` should be the email address of the **author of the code**.
This person will receive critical security and API change notifications.
- `subscriberEmail` should be the email of the **admin of the hosting service**.
This person agrees to the Let's Encrypt Terms of Service and will be notified
when a certificate fails to renew.
- `customerEmail` should be the email of individual who owns the domain.
This is optional (not currently implemented).
Generally speaking **YOU** are the _maintainer_ and you **or your employer** is the _subscriber_.
If you (or your employer) is running any type of service
you **SHOULD NOT** pass the _customer_ email as the subscriber email.
If you are not running a service (you may be building a CLI, for example),
then you should prompt the user for their email address, and they are the subscriber.
### Instantiate ACME.js
Although built for Let's Encrypt, ACME.js will work with any server
that supports draft-15 of the ACME spec (includes POST-as-GET support).
The `init()` method takes a _directory url_ and initializes internal state according to its response.
```js
var acme = ACME.create({
maintainerEmail: 'jon@example.com'
});
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(
function() {
// Ready to use, show page
$('body').hidden = false;
}
);
```
The example cli has the following prompts:
### Create ACME Account with Let's Encrypt
ACME Accounts are key and device based, with an email address as a backup identifier.
A public account key must be registered before an SSL certificate can be requested.
```js
var accountPrivateKey;
var account;
Keypairs.generate({ kty: 'EC' }).then(function(pair) {
accountPrivateKey = pair.private;
return acme.accounts
.create({
agreeToTerms: function(tos) {
if (
window.confirm(
"Do you agree to the ACME.js and Let's Encrypt Terms of Service?"
)
) {
return Promise.resolve(tos);
}
},
accountKeypair: { privateKeyJwk: pair.private },
subscriberEmail: $('.js-email-input').value
})
.then(function(_account) {
account = _account;
});
});
```
What web address(es) would you like to get certificates for? (ex: example.com,*.example.com)
What challenge will you be testing today? http-01 or dns-01? [http-01]
What email should we use? (optional)
What API style would you like to test? v1-compat or promise? [v1-compat]
Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
### Generate a Certificate Private Key
echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
```js
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
Then hit the 'any' key to continue...
// This should be saved as `privkey.pem`
console.log(pem);
```
When you've completed the challenge you can hit a key to continue the process.
### Generate a CSR
If you place the certificate you receive back in `tests/fullchain.pem`
you can then test it with `examples/https-server.js`.
The easiest way to generate a Certificate Signing Request will be either with `openssl` or with `@root/CSR`.
```js
var CSR = require('@root/csr');
var Enc = require('@root/encoding');
// 'subject' should be first in list
var sortedDomains = ['example.com', 'www.example.com'];
var csr = await CSR.csr({
jwk: certKeypair.private,
domains: sortedDomains,
encoding: 'der'
}).then(function(der) {
return Enc.bufToUrlBase64(der);
});
```
examples/cli.js
examples/genkeypair.js
tests/compat.js
examples/https-server.js
examples/http-server.js
### Get Free 90-day SSL Certificate
Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key,
the names of domains to be secured, and a distinctly separate server private key.
A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record)
is a required part of the process, which requires `set` and `remove` callbacks/promises.
```js
var certinfo = await acme.certificates.create({
agreeToTerms: function(tos) {
return tos;
},
account: account,
accountKeypair: { privateKeyJwk: accountPrivateKey },
csr: csr,
domains: sortedDomains,
challenges: challenges, // must be implemented
customerEmail: null,
skipChallengeTests: false,
skipDryRun: false
});
console.log('Got SSL Certificate:');
console.log(results.expires);
// This should be saved as `fullchain.pem`
console.log([results.cert, results.chain].join('\n'));
```
-->
### Example "Challenge" Implementation
## API
Typically here you're just presenting some sort of dialog to the user to ask them to
upload a file or set a DNS record.
It may be possible to do something fancy like using OAuth2 to login to Google Domanis
to set a DNS address, etc, but it seems like that sort of fanciness is probably best
reserved for server-side plugins.
```js
var challenges = {
'http-01': {
set: function(opts) {
console.info('http-01 set challenge:');
console.info(opts.challengeUrl);
console.info(opts.keyAuthorization);
while (
!window.confirm('Upload the challenge file before continuing.')
) {}
return Promise.resolve();
},
remove: function(opts) {
console.log('http-01 remove challenge:', opts.challengeUrl);
return Promise.resolve();
}
}
};
```
Status: Small, but breaking changes coming in v2
# IDN - International Domain Names
This API is a simple evolution of le-acme-core,
but tries to provide a better mapping to the new draft 11 APIs.
Convert domain names to `punycode` before creating the certificate:
```js
var ACME = require('acme-v2').ACME.create({
// used for overriding the default user-agent
userAgent: 'My custom UA String',
getUserAgentString: function(deps) {
return 'My custom UA String';
},
var punycode = require('punycode');
// 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
acme.certificates.create({
// ...
domains: ['example.com', 'www.example.com'].map(function(name) {
return punycode.toASCII(name);
})
});
```
// Discover Directory URLs
ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}>
The punycode library itself is lightweight and dependency-free.
It is available both in node and for browsers.
// Accounts
ACME.accounts.create(options); // returns Promise<regr> registration data
# Testing
options = {
email: '<email>', // valid email (server checks MX records)
accountKeypair: {
// privateKeyPem or privateKeyJwt
privateKeyPem: '<ASCII PEM>'
},
agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back
};
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.
// Registration
ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }>
You'll also need a `.env` that looks something like the one in `examples/example.env`:
options = {
domainKeypair: {
privateKeyPem: '<ASCII PEM>'
},
accountKeypair: {
privateKeyPem: '<ASCII PEM>'
},
domains: ['example.com'],
```bash
ENV=DEV
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
BASE_DOMAIN=test.example.com
CHALLENGE_TYPE=dns-01
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
```
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
};
For example:
```bash
# Get the repo and change directories into it
git clone https://git.rootprojects.org/root/bluecrypt-acme.js
pushd bluecrypt-acme.js/
# Install the challenge plugin you'll use for the tests
npm install --save-dev acme-dns-01-digitalocean
# Copy the sample .env file
rsync -av examples/example.env .env
# Edit the config file to use a domain in your account, and your API token
#vim .env
code .env
# Run the tests
node tests/index.js
```
# 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) |
# Developing
You can see `<script>` tags in the `index.html` in the repo, which references the original
source files.
Join `@rootprojects` `#general` on [Keybase](https://keybase.io) if you'd like to chat with us.
# Commercial Support
We have both commercial support and commercial licensing available.
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
Enterprise, and Internal installations, integrations, and deployments.
We also offer consulting for all-things-ACME and Let's Encrypt.
# Legal &amp; Rules of the Road
Greenlock&trade; 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/bluecrypt-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) |
MPL-2.0 |
[Terms of Use](https://therootcompany.com/legal/#terms) |
[Privacy Policy](https://therootcompany.com/legal/#privacy)

1810
acme.js

File diff suppressed because it is too large

60
bin/bundle.js

@ -0,0 +1,60 @@
#!/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)
);
})();

50
browser.js

@ -0,0 +1,50 @@
'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 new 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 new me.request({
url: me._baseUrl + '/api/http?url=' + url
}).then(function(resp) {
return resp.body;
});
};

94
compat.js

@ -1,94 +0,0 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
/* global Promise */
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;

340
examples/app.js

@ -0,0 +1,340 @@
/*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);
})();

69
examples/dns-01-digitalocean.js

@ -1,69 +0,0 @@
(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);

9
examples/example.env

@ -1,3 +1,6 @@
ACME_EMAIL=jon.doe@gmail.com
ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com
DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ENV=DEV
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
BASE_DOMAIN=test.example.com
CHALLENGE_TYPE=dns-01
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'

5
examples/genkeypair.js

@ -11,7 +11,10 @@ if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) {
var privkeyPem = RSA.exportPrivatePem(keypair);
console.log(privkeyPem);
fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem);
fs.writeFileSync(
__dirname + '/../tests/account.privkey.pem',
privkeyPem
);
});
}

13
examples/http-server.js

@ -1,13 +0,0 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
var http = require('http');
var express = require('express');
var server = http
.createServer(express.static('../tests'))
.listen(80, function() {
console.log('Listening on', this.address());
});

20
examples/https-server.js

@ -1,20 +0,0 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
var https = require('https');
var server = https
.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());
});

231
examples/index.html

@ -0,0 +1,231 @@
<html>
<head>
<title>Bluecrypt ACME - A Root Project</title>
<meta charset="UTF-8" />
<style>
textarea {
width: 42em;
height: 10em;
}
/* need to word wrap the binary no space der */
.js-der-public,
.js-der-private {
white-space: pre-wrap; /* CSS3 */
white-space: -moz-pre-wrap; /* Firefox */
white-space: -pre-wrap; /* Opera <7 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* IE */
}
</style>
</head>
<body>
<h1>
@bluecrypt/acme: Let's&nbsp;Encrypt&nbsp;for&nbsp;the&nbsp;Browser
</h1>
<p>
This is intended to be explored with your JavaScript console open.
</p>
<pre><code>&lt;script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.js">https://rootprojects.org/acme/bluecrypt-acme.js</a>"&gt;&lt;/script&gt;</code></pre>
<pre><code>&lt;script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.min.js">https://rootprojects.org/acme/bluecrypt-acme.min.js</a>"&gt;&lt;/script&gt;</code></pre>
<a href="https://git.rootprojects.org/root/bluecrypt-acme.js"
>Documentation</a
>
<h2>1. Keypair Generation</h2>
<form class="js-keygen">
<p>Key Type:</p>
<div>
<input type="radio" id="-ktyEC" name="kty" value="EC" checked />
<label for="-ktyEC">ECDSA</label>
<input type="radio" id="-ktyRSA" name="kty" value="RSA" />
<label for="-ktyRSA">RSA</label>
</div>
<div class="js-ec-opts">
<p>EC Options:</p>
<label for="-crv2"
><input
type="radio"
id="-crv2"
name="ec-crv"
value="P-256"
checked
/>P-256</label
>
<label for="-crv3"
><input
type="radio"
id="-crv3"
name="ec-crv"
value="P-384"
/>P-384</label
>
<!-- label for="-crv5"><input type="radio" id="-crv5"
name="ec-crv" value="P-521">P-521</label -->
</div>
<div class="js-rsa-opts" hidden>
<p>RSA Options:</p>
<label for="-modlen2"
><input
type="radio"
id="-modlen2"
name="rsa-len"
value="2048"
checked
/>2048</label
>
<label for="-modlen3"
><input
type="radio"
id="-modlen3"
name="rsa-len"
value="3072"
/>3072</label
>
<label for="-modlen5"
><input
type="radio"
id="-modlen5"
name="rsa-len"
value="4096"
/>4096</label
>
</div>
<button class="js-generate" hidden>Generate</button>
</form>
<h2>2. ACME Account</h2>
<form class="js-acme-account">
<label for="-acmeEmail">Email:</label>
<input
class="js-email"
type="email"
id="-acmeEmail"
value="john.doe@gmail.com"
/>
<br />
<label for="-acmeTos"
><input
class="js-tos"
name="tos"
type="checkbox"
id="-acmeTos"
checked
/>
Agree to Let's Encrypt Terms of Service</label
>
<br />
<button class="js-create-account" hidden>Create Account</button>
</form>
<h2>3. (optional) Certificate Signing Request</h2>
<form class="js-csr">
<label for="-acmeDomains">Domains:</label>
<input
class="js-domains"
type="text"
id="-acmeDomains"
value="example.com www.example.com"
/>
<br />
<button class="js-create-csr" hidden>Create CSR</button>
</form>
<h2>4. ACME Certificate Order</h2>
<form class="js-acme-order">
Challenge type:
<label for="-http01"
><input
type="radio"
id="-http01"
name="acme-challenge-type"
value="http-01"
checked
/>http-01</label
>
<label for="-dns01"
><input
type="radio"
id="-dns01"
name="acme-challenge-type"
value="dns-01"
/>dns-01</label
>
<br />
<label for="-skipDryrun"
><input
class="js-skip-dryrun"
name="skip-dryrun"
type="checkbox"
id="-skipDryrun"
checked
/>
Skip dry-run challenge</label
>
<br />
<button class="js-create-order" hidden>Create Order</button>
</form>
<div class="js-loading" hidden>Loading</div>
<details class="js-toc-jwk" hidden>
<summary>JWK Keypair</summary>
<pre><code class="js-jwk">&nbsp;</code></pre>
</details>
<details class="js-toc-der-private" hidden>
<summary>DER Private Binary</summary>
<pre><code class="js-der-private">&nbsp;</code></pre>
</details>
<details class="js-toc-der-public" hidden>
<summary>DER Public Binary</summary>
<pre><code class="js-der-public">&nbsp;</code></pre>
</details>
<details class="js-toc-pem js-toc-pem-pkcs1-private" hidden>
<summary>PEM Private (base64-encoded PKCS1 DER)</summary>
<pre><code class="js-input-pem-pkcs1-private" ></code></pre>
</details>
<details class="js-toc-pem js-toc-pem-sec1-private" hidden>
<summary>PEM Private (base64-encoded SEC1 DER)</summary>
<pre><code class="js-input-pem-sec1-private" ></code></pre>
</details>
<details class="js-toc-pem js-toc-pem-pkcs8-private" hidden>
<summary>PEM Private (base64-encoded PKCS8 DER)</summary>
<pre><code class="js-input-pem-pkcs8-private" ></code></pre>
</details>
<details class="js-toc-pem js-toc-pem-pkcs1-public" hidden>
<summary>PEM Public (base64-encoded PKCS1 DER)</summary>
<pre><code class="js-input-pem-pkcs1-public" ></code></pre>
</details>
<details class="js-toc-pem js-toc-pem-spki-public" hidden>
<summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary>
<pre><code class="js-input-pem-spki-public" ></code></pre>
</details>
<details class="js-toc-acme-account-response" hidden>
<summary>ACME Account Request</summary>
<pre><code class="js-acme-account-response">&nbsp;</code></pre>
</details>
<details class="js-toc-acme-order-response" hidden>
<summary>ACME Order Response</summary>
<pre><code class="js-acme-order-response">&nbsp;</code></pre>
</details>
<br />
<p>
Bluecrypt&trade; is 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.
</p>
<br />
<footer>
View (git) source
<a href="https://git.rootprojects.org/root/bluecrypt-acme.js"
>@bluecrypt/acme</a
>
</footer>
<script src="./app.js"></script>
<!-- script src="../dist/acme.js"></script -->
<!-- script src="../dist/app.js"></script -->
</body>
</html>

174
examples/server.js

@ -0,0 +1,174 @@
'use strict';
var crypto = require('crypto');
//var dnsjs = require('dns-suite');
var dig = require('dig.js/dns-request');
var request = require('util').promisify(require('@root/request'));
var express = require('express');
var app = express();
var nameservers = require('dns').getServers();
var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length;
var nameserver = nameservers[index];
app.use('/', express.static(__dirname));
app.use('/api', express.json());
app.get('/api/dns/:domain', function(req, res, next) {
var domain = req.params.domain;
var casedDomain = domain
.toLowerCase()
.split('')
.map(function(ch) {
// dns0x20 takes advantage of the fact that the binary operation for toUpperCase is
// ch = ch | 0x20;
return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase();
})
.join('');
var typ = req.query.type;
var query = {
header: {
id: crypto.randomBytes(2).readUInt16BE(0),
qr: 0,
opcode: 0,
aa: 0, // Authoritative-Only
tc: 0, // NA
rd: 1, // Recurse
ra: 0, // NA
rcode: 0 // NA
},
question: [
{
name: casedDomain,
//, type: typ || 'A'
typeName: typ || 'A',
className: 'IN'
}
]
};
var opts = {
onError: function(err) {
next(err);
},
onMessage: function(packet) {
var fail0x20;
if (packet.id !== query.id) {
console.error(
"[SECURITY] ignoring packet for '" +
packet.question[0].name +
"' due to mismatched id"
);
console.error(packet);
return;
}
packet.question.forEach(function(q) {
// if (-1 === q.name.lastIndexOf(cli.casedQuery))
if (q.name !== casedDomain) {
fail0x20 = q.name;
}
});
['question', 'answer', 'authority', 'additional'].forEach(function(
group
) {
(packet[group] || []).forEach(function(a) {
var an = a.name;
var i = domain
.toLowerCase()
.lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM
var j = a.name
.toLowerCase()
.lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM
// it's important to note that these should only relpace changes in casing that we expected
// any abnormalities should be left intact to go "huh?" about
// TODO detect abnormalities?
if (-1 !== i) {
// "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4))
a.name = a.name.replace(
casedDomain.substr(i),
domain.substr(i)
);
} else if (-1 !== j) {
// "www.example.com".replace("EXamPLE.cOm", "example.com")
a.name =
a.name.substr(0, j) +
a.name.substr(j).replace(casedDomain, domain);
}
// NOTE: right now this assumes that anything matching the query matches all the way to the end
// it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly
// (but I don't think it should need to)
if (a.name.length !== an.length) {
console.error(
"[ERROR] question / answer mismatch: '" +
an +
"' != '" +
a.length +
"'"
);
console.error(a);
}
});
});
if (fail0x20) {
console.warn(
";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" +
casedDomain +
"' but got response for '" +
fail0x20 +
"'."
);
return;
}
res.send({
header: packet.header,
question: packet.question,
answer: packet.answer,
authority: packet.authority,
additional: packet.additional,
edns_options: packet.edns_options
});
},
onListening: function() {},
onSent: function(/*res*/) {},
onTimeout: function(res) {
console.error('dns timeout:', res);
next(new Error('DNS timeout - no response'));
},
onClose: function() {},
//, mdns: cli.mdns
nameserver: nameserver,
port: 53,
timeout: 2000
};
dig.resolveJson(query, opts);
});
app.get('/api/http', function(req, res) {
var url = req.query.url;
return request({ method: 'GET', url: url }).then(function(resp) {
res.send(resp.body);
});
});
app.get('/api/_acme_api_', function(req, res) {
res.send({ success: true });
});
module.exports = app;
if (require.main === module) {
// curl -L http://localhost:3000/api/dns/example.com?type=A
console.info('Listening on localhost:3000');
app.listen(3000);
console.info('Try this:');
console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'");
console.info(
"\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"
);
console.info(
"\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'"
);
}

17
fixtures/account.jwk.json

@ -0,0 +1,17 @@
{
"private": {
"kty": "EC",
"crv": "P-256",
"d": "HB1OvdHfLnIy2mYYO9cLU4BqP36CeyS8OsDf3OnYP-M",
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs"
},
"public": {
"kty": "EC",
"crv": "P-256",
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs"
}
}

13
fixtures/account.registration.json

@ -0,0 +1,13 @@
{
"key": {
"kty": "EC",
"crv": "P-256",
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
"kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11265299"
},
"contact": [],
"initialIp": "66.219.236.169",
"createdAt": "2019-10-04T22:54:28.569489074Z",
"status": "valid"
}

20
fixtures/server.jwk.json

@ -0,0 +1,20 @@
{
"private": {
"kty": "RSA",
"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw",
"e": "AQAB",
"d": "YCzN9yVr4Jw5D_UK7WEMuzGUcMAZZs-TQFgY4UK7Ovbj18_QQrhKElb6Zfhepcf1HUYkO6PVjpuZ1tEl9hWgVcFa781AROyvSj04beiaVMDeSCCwjgW3MM3w6olnxTOUDaBMl9NNiqq0v9riDImkQbAQbe3To-KAH2ig4AMNlSZJAhmI2zAMiJhQE_pAcCxc-bQ5oNO-WSU0GRHWdMJSXp9mFgoBhVPDYGW-dmnoFzuNWssxlSqGXY-8a2YOuiunK6XM5_80c1eQqmy-k1InUIViR_wljskc8UiH6xa8BCznZYacgSz4PnvKsiKWKQQ1eliIucV3MC6BzMD3N8EWqQ",
"p": "8NUtOIglu0dvDGmEB7QC5eC02Y2jZKnoxHSPKMAEPxQ0131_2aL49IzADWoTvae3NBPzU7ol3RwJo_GvS967OysfOr6Od699p1FSLwLfK89aql7_uVPJh4Q43H-W_NtRHKUkv0OmkDiwa4WqBQTVfREdPQ3NJT7vIY-cqH_AMRc",
"q": "xZNIl9NRl3b0_V8Y-7_6_foIu9Sx5ILv2XV7WONDx2jp4vuT7byLm1UWdYPBbxLyd5TAvWqtyvaRtVNyplrD0PyyPK3NxqVJde0uzScAU-bf25DeK30V22Xo7IEZiPZoizrjtzGnS6VVNJmZ-Ictz3xmWIudw5d5XDH12fFRlmU",
"dp": "F1Ld9UqiNNf_NjmF0uUpHrA7c5JXD6mw5E3Ri4XFI4LGd1QtLJuu9qgm9WWfkc-LW5zPBP3TKu3LNThz3KougdV0SdEopQi255xllC34BRso0bUvmPg3XUt94kTtD4ICAf8wZuGbYP5Mf61LQP8t2dXtefs7Me89Y4ewCVWN_HM",
"dq": "oPuT35lgVtCnZ7dPrPjNMpnC-gCg_fcuJPqTiWaLuHQkdjzUWJYTDnqy9Qdo2e8PPx4mOXAtsT1clekrdp5oBOWQ-N4I172fcIXUZ3ZKzxJD_iw4yih-YajUs7exLabQoflWx9KeZIWPOm-ZRCYoznGnFqiT4GWQje1rS6xT9P0",
"qi": "aXkK-w4Npw0BpUEzQ1PURVGm5y5cKIdd-CfEYwub19rronI9EEvuQHoqR7ODtZ_mlIIffHmHaM3ug50fJDB9QDOG4Ioc5S4YxVURT58Ps8at-dQAAP1UgSlV3vhXh4WZRaDECUI_728U3fxQqH78bJsy81mU8MtGU8LR_eTMXx8",
"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk"
},
"public": {
"kty": "RSA",
"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw",
"e": "AQAB",
"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk"
}
}

32
lib/browser/http.js

@ -0,0 +1,32 @@
'use strict';
var http = module.exports;
http.request = function(opts) {
return window.fetch(opts.url, opts).then(function(resp) {
var headers = {};
var result = {
statusCode: resp.status,
headers: headers,
toJSON: function() {
return this;
}
};
Array.from(resp.headers.entries()).forEach(function(h) {
headers[h[0]] = h[1];
});
if (!headers['content-type']) {
return result;
}
if (/json/.test(headers['content-type'])) {
return resp.json().then(function(json) {
result.body = json;
return result;
});
}
return resp.text().then(function(txt) {
result.body = txt;
return result;
});
});
};

13
lib/browser/sha2.js

@ -0,0 +1,13 @@
'use strict';
var sha2 = module.exports;
var encoder = new TextEncoder();
sha2.sum = function(alg, str) {
var data = str;
if ('string' === typeof data) {
data = encoder.encode(str);
}
var sha = 'SHA-' + String(alg).replace(/^sha-?/i, '');
return window.crypto.subtle.digest(sha, data);
};

19
lib/node/http.js

@ -0,0 +1,19 @@
'use strict';
var http = module.exports;
var promisify = require('util').promisify;
var request = promisify(require('@root/request'));
http.request = function(opts) {
if (!opts.headers) {
opts.headers = {};
}
if (
!Object.keys(opts.headers).some(function(key) {
return 'user-agent' === key.toLowerCase();
})
) {
// TODO opts.headers['User-Agent'] = 'TODO';
}
return request(opts);
};

17
lib/node/sha2.js

@ -0,0 +1,17 @@
/* global Promise */
'use strict';
var sha2 = module.exports;
var crypto = require('crypto');
sha2.sum = function(alg, str) {
return Promise.resolve().then(function() {
var sha = 'sha' + String(alg).replace(/^sha-?/i, '');
// utf8 is the default for strings
var buf = Buffer.from(str);
return crypto
.createHash(sha)
.update(buf)
.digest();
});
};

33
native.js

@ -0,0 +1,33 @@
'use strict';
var native = module.exports;
var promisify = require('util').promisify;
var resolveTxt = promisify(require('dns').resolveTxt);
native._canCheck = function(me) {
me._canCheck = {};
me._canCheck['http-01'] = true;
me._canCheck['dns-01'] = true;
return Promise.resolve();
};
native._dns01 = function(me, ch) {
// TODO use digd.js
return resolveTxt(ch.dnsHost).then(function(records) {
return {
answer: records.map(function(rr) {
return {
data: rr
};
})
};
});
};
native._http01 = function(me, ch) {
return new me.request({
url: ch.challengeUrl
}).then(function(resp) {
return resp.body;
});
};

229
package-lock.json

@ -1,46 +1,227 @@
{
"name": "acme-v2",
"version": "1.8.6",
"name": "@root/acme",
"version": "3.0.0-wip.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@root/asn1": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
"requires": {
"@root/encoding": "^1.0.1"
}
},
"@root/csr": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
"dev": true,
"requires": {
"@root/asn1": "^1.0.0",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
}
},
"@root/encoding": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
},
"@root/keypairs": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
"requires": {
"@root/encoding": "^1.0.1",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
}
},
"@root/pem": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz",
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
},
"@root/request": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
},
"@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
}
}
},
"dotenv": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==",
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
},
"eckles": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"glob": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"keypairs": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
"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": {
"eckles": "^1.4.1",
"rasha": "^1.2.4"
"once": "^1.3.0",
"wrappy": "1"
}
},
"rasha": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz",
"integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw=="
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"rsa-compat": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz",
"integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==",
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"keypairs": "^1.2.14"
"brace-expansion": "^1.1.7"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}

61
package.json

@ -1,41 +1,60 @@
{
"name": "acme-v2",
"version": "1.8.6",
"description": "A lightweight library for getting Free SSL certifications through Let's Encrypt, using the ACME protocol.",
"homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js",
"main": "index.js",
"name": "@root/acme",
"version": "3.0.0-wip.4",
"description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt",
"homepage": "https://rootprojects.org/acme/",
"main": "acme.js",
"browser": {
"./native.js": "./browser.js",
"./lib/node/sha2.js": "./lib/browser/sha2.js",
"./lib/node/http.js": "./lib/browser/http.js"
},
"files": [
"compat.js",
"*.js",
"lib",
"scripts"
"dist"
],
"scripts": {
"build": "node_xxx bin/bundle.js",
"lint": "jshint lib bin",
"postinstall": "node scripts/postinstall",
"test": "node ./test.js"
"test": "node server.js",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://git.coolaj86.com/coolaj86/acme-v2.js.git"
"url": "https://git.rootprojects.org/root/acme.js.git"
},
"keywords": [
"Let's Encrypt",
"ACME",
"v02",
"v2",
"draft-11",
"draft-12",
"free ssl",
"tls",
"automated https",
"letsencrypt"
"Let's Encrypt",
"EC",
"RSA",
"CSR",
"browser",
"greenlock",
"VanillaJS",
"ZeroSSL"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)",
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0",
"dependencies": {
"@root/encoding": "^1.0.1",
"@root/keypairs": "^0.9.0",
"@root/pem": "^1.0.4",
"@root/request": "^1.3.11",
"rsa-compat": "^2.0.8"
"@root/x509": "^0.7.2"
},
"devDependencies": {
"dotenv": "^8.0.0"
"@root/csr": "^0.8.1",
"dig.js": "^1.3.9",
"dns-suite": "^1.2.13",
"dotenv": "^8.1.0",
"punycode": "^1.4.1"
},
"trulyOptionalDependencies": {
"eslint": "^6.5.1",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9"
}
}

22
scripts/postinstall

@ -1,24 +1,4 @@
#!/usr/bin/env node
'use strict';
// BG WH \u001b[47m
// BOLD \u001b[1m
// RED \u001b[31m
// GREEN \u001b[32m
// RESET \u001b[0m
setTimeout(function() {
[
'',
'\u001b[31mGreenlock and ACME.js v3 are on the way!\u001b[0m',
'Watch for updates at https://indiegogo.com/at/greenlock',
''
]
.forEach(function(line) {
console.info(line);
});
}, 300);
setTimeout(function() {
// give time to read
}, 1500);
// TODO put postinstall back

3
test.js

@ -1,3 +0,0 @@
'use strict';
require('dotenv').config();
require('./examples/dns-01-digitalocean.js');

118
tests/cb.js

@ -1,118 +0,0 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports.run = function run(
directoryUrl,
RSA,
web,
chType,
email,
accountKeypair,
domainKeypair
) {
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
var acme2 = require('../').ACME.create({ RSA: RSA });
acme2.init(directoryUrl).then(function() {
var options = {
agreeToTerms: function(tosUrl, agree) {
agree(null, tosUrl);
},
setChallenge: function(opts, cb) {
var pathname;
console.log('');
console.log('identifier:');
console.log(opts.identifier);
console.log('hostname:');
console.log(opts.hostname);
console.log('type:');
console.log(opts.type);
console.log('token:');
console.log(opts.token);
console.log('thumbprint:');
console.log(opts.thumbprint);
console.log('keyAuthorization:');
console.log(opts.keyAuthorization);
console.log('dnsAuthorization:');
console.log(opts.dnsAuthorization);
console.log('');
if ('http-01' === opts.type) {
pathname =
opts.hostname +
acme2.challengePrefixes['http-01'] +
'/' +
opts.token;
console.log(
"Put the string '" +
opts.keyAuthorization +
"' into a file at '" +
pathname +
"'"
);
console.log(
"echo '" + opts.keyAuthorization + "' > '" + pathname + "'"
);
} else if ('dns-01' === opts.type) {
pathname =
acme2.challengePrefixes['dns-01'] +
'.' +
opts.hostname.replace(/^\*\./, '');
console.log(
"Put the string '" +
opts.dnsAuthorization +
"' into the TXT record '" +
pathname +
"'"
);
console.log(
'ddig TXT ' + pathname + " '" + opts.dnsAuthorization + "'"
);
} else {
cb(new Error('[acme-v2] unrecognized challenge type'));
return;
}
console.log("\nThen hit the 'any' key to continue...");
function onAny() {
console.log("'any' key was hit");
process.stdin.pause();
process.stdin.removeListener('data', onAny);
process.stdin.setRawMode(false);
cb();
}
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', onAny);
},
removeChallenge: function(opts, cb) {
// hostname, key
console.log(
'[acme-v2] remove challenge',
opts.hostname,
opts.keyAuthorization
);
setTimeout(cb, 1 * 1000);
},
challengeType: chType,
email: email,
accountKeypair: accountKeypair,
domainKeypair: domainKeypair,
domains: web
};
acme2.accounts.create(options).then(function(account) {
console.log('[acme-v2] account:');
console.log(account);
acme2.certificates.create(options).then(function(fullchainPem) {
console.log('[acme-v2] fullchain.pem:');
console.log(fullchainPem);
});
});
});
};

106
tests/compat.js

@ -1,106 +0,0 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports.run = function(
directoryUrl,
RSA,
web,
chType,
email,
accountKeypair,
domainKeypair
) {
console.log('[DEBUG] run', web, chType, email);
var acme2 = require('../compat.js').ACME.create({ RSA: RSA });
acme2.getAcmeUrls(acme2.stagingServerUrl, function(err /*, directoryUrls*/) {
if (err) {
console.log('err 1');
throw err;
}
var options = {
agreeToTerms: function(tosUrl, agree) {
agree(null, tosUrl);
},
setChallenge: function(hostname, token, val, cb) {
var pathname;
if ('http-01' === cb.type) {
pathname = hostname + acme2.acmeChallengePrefix + token;
console.log(
"Put the string '" +
val /*keyAuthorization*/ +
"' into a file at '" +
pathname +
"'"
);
console.log(
"echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'"
);
console.log("\nThen hit the 'any' key to continue...");
} else if ('dns-01' === cb.type) {
// forwards-backwards compat
pathname =
acme2.challengePrefixes['dns-01'] +
'.' +
hostname.replace(/^\*\./, '');
console.log(
"Put the string '" +
cb.dnsAuthorization +
"' into the TXT record '" +
pathname +
"'"
);
console.log('dig TXT ' + pathname + " '" + cb.dnsAuthorization + "'");
console.log("\nThen hit the 'any' key to continue...");
} else {
cb(new Error('[acme-v2] unrecognized challenge type: ' + cb.type));
return;
}
function onAny() {
console.log("'any' key was hit");
process.stdin.pause();
process.stdin.removeListener('data', onAny);
process.stdin.setRawMode(false);
cb();
}
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', onAny);
},
removeChallenge: function(hostname, key, cb) {
console.log('[DEBUG] remove challenge', hostname, key);
setTimeout(cb, 1 * 1000);
},
challengeType: chType,
email: email,
accountKeypair: accountKeypair,
domainKeypair: domainKeypair,
domains: web
};
acme2.registerNewAccount(options, function(err, account) {
if (err) {
console.log('err 2');
throw err;
}
if (options.debug) console.debug('account:');
if (options.debug) console.log(account);
acme2.getCertificate(options, function(err, fullchainPem) {
if (err) {
console.log('err 3');
throw err;
}
console.log('[acme-v2] A fullchain.pem:');
console.log(fullchainPem);
});
});
});
};

15
tests/generate-cert-key.js

@ -0,0 +1,15 @@
'use strict';
async function run() {
var Keypairs = require('@root/keypairs');
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
console.log(certKeypair);
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
console.log(pem);
}
run();

225
tests/index.js

@ -0,0 +1,225 @@
'use strict';
require('dotenv').config();
var CSR = require('@root/csr');
var Enc = require('@root/encoding/base64');
var PEM = require('@root/pem');
var punycode = require('punycode');
var ACME = require('../acme.js');
var Keypairs = require('@root/keypairs');
var acme = ACME.create({
// debug: true
});
// TODO exec npm install --save-dev CHALLENGE_MODULE
var config = {
env: process.env.ENV,
email: process.env.SUBSCRIBER_EMAIL,
domain: process.env.BASE_DOMAIN,
challengeType: process.env.CHALLENGE_TYPE,
challengeModule: process.env.CHALLENGE_PLUGIN,
challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS)
};
config.debug = !/^PROD/i.test(config.env);
var pluginPrefix = 'acme-' + config.challengeType + '-';
var pluginName = config.challengeModule;
var plugin;
function badPlugin(err) {
if ('MODULE_NOT_FOUND' !== err.code) {
console.error(err);
return;
}
console.error("Couldn't find '" + pluginName + "'. Is it installed?");
console.error("\tnpm install --save-dev '" + pluginName + "'");
}
try {
plugin = require(pluginName);
} catch (err) {
if (
'MODULE_NOT_FOUND' !== err.code ||
0 === pluginName.indexOf(pluginPrefix)
) {
badPlugin(err);
process.exit(1);
}
try {
pluginName = pluginPrefix + pluginName;
plugin = require(pluginName);
} catch (e) {
badPlugin(e);
process.exit(1);
}
}
config.challenger = plugin.create(config.challengeOptions);
if (!config.challengeType || !config.domain) {
console.error(
new Error('Missing config variables. Check you .env and the docs')
.message
);
console.error(config);
process.exit(1);
}
var challenges = {};
challenges[config.challengeType] = config.challenger;
async function happyPath(accKty, srvKty, rnd) {
var agreed = false;
var metadata = await acme.init(
'https://acme-staging-v02.api.letsencrypt.org/directory'
);
// Ready to use, show page
if (config.debug) {
console.info('ACME.js initialized');
console.info(metadata);
console.info();
console.info();
}
var accountKeypair = await Keypairs.generate({ kty: accKty });
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKeypair, null, 2));
console.info();
console.info();
}
var account = await acme.accounts.create({
agreeToTerms: agree,
// TODO detect jwk/pem/der?
accountKeypair: { privateKeyJwk: accountKeypair.private },
subscriberEmail: config.email
});
// TODO top-level agree
function agree(tos) {
if (config.debug) {
console.info('Agreeing to Terms of Service:');
console.info(tos);
console.info();
console.info();
}
agreed = true;
return Promise.resolve(tos);
}
if (config.debug) {
console.info('New Subscriber Account');
console.info(JSON.stringify(account, null, 2));
console.info();
console.info();
}
if (!agreed) {
throw new Error('Failed to ask the user to agree to terms');
}
var certKeypair = await Keypairs.generate({ kty: srvKty });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
if (config.debug) {
console.info('Server Key Created');
console.info('privkey.jwk.json');
console.info(JSON.stringify(certKeypair, null, 2));
// This should be saved as `privkey.pem`
console.info();
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
console.info(pem);
console.info();
}
// 'subject' should be first in list
var domains = randomDomains(rnd);
if (config.debug) {
console.info('Get certificates for random domains:');
console.info(
domains
.map(function(puny) {
var uni = punycode.toUnicode(puny);
if (puny !== uni) {
return puny + ' (' + uni + ')';
}
return puny;
})
.join('\n')
);
console.info();
}
// Create CSR
var csrDer = await CSR.csr({
jwk: certKeypair.private,
domains: domains,
encoding: 'der'
});
var csr = Enc.bufToUrlBase64(csrDer);
var csrPem = PEM.packBlock({
type: 'CERTIFICATE REQUEST',
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
});
if (config.debug) {
console.info('Certificate Signing Request');
console.info(csrPem);
console.info();
}
var results = await acme.certificates.create({
account: account,
accountKeypair: { privateKeyJwk: accountKeypair.private },
csr: csr,
domains: domains,
challenges: challenges, // must be implemented
customerEmail: null
});
if (config.debug) {
console.info('Got SSL Certificate:');
console.info(Object.keys(results));
console.info(results.expires);
console.info(results.cert);
console.info(results.chain);
console.info();
console.info();
}
}
// Try EC + RSA
var rnd = random();
happyPath('EC', 'RSA', rnd)
.then(function() {
// Now try RSA + EC
rnd = random();
return happyPath('RSA', 'EC', rnd).then(function() {
console.info('success');
});
})
.catch(function(err) {
console.error('Error:');
console.error(err.stack);
});
function randomDomains(rnd) {
return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map(
function(pre) {
return punycode.toASCII(pre + '-' + rnd + '.' + config.domain);
}
);
}
function random() {
return (
parseInt(
Math.random()
.toString()
.slice(2, 99),
10
)
.toString(16)
.slice(0, 4) + '例'
);
}

124
tests/promise.js

@ -1,124 +0,0 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
/* global Promise */
module.exports.run = function run(
directoryUrl,
RSA,
web,
chType,
email,
accountKeypair,
domainKeypair
) {
var acme2 = require('../').ACME.create({ RSA: RSA });
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
acme2.init(directoryUrl).then(function() {
var options = {
agreeToTerms: function(tosUrl) {
return Promise.resolve(tosUrl);
},
setChallenge: function(opts) {
return new Promise(function(resolve, reject) {
var pathname;
console.log('');
console.log('identifier:');
console.log(opts.identifier);
console.log('hostname:');
console.log(opts.hostname);
console.log('type:');
console.log(opts.type);
console.log('token:');
console.log(opts.token);
console.log('thumbprint:');
console.log(opts.thumbprint);
console.log('keyAuthorization:');
console.log(opts.keyAuthorization);
console.log('dnsAuthorization:');
console.log(opts.dnsAuthorization);
console.log('');
if ('http-01' === opts.type) {
pathname =
opts.hostname +
acme2.challengePrefixes['http-01'] +
'/' +
opts.token;
console.log(
"Put the string '" +
opts.keyAuthorization +
"' into a file at '" +
pathname +
"'"
);
console.log(
"echo '" + opts.keyAuthorization + "' > '" + pathname + "'"
);
} else if ('dns-01' === opts.type) {
pathname =
acme2.challengePrefixes['dns-01'] +
'.' +
opts.hostname.replace(/^\*\./, '');
console.log(
"Put the string '" +
opts.dnsAuthorization +
"' into the TXT record '" +
pathname +
"'"
);
console.log(
'dig TXT ' + pathname + " '" + opts.dnsAuthorization + "'"
);
} else {
reject(new Error('[acme-v2] unrecognized challenge type'));
return;
}
console.log("\nThen hit the 'any' key to continue...");
function onAny() {
console.log("'any' key was hit");
process.stdin.pause();
process.stdin.removeListener('data', onAny);
process.stdin.setRawMode(false);
resolve();
return;
}
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', onAny);
});
},
removeChallenge: function(opts) {
console.log(
'[acme-v2] remove challenge',
opts.hostname,
opts.keyAuthorization
);
return new Promise(function(resolve) {
// hostname, key
setTimeout(resolve, 1 * 1000);
});
},
challengeType: chType,
email: email,
accountKeypair: accountKeypair,
domainKeypair: domainKeypair,
domains: web
};
acme2.accounts.create(options).then(function(account) {
console.log('[acme-v2] account:');
console.log(account);
acme2.certificates.create(options).then(function(fullchainPem) {
console.log('[acme-v2] fullchain.pem:');
console.log(fullchainPem);
});
});
});
};

20
webpack.config.js

@ -0,0 +1,20 @@
'use strict';
var path = require('path');
module.exports = {
entry: './examples/app.js',
//entry: './acme.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js'
//filename: 'acme.js',
//library: '@root/acme',
//libraryTarget: 'umd'
//globalObject: "typeof self !== 'undefined' ? self : this"
},
resolve: {
aliasFields: ['webpack', 'browser'],
mainFields: ['browser', 'main']
}
};
Loading…
Cancel
Save