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

This commit is contained in:
AJ ONeal 2019-10-21 15:22:07 -06:00
commit ad42d34587
35 changed files with 2937 additions and 1636 deletions

4
.gitignore vendored
View File

@ -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

View File

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

53
CHANGELOG.md Normal file
View File

@ -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

View File

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

498
README.md
View File

@ -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 Draft 12
- [ ] (in-progress) Let's Encrypt™ v2.1 / ACME Draft 18
- [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
- [x] Zero External Dependencies
- [x] VanillaJS, Zero External Dependencies
- [x] Safe, Efficient, Maintained
- [x] Works in Node v6+
- [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains))
- [x] Node.js\* (v6+)
- [x] WebPack
- [x] Online Demo
- See https://greenlock.domains
\* <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>
\* 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.
## Looking for Quick 'n' Easy&trade;?
# Want Quick and Easy?
If you want something that's more "batteries included" give
[greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
a try.
ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers.
- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
If you're looking for maximum convenience, try
[Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js).
## v1.7+: Transitional v2 Support
- <https://git.rootprojects.org/root/greenlock-express.js>
By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18).
# Online Demos
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.
- Greenlock for the Web <https://greenlock.domains>
- ACME.js Demo <https://rootprojects.org/acme/>
We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify**
the code with a fresh new release.
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.
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
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.
## Recommended Example
# QuickStart
Due to the upcoming changes we've removed the old documentation.
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)
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
```
<!--
## How to build ACME clients
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)).
I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example:
```bash
node examples/cli.js
```
The example cli has the following prompts:
```
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'
echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
Then hit the 'any' key to continue...
```
When you've completed the challenge you can hit a key to continue the process.
If you place the certificate you receive back in `tests/fullchain.pem`
you can then test it with `examples/https-server.js`.
```
examples/cli.js
examples/genkeypair.js
tests/compat.js
examples/https-server.js
examples/http-server.js
```
-->
## API
Status: Small, but breaking changes coming in v2
This API is a simple evolution of le-acme-core,
but tries to provide a better mapping to the new draft 11 APIs.
## Node.js
```js
var ACME = require('acme-v2').ACME.create({
// used for overriding the default user-agent
userAgent: 'My custom UA String',
getUserAgentString: function(deps) {
return 'My custom UA String';
var ACME = require('@root/acme');
```
## WebPack
```html
<meta charset="UTF-8" />
```
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
```js
var ACME = require('@root/acme');
```
## 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>
```
`acme.min.js`
```html
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.min.js"></script>
```
Use
```js
var ACME = window['@root/acme'];
```
## Examples
You can see `tests/index.js`, `examples/index.html`, `examples/app.js` in the repo for full example usage.
### Emails: Maintainer vs Subscriber vs Customer
- `maintainerEmail` should be the email address of the **author of the code**.
This person will receive critical security and API change notifications.
- `subscriberEmail` should be the email of the **admin of the hosting service**.
This person agrees to the Let's Encrypt Terms of Service and will be notified
when a certificate fails to renew.
- `customerEmail` should be the email of individual who owns the domain.
This is optional (not currently implemented).
Generally speaking **YOU** are the _maintainer_ and you **or your employer** is the _subscriber_.
If you (or your employer) is running any type of service
you **SHOULD NOT** pass the _customer_ email as the subscriber email.
If you are not running a service (you may be building a CLI, for example),
then you should prompt the user for their email address, and they are the subscriber.
### Instantiate ACME.js
Although built for Let's Encrypt, ACME.js will work with any server
that supports draft-15 of the ACME spec (includes POST-as-GET support).
The `init()` method takes a _directory url_ and initializes internal state according to its response.
```js
var acme = ACME.create({
maintainerEmail: 'jon@example.com'
});
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(
function() {
// Ready to use, show page
$('body').hidden = false;
}
);
```
### Create ACME Account with Let's Encrypt
ACME Accounts are key and device based, with an email address as a backup identifier.
A public account key must be registered before an SSL certificate can be requested.
```js
var accountPrivateKey;
var account;
Keypairs.generate({ kty: 'EC' }).then(function(pair) {
accountPrivateKey = pair.private;
return acme.accounts
.create({
agreeToTerms: function(tos) {
if (
window.confirm(
"Do you agree to the ACME.js and Let's Encrypt Terms of Service?"
)
) {
return Promise.resolve(tos);
}
},
accountKeypair: { privateKeyJwk: pair.private },
subscriberEmail: $('.js-email-input').value
})
.then(function(_account) {
account = _account;
});
});
```
// don't try to validate challenges locally
skipChallengeTest: false,
skipDryRun: false,
### Generate a Certificate Private Key
// 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
```js
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
// Discover Directory URLs
ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}>
// This should be saved as `privkey.pem`
console.log(pem);
```
// Accounts
ACME.accounts.create(options); // returns Promise<regr> registration data
### Generate a CSR
options = {
email: '<email>', // valid email (server checks MX records)
accountKeypair: {
// privateKeyPem or privateKeyJwt
privateKeyPem: '<ASCII PEM>'
The easiest way to generate a Certificate Signing Request will be either with `openssl` or with `@root/CSR`.
```js
var CSR = require('@root/csr');
var Enc = require('@root/encoding');
// 'subject' should be first in list
var sortedDomains = ['example.com', 'www.example.com'];
var csr = await CSR.csr({
jwk: certKeypair.private,
domains: sortedDomains,
encoding: 'der'
}).then(function(der) {
return Enc.bufToUrlBase64(der);
});
```
### Get Free 90-day SSL Certificate
Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key,
the names of domains to be secured, and a distinctly separate server private key.
A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record)
is a required part of the process, which requires `set` and `remove` callbacks/promises.
```js
var certinfo = await acme.certificates.create({
agreeToTerms: function(tos) {
return tos;
},
agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back
};
account: account,
accountKeypair: { privateKeyJwk: accountPrivateKey },
csr: csr,
domains: sortedDomains,
challenges: challenges, // must be implemented
customerEmail: null,
skipChallengeTests: false,
skipDryRun: false
});
// Registration
ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }>
console.log('Got SSL Certificate:');
console.log(results.expires);
options = {
domainKeypair: {
privateKeyPem: '<ASCII PEM>'
// This should be saved as `fullchain.pem`
console.log([results.cert, results.chain].join('\n'));
```
### Example "Challenge" Implementation
Typically here you're just presenting some sort of dialog to the user to ask them to
upload a file or set a DNS record.
It may be possible to do something fancy like using OAuth2 to login to Google Domanis
to set a DNS address, etc, but it seems like that sort of fanciness is probably best
reserved for server-side plugins.
```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();
},
accountKeypair: {
privateKeyPem: '<ASCII PEM>'
},
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
remove: function(opts) {
console.log('http-01 remove challenge:', opts.challengeUrl);
return Promise.resolve();
}
}
};
```
# Changelog
# IDN - International Domain Names
- v1.8
- more transitional prepwork for new v2 API
- support newer (simpler) dns-01 and http-01 libraries
- v1.5
- perform full test challenge first (even before nonce)
- v1.3
- Use node RSA keygen by default
- No non-optional external deps!
- v1.2
- fix some API out-of-specness
- doc some magic numbers (status)
- updated deps
- v1.1.0
- reduce dependencies (use lightweight @coolaj86/request instead of request)
- v1.0.5 - cleanup logging
- v1.0.4 - v6- compat use `promisify` from node's util or bluebird
- v1.0.3 - documentation cleanup
- v1.0.2
- use `options.contact` to provide raw contact array
- made `options.email` optional
- file cleanup
- v1.0.1
- Compat API is ready for use
- Eliminate debug logging
- Apr 10, 2018 - tested backwards-compatibility using greenlock.js
- Apr 5, 2018 - export http and dns challenge tests
- Apr 5, 2018 - test http and dns challenges (success and failure)
- Apr 5, 2018 - test subdomains and its wildcard
- Apr 5, 2018 - test two subdomains
- Apr 5, 2018 - test wildcard
- Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js)
- Mar 21, 2018 - _mostly_ matches le-acme-core.js API
- Mar 21, 2018 - can now accept values (not hard coded)
- Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded)
- Mar 20, 2018 - download certificate
- Mar 20, 2018 - poll for status
- Mar 20, 2018 - finalize order (submit csr)
- Mar 20, 2018 - generate domain keypair
- Mar 20, 2018 - respond to challenges
- Mar 16, 2018 - get challenges
- Mar 16, 2018 - new order
- Mar 15, 2018 - create account
- Mar 15, 2018 - generate account keypair
- Mar 15, 2018 - get nonce
- Mar 15, 2018 - get directory
Convert domain names to `punycode` before creating the certificate:
# Legal
```js
var punycode = require('punycode');
[acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) |
acme.certificates.create({
// ...
domains: ['example.com', 'www.example.com'].map(function(name) {
return punycode.toASCII(name);
})
});
```
The punycode library itself is lightweight and dependency-free.
It is available both in node and for browsers.
# Testing
You will need to use one of the [`acme-dns-01-*` plugins](https://www.npmjs.com/search?q=acme-dns-01-)
to run the test locally.
You'll also need a `.env` that looks something like the one in `examples/example.env`:
```bash
ENV=DEV
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
BASE_DOMAIN=test.example.com
CHALLENGE_TYPE=dns-01
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
```
For example:
```bash
# Get the repo and change directories into it
git clone https://git.rootprojects.org/root/bluecrypt-acme.js
pushd bluecrypt-acme.js/
# Install the challenge plugin you'll use for the tests
npm install --save-dev acme-dns-01-digitalocean
# Copy the sample .env file
rsync -av examples/example.env .env
# Edit the config file to use a domain in your account, and your API token
#vim .env
code .env
# Run the tests
node tests/index.js
```
# Developing
You can see `<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)

1290
acme.js

File diff suppressed because it is too large Load Diff

60
bin/bundle.js Normal file
View File

@ -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 Normal file
View File

@ -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;
});
};

View File

@ -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 Normal file
View File

@ -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);
})();

View File

@ -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);

View File

@ -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"}'

View File

@ -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
);
});
}

View File

@ -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());
});

View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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"
}
}

View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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;
});
};

241
package-lock.json generated
View File

@ -1,46 +1,227 @@
{
"name": "acme-v2",
"version": "1.8.6",
"name": "@root/acme",
"version": "3.0.0-wip.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@root/asn1": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
"requires": {
"@root/encoding": "^1.0.1"
}
},
"@root/csr": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
"dev": true,
"requires": {
"@root/asn1": "^1.0.0",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
}
},
"@root/encoding": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
},
"@root/keypairs": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
"requires": {
"@root/encoding": "^1.0.1",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
}
},
"@root/pem": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz",
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
},
"@root/request": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
},
"dotenv": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==",
"@root/x509": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==",
"requires": {
"@root/asn1": "^1.0.0",
"@root/encoding": "^1.0.1"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"eckles": {
"bluebird": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz",
"integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"cli": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz",
"integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=",
"dev": true,
"requires": {
"exit": "0.1.2",
"glob": "^7.1.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"dig.js": {
"version": "1.3.9",
"resolved": "https://registry.npmjs.org/dig.js/-/dig.js-1.3.9.tgz",
"integrity": "sha512-O/tSWZuW7AwpjsgePPmTanwvSDL9xF+FzLTJD9byN3C6lk79iMejC/Ahz9CERAXTW4e2TXL1vtqh3T0Ug79ocA==",
"dev": true,
"requires": {
"cli": "^1.0.1",
"dns-suite": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2",
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
},
"dependencies": {
"dns-suite": {
"version": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#092008f766540909d27c934211495c9e03705bf3",
"from": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2",
"dev": true,
"requires": {
"bluebird": "^3.5.0",
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
}
}
}
},
"dns-suite": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/dns-suite/-/dns-suite-1.2.13.tgz",
"integrity": "sha512-veYKPHUc2RfRCe7c4G/iKxhRv0S4InJ3JsW8tEhW6Yb7dn3ac34iozC6cNX0uzHYZUw0BG5V9Fu65L1bx1GeBg==",
"dev": true,
"requires": {
"@root/hexdump": "^1.1.1"
},
"dependencies": {
"@root/hexdump": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@root/hexdump/-/hexdump-1.1.1.tgz",
"integrity": "sha512-AmrmLOutlzctR599ittO06lINOco1TIqb0c1wu83fP2Eoi5iSvx7kVWC4mDufze8rxPewC+aQOx4e6Pw7izV4A==",
"dev": true
}
}
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"glob": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"hexdump.js": {
"version": "git+https://git.coolaj86.com/coolaj86/hexdump.js#222fa7de5036a16397de2fe703c35ac54a3d8d0c",
"from": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"keypairs": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
"requires": {
"eckles": "^1.4.1",
"rasha": "^1.2.4"
}
},
"rasha": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz",
"integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw=="
},
"rsa-compat": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz",
"integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==",
"requires": {
"keypairs": "^1.2.14"
}
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}

View File

@ -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"
}
}

View File

@ -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

View File

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

View File

@ -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);
});
});
});
};

View File

@ -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);
});
});
});
};

View File

@ -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 Normal file
View File

@ -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) + '例'
);
}

View File

@ -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 Normal file
View File

@ -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']
}
};