AJ ONeal
5 years ago
35 changed files with 2892 additions and 1591 deletions
@ -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 |
@ -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™? |
|||
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 & Rules of the Road |
|||
|
|||
Greenlock™ 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) |
|||
|
File diff suppressed because it is too large
@ -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) |
|||
); |
|||
})(); |
@ -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; |
|||
}); |
|||
}; |
@ -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; |
@ -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); |
|||
})(); |
@ -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); |
@ -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"}' |
|||
|
@ -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()); |
|||
}); |
@ -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()); |
|||
}); |
@ -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 Encrypt for the Browser |
|||
</h1> |
|||
|
|||
<p> |
|||
This is intended to be explored with your JavaScript console open. |
|||
</p> |
|||
<pre><code><script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.js">https://rootprojects.org/acme/bluecrypt-acme.js</a>"></script></code></pre> |
|||
<pre><code><script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.min.js">https://rootprojects.org/acme/bluecrypt-acme.min.js</a>"></script></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"> </code></pre> |
|||
</details> |
|||
<details class="js-toc-der-private" hidden> |
|||
<summary>DER Private Binary</summary> |
|||
<pre><code class="js-der-private"> </code></pre> |
|||
</details> |
|||
<details class="js-toc-der-public" hidden> |
|||
<summary>DER Public Binary</summary> |
|||
<pre><code class="js-der-public"> </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"> </code></pre> |
|||
</details> |
|||
<details class="js-toc-acme-order-response" hidden> |
|||
<summary>ACME Order Response</summary> |
|||
<pre><code class="js-acme-order-response"> </code></pre> |
|||
</details> |
|||
|
|||
<br /> |
|||
<p> |
|||
Bluecrypt™ 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> |
@ -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'" |
|||
); |
|||
} |
@ -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" |
|||
} |
|||
} |
@ -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" |
|||
} |
@ -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" |
|||
} |
|||
} |
@ -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; |
|||
}); |
|||
}); |
|||
}; |
@ -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); |
|||
}; |
@ -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); |
|||
}; |
@ -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(); |
|||
}); |
|||
}; |
@ -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; |
|||
}); |
|||
}; |
@ -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 |
|||
} |
|||
} |
|||
} |
|||
|
@ -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" |
|||
} |
|||
} |
|||
|
@ -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 |
|||
|
@ -1,3 +0,0 @@ |
|||
'use strict'; |
|||
require('dotenv').config(); |
|||
require('./examples/dns-01-digitalocean.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); |
|||
}); |
|||
}); |
|||
}); |
|||
}; |
@ -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); |
|||
}); |
|||
}); |
|||
}); |
|||
}; |
@ -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(); |
@ -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) + '例' |
|||
); |
|||
} |
@ -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); |
|||
}); |
|||
}); |
|||
}); |
|||
}; |
@ -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…
Reference in new issue