merge unrelated v2 (historical) and v3 (new from scratch)
This commit is contained in:
commit
ad42d34587
|
@ -1,4 +1,7 @@
|
||||||
.env
|
.env
|
||||||
|
*.gz
|
||||||
|
.*.sw*
|
||||||
|
.ignore
|
||||||
|
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
@ -14,4 +17,5 @@ coverage
|
||||||
|
|
||||||
# Dependency directory
|
# Dependency directory
|
||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"tabWidth": 2,
|
"tabWidth": 4,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"useTabs": true
|
"useTabs": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
- v3 (Oct 2019)
|
||||||
|
- Add POST-as-GET for Let's Encrypt v2 release 2 (ACME / RFC 8555)
|
||||||
|
- Jump to v3 for parity with Greenlock
|
||||||
|
- Merge browser and node.js versions in one
|
||||||
|
- Drop all backwards-compat complexity
|
||||||
|
- Move to zero-external deps, using @root packages only
|
||||||
|
- v1.8
|
||||||
|
- more transitional prepwork for new v2 API
|
||||||
|
- support newer (simpler) dns-01 and http-01 libraries
|
||||||
|
- v1.5
|
||||||
|
- perform full test challenge first (even before nonce)
|
||||||
|
- v1.3
|
||||||
|
- Use node RSA keygen by default
|
||||||
|
- No non-optional external deps!
|
||||||
|
- v1.2
|
||||||
|
- fix some API out-of-specness
|
||||||
|
- doc some magic numbers (status)
|
||||||
|
- updated deps
|
||||||
|
- v1.1.0
|
||||||
|
- reduce dependencies (use lightweight @coolaj86/request instead of request)
|
||||||
|
- v1.0.5 - cleanup logging
|
||||||
|
- v1.0.4 - v6- compat use `promisify` from node's util or bluebird
|
||||||
|
- v1.0.3 - documentation cleanup
|
||||||
|
- v1.0.2
|
||||||
|
- use `options.contact` to provide raw contact array
|
||||||
|
- made `options.email` optional
|
||||||
|
- file cleanup
|
||||||
|
- v1.0.1
|
||||||
|
- Compat API is ready for use
|
||||||
|
- Eliminate debug logging
|
||||||
|
- Apr 10, 2018 - tested backwards-compatibility using greenlock.js
|
||||||
|
- Apr 5, 2018 - export http and dns challenge tests
|
||||||
|
- Apr 5, 2018 - test http and dns challenges (success and failure)
|
||||||
|
- Apr 5, 2018 - test subdomains and its wildcard
|
||||||
|
- Apr 5, 2018 - test two subdomains
|
||||||
|
- Apr 5, 2018 - test wildcard
|
||||||
|
- Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js)
|
||||||
|
- Mar 21, 2018 - _mostly_ matches le-acme-core.js API
|
||||||
|
- Mar 21, 2018 - can now accept values (not hard coded)
|
||||||
|
- Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded)
|
||||||
|
- Mar 20, 2018 - download certificate
|
||||||
|
- Mar 20, 2018 - poll for status
|
||||||
|
- Mar 20, 2018 - finalize order (submit csr)
|
||||||
|
- Mar 20, 2018 - generate domain keypair
|
||||||
|
- Mar 20, 2018 - respond to challenges
|
||||||
|
- Mar 16, 2018 - get challenges
|
||||||
|
- Mar 16, 2018 - new order
|
||||||
|
- Mar 15, 2018 - create account
|
||||||
|
- Mar 15, 2018 - generate account keypair
|
||||||
|
- Mar 15, 2018 - get nonce
|
||||||
|
- Mar 15, 2018 - get directory
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright 2018 AJ ONeal
|
Copyright 2015-2019 AJ ONeal
|
||||||
|
|
||||||
Mozilla Public License Version 2.0
|
Mozilla Public License Version 2.0
|
||||||
==================================
|
==================================
|
||||||
|
|
498
README.md
498
README.md
|
@ -1,239 +1,355 @@
|
||||||
# ACME.js v3 on its way (Nov 1st, 2019)
|
# [ACME.js](https://git.rootprojects.org/root/acme.js) v3
|
||||||
|
|
||||||
ACME.js v3 is in private beta and will be available by Nov 1st.
|
| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains)
|
||||||
|
|
||||||
Follow the updates on the [campaign page](https://indiegogo.com/at/greenlock),
|
Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers
|
||||||
and contribute to support the project and get beta access now.
|
|
||||||
|
|
||||||
| **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2))
|
Lightweight. Fast. Modern Crypto. Zero external dependecies.
|
||||||
| [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)
|
|
||||||
|
|
||||||
# [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
|
| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |
|
||||||
Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates.
|
|
||||||
|
|
||||||
The primary goal of this library is to make it easy to
|
The primary goal of this library is to make it easy to
|
||||||
get Accounts and Certificates through Let's Encrypt.
|
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] Let's Encrypt™ v2 / ACME Draft 12
|
- [x] Secure support for EC and RSA for account and server keys
|
||||||
- [ ] (in-progress) Let's Encrypt™ v2.1 / ACME Draft 18
|
- [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations
|
||||||
- [ ] (in-progress) StartTLS Everywhere™
|
- [ ] (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] 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] **http-01** for single or multiple domains per certificate
|
||||||
- [x] **dns-01** for wildcards, localhost, private networks, etc
|
- [x] **dns-01** for wildcards, localhost, private networks, etc
|
||||||
- [x] VanillaJS
|
- [x] VanillaJS, Zero External Dependencies
|
||||||
- [x] Zero External Dependencies
|
|
||||||
- [x] Safe, Efficient, Maintained
|
- [x] Safe, Efficient, Maintained
|
||||||
- [x] Works in Node v6+
|
- [x] Node.js\* (v6+)
|
||||||
- [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains))
|
- [x] WebPack
|
||||||
|
- [x] Online Demo
|
||||||
|
- See https://greenlock.domains
|
||||||
|
|
||||||
\* <small>The only required dependencies were built by us, specifically for this and related libraries.
|
\* Although we use `async/await` in the examples, the code is written in CommonJS,
|
||||||
There are some, truly optional, backwards-compatibility dependencies for node v6.</small>
|
with Promises, so you can use it in Node.js and Browsers without transpiling.
|
||||||
|
|
||||||
## Looking for Quick 'n' Easy™?
|
# Want Quick and Easy?
|
||||||
|
|
||||||
If you want something that's more "batteries included" give
|
ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers.
|
||||||
[greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
|
|
||||||
a try.
|
|
||||||
|
|
||||||
- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
|
If you're looking for maximum convenience, try
|
||||||
|
[Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js).
|
||||||
|
|
||||||
## v1.7+: Transitional v2 Support
|
- <https://git.rootprojects.org/root/greenlock-express.js>
|
||||||
|
|
||||||
By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18).
|
# Online Demos
|
||||||
|
|
||||||
Although the draft 18 changes themselves don't requiring breaking the API,
|
- Greenlock for the Web <https://greenlock.domains>
|
||||||
we've been keeping backwards compatibility for a long time and the API has become messy.
|
- ACME.js Demo <https://rootprojects.org/acme/>
|
||||||
|
|
||||||
We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify**
|
We expect that our hosted versions will meet all of yours needs.
|
||||||
the code with a fresh new release.
|
If they don't, please open an issue to let us know why.
|
||||||
|
|
||||||
As of **v1.7** we started adding **transitional support** for the **next major version**, v2.0 of acme-v2.js.
|
We'd much rather improve the app than have a hundred different versions running in the wild.
|
||||||
We've been really good about backwards compatibility for
|
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
|
||||||
|
|
||||||
## Recommended Example
|
# QuickStart
|
||||||
|
|
||||||
Due to the upcoming changes we've removed the old documentation.
|
To make it easy to generate, encode, and decode keys and certificates,
|
||||||
|
ACME.js uses [Keypairs.js](https://git.rootprojects.org/root/keypairs.js)
|
||||||
|
and [CSR.js](https://git.rootprojects.org/root/csr.js)
|
||||||
|
|
||||||
Instead we recommend that you take a look at the
|
## Node.js
|
||||||
[Digital Ocean DNS-01 Example](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
|
|
||||||
|
|
||||||
- [examples/dns-01-digitalocean.js](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
|
|
||||||
|
|
||||||
That's not exactly the new API, but it's close.
|
|
||||||
|
|
||||||
## Let's Encrypt v02 Directory URLs
|
|
||||||
|
|
||||||
```
|
|
||||||
# Production URL
|
|
||||||
https://acme-v02.api.letsencrypt.org/directory
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
# Staging URL
|
|
||||||
https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
```
|
|
||||||
|
|
||||||
<!--
|
|
||||||
## How to build ACME clients
|
|
||||||
|
|
||||||
As this is intended to build ACME clients, there is not a simple 2-line example
|
|
||||||
(and if you want that, see [greenlock-express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js)).
|
|
||||||
|
|
||||||
I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node examples/cli.js
|
|
||||||
```
|
|
||||||
|
|
||||||
The example cli has the following prompts:
|
|
||||||
|
|
||||||
```
|
|
||||||
What web address(es) would you like to get certificates for? (ex: example.com,*.example.com)
|
|
||||||
What challenge will you be testing today? http-01 or dns-01? [http-01]
|
|
||||||
What email should we use? (optional)
|
|
||||||
What API style would you like to test? v1-compat or promise? [v1-compat]
|
|
||||||
|
|
||||||
Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
|
|
||||||
|
|
||||||
echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
|
|
||||||
|
|
||||||
Then hit the 'any' key to continue...
|
|
||||||
```
|
|
||||||
|
|
||||||
When you've completed the challenge you can hit a key to continue the process.
|
|
||||||
|
|
||||||
If you place the certificate you receive back in `tests/fullchain.pem`
|
|
||||||
you can then test it with `examples/https-server.js`.
|
|
||||||
|
|
||||||
```
|
|
||||||
examples/cli.js
|
|
||||||
examples/genkeypair.js
|
|
||||||
tests/compat.js
|
|
||||||
examples/https-server.js
|
|
||||||
examples/http-server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
Status: Small, but breaking changes coming in v2
|
|
||||||
|
|
||||||
This API is a simple evolution of le-acme-core,
|
|
||||||
but tries to provide a better mapping to the new draft 11 APIs.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var ACME = require('acme-v2').ACME.create({
|
var ACME = require('@root/acme');
|
||||||
// used for overriding the default user-agent
|
```
|
||||||
userAgent: 'My custom UA String',
|
|
||||||
getUserAgentString: function(deps) {
|
## WebPack
|
||||||
return 'My custom UA String';
|
|
||||||
|
```html
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
```
|
||||||
|
|
||||||
|
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ACME = require('@root/acme');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vanilla JS
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
```
|
||||||
|
|
||||||
|
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
|
||||||
|
|
||||||
|
`acme.js`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
`acme.min.js`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ACME = window['@root/acme'];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
You can see `tests/index.js`, `examples/index.html`, `examples/app.js` in the repo for full example usage.
|
||||||
|
|
||||||
|
### Emails: Maintainer vs Subscriber vs Customer
|
||||||
|
|
||||||
|
- `maintainerEmail` should be the email address of the **author of the code**.
|
||||||
|
This person will receive critical security and API change notifications.
|
||||||
|
- `subscriberEmail` should be the email of the **admin of the hosting service**.
|
||||||
|
This person agrees to the Let's Encrypt Terms of Service and will be notified
|
||||||
|
when a certificate fails to renew.
|
||||||
|
- `customerEmail` should be the email of individual who owns the domain.
|
||||||
|
This is optional (not currently implemented).
|
||||||
|
|
||||||
|
Generally speaking **YOU** are the _maintainer_ and you **or your employer** is the _subscriber_.
|
||||||
|
|
||||||
|
If you (or your employer) is running any type of service
|
||||||
|
you **SHOULD NOT** pass the _customer_ email as the subscriber email.
|
||||||
|
|
||||||
|
If you are not running a service (you may be building a CLI, for example),
|
||||||
|
then you should prompt the user for their email address, and they are the subscriber.
|
||||||
|
|
||||||
|
### Instantiate ACME.js
|
||||||
|
|
||||||
|
Although built for Let's Encrypt, ACME.js will work with any server
|
||||||
|
that supports draft-15 of the ACME spec (includes POST-as-GET support).
|
||||||
|
|
||||||
|
The `init()` method takes a _directory url_ and initializes internal state according to its response.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var acme = ACME.create({
|
||||||
|
maintainerEmail: 'jon@example.com'
|
||||||
|
});
|
||||||
|
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(
|
||||||
|
function() {
|
||||||
|
// Ready to use, show page
|
||||||
|
$('body').hidden = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create ACME Account with Let's Encrypt
|
||||||
|
|
||||||
|
ACME Accounts are key and device based, with an email address as a backup identifier.
|
||||||
|
|
||||||
|
A public account key must be registered before an SSL certificate can be requested.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var accountPrivateKey;
|
||||||
|
var account;
|
||||||
|
|
||||||
|
Keypairs.generate({ kty: 'EC' }).then(function(pair) {
|
||||||
|
accountPrivateKey = pair.private;
|
||||||
|
|
||||||
|
return acme.accounts
|
||||||
|
.create({
|
||||||
|
agreeToTerms: function(tos) {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
"Do you agree to the ACME.js and Let's Encrypt Terms of Service?"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return Promise.resolve(tos);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
accountKeypair: { privateKeyJwk: pair.private },
|
||||||
|
subscriberEmail: $('.js-email-input').value
|
||||||
|
})
|
||||||
|
.then(function(_account) {
|
||||||
|
account = _account;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
// don't try to validate challenges locally
|
### Generate a Certificate Private Key
|
||||||
skipChallengeTest: false,
|
|
||||||
skipDryRun: false,
|
|
||||||
|
|
||||||
// ask if the certificate can be issued up to 10 times before failing
|
```js
|
||||||
retryPoll: 8,
|
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
|
||||||
// ask if the certificate has been validated up to 6 times before cancelling
|
var pem = await Keypairs.export({
|
||||||
retryPending: 4,
|
jwk: certKeypair.private,
|
||||||
// Wait 1000ms between retries
|
encoding: 'pem'
|
||||||
retryInterval: 1000,
|
|
||||||
// Wait 10,000ms after deauthorizing a challenge before retrying
|
|
||||||
deauthWait: 10 * 1000
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Discover Directory URLs
|
// This should be saved as `privkey.pem`
|
||||||
ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}>
|
console.log(pem);
|
||||||
|
```
|
||||||
|
|
||||||
// Accounts
|
### Generate a CSR
|
||||||
ACME.accounts.create(options); // returns Promise<regr> registration data
|
|
||||||
|
|
||||||
options = {
|
The easiest way to generate a Certificate Signing Request will be either with `openssl` or with `@root/CSR`.
|
||||||
email: '<email>', // valid email (server checks MX records)
|
|
||||||
accountKeypair: {
|
```js
|
||||||
// privateKeyPem or privateKeyJwt
|
var CSR = require('@root/csr');
|
||||||
privateKeyPem: '<ASCII PEM>'
|
var Enc = require('@root/encoding');
|
||||||
|
|
||||||
|
// 'subject' should be first in list
|
||||||
|
var sortedDomains = ['example.com', 'www.example.com'];
|
||||||
|
var csr = await CSR.csr({
|
||||||
|
jwk: certKeypair.private,
|
||||||
|
domains: sortedDomains,
|
||||||
|
encoding: 'der'
|
||||||
|
}).then(function(der) {
|
||||||
|
return Enc.bufToUrlBase64(der);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Free 90-day SSL Certificate
|
||||||
|
|
||||||
|
Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key,
|
||||||
|
the names of domains to be secured, and a distinctly separate server private key.
|
||||||
|
|
||||||
|
A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record)
|
||||||
|
is a required part of the process, which requires `set` and `remove` callbacks/promises.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var certinfo = await acme.certificates.create({
|
||||||
|
agreeToTerms: function(tos) {
|
||||||
|
return tos;
|
||||||
},
|
},
|
||||||
agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back
|
account: account,
|
||||||
};
|
accountKeypair: { privateKeyJwk: accountPrivateKey },
|
||||||
|
csr: csr,
|
||||||
|
domains: sortedDomains,
|
||||||
|
challenges: challenges, // must be implemented
|
||||||
|
customerEmail: null,
|
||||||
|
skipChallengeTests: false,
|
||||||
|
skipDryRun: false
|
||||||
|
});
|
||||||
|
|
||||||
// Registration
|
console.log('Got SSL Certificate:');
|
||||||
ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }>
|
console.log(results.expires);
|
||||||
|
|
||||||
options = {
|
// This should be saved as `fullchain.pem`
|
||||||
domainKeypair: {
|
console.log([results.cert, results.chain].join('\n'));
|
||||||
privateKeyPem: '<ASCII PEM>'
|
```
|
||||||
|
|
||||||
|
### Example "Challenge" Implementation
|
||||||
|
|
||||||
|
Typically here you're just presenting some sort of dialog to the user to ask them to
|
||||||
|
upload a file or set a DNS record.
|
||||||
|
|
||||||
|
It may be possible to do something fancy like using OAuth2 to login to Google Domanis
|
||||||
|
to set a DNS address, etc, but it seems like that sort of fanciness is probably best
|
||||||
|
reserved for server-side plugins.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var challenges = {
|
||||||
|
'http-01': {
|
||||||
|
set: function(opts) {
|
||||||
|
console.info('http-01 set challenge:');
|
||||||
|
console.info(opts.challengeUrl);
|
||||||
|
console.info(opts.keyAuthorization);
|
||||||
|
while (
|
||||||
|
!window.confirm('Upload the challenge file before continuing.')
|
||||||
|
) {}
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
accountKeypair: {
|
remove: function(opts) {
|
||||||
privateKeyPem: '<ASCII PEM>'
|
console.log('http-01 remove challenge:', opts.challengeUrl);
|
||||||
},
|
return Promise.resolve();
|
||||||
domains: ['example.com'],
|
}
|
||||||
|
}
|
||||||
getZones: function(opts) {}, // should Promise an array of domain zone names
|
|
||||||
setChallenge: function(opts) {}, // should Promise the record id, or name
|
|
||||||
removeChallenge: function(opts) {} // should Promise null
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
# Changelog
|
# IDN - International Domain Names
|
||||||
|
|
||||||
- v1.8
|
Convert domain names to `punycode` before creating the certificate:
|
||||||
- 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
|
```js
|
||||||
|
var punycode = require('punycode');
|
||||||
|
|
||||||
[acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) |
|
acme.certificates.create({
|
||||||
|
// ...
|
||||||
|
domains: ['example.com', 'www.example.com'].map(function(name) {
|
||||||
|
return punycode.toASCII(name);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The punycode library itself is lightweight and dependency-free.
|
||||||
|
It is available both in node and for browsers.
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
You will need to use one of the [`acme-dns-01-*` plugins](https://www.npmjs.com/search?q=acme-dns-01-)
|
||||||
|
to run the test locally.
|
||||||
|
|
||||||
|
You'll also need a `.env` that looks something like the one in `examples/example.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENV=DEV
|
||||||
|
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
|
||||||
|
BASE_DOMAIN=test.example.com
|
||||||
|
CHALLENGE_TYPE=dns-01
|
||||||
|
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
|
||||||
|
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the repo and change directories into it
|
||||||
|
git clone https://git.rootprojects.org/root/bluecrypt-acme.js
|
||||||
|
pushd bluecrypt-acme.js/
|
||||||
|
|
||||||
|
# Install the challenge plugin you'll use for the tests
|
||||||
|
npm install --save-dev acme-dns-01-digitalocean
|
||||||
|
|
||||||
|
# Copy the sample .env file
|
||||||
|
rsync -av examples/example.env .env
|
||||||
|
|
||||||
|
# Edit the config file to use a domain in your account, and your API token
|
||||||
|
#vim .env
|
||||||
|
code .env
|
||||||
|
|
||||||
|
# Run the tests
|
||||||
|
node tests/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
# Developing
|
||||||
|
|
||||||
|
You can see `<script>` tags in the `index.html` in the repo, which references the original
|
||||||
|
source files.
|
||||||
|
|
||||||
|
Join `@rootprojects` `#general` on [Keybase](https://keybase.io) if you'd like to chat with us.
|
||||||
|
|
||||||
|
# Commercial Support
|
||||||
|
|
||||||
|
We have both commercial support and commercial licensing available.
|
||||||
|
|
||||||
|
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
|
||||||
|
Enterprise, and Internal installations, integrations, and deployments.
|
||||||
|
|
||||||
|
We also offer consulting for all-things-ACME and Let's Encrypt.
|
||||||
|
|
||||||
|
# Legal & 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 |
|
MPL-2.0 |
|
||||||
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||||
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
};
|
94
compat.js
94
compat.js
|
@ -1,94 +0,0 @@
|
||||||
// Copyright 2018 AJ ONeal. All rights reserved
|
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
'use strict';
|
|
||||||
/* global Promise */
|
|
||||||
|
|
||||||
var ACME2 = require('./').ACME;
|
|
||||||
|
|
||||||
function resolveFn(cb) {
|
|
||||||
return function(val) {
|
|
||||||
// nextTick to get out of Promise chain
|
|
||||||
process.nextTick(function() {
|
|
||||||
cb(null, val);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function rejectFn(cb) {
|
|
||||||
return function(err) {
|
|
||||||
console.error('[acme-v2] handled(?) rejection as errback:');
|
|
||||||
console.error(err.stack);
|
|
||||||
|
|
||||||
// nextTick to get out of Promise chain
|
|
||||||
process.nextTick(function() {
|
|
||||||
cb(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// do not resolve promise further
|
|
||||||
return new Promise(function() {});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function create(deps) {
|
|
||||||
deps.LeCore = {};
|
|
||||||
var acme2 = ACME2.create(deps);
|
|
||||||
acme2.registerNewAccount = function(options, cb) {
|
|
||||||
acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb));
|
|
||||||
};
|
|
||||||
acme2.getCertificate = function(options, cb) {
|
|
||||||
options.agreeToTerms =
|
|
||||||
options.agreeToTerms ||
|
|
||||||
function(tos) {
|
|
||||||
return Promise.resolve(tos);
|
|
||||||
};
|
|
||||||
acme2.certificates.create(options).then(function(certs) {
|
|
||||||
var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair);
|
|
||||||
certs.privkey = privkeyPem;
|
|
||||||
resolveFn(cb)(certs);
|
|
||||||
}, rejectFn(cb));
|
|
||||||
};
|
|
||||||
acme2.getAcmeUrls = function(options, cb) {
|
|
||||||
acme2.init(options).then(resolveFn(cb), rejectFn(cb));
|
|
||||||
};
|
|
||||||
acme2.getOptions = function() {
|
|
||||||
var defs = {};
|
|
||||||
|
|
||||||
Object.keys(module.exports.defaults).forEach(function(key) {
|
|
||||||
defs[key] = defs[deps] || module.exports.defaults[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
return defs;
|
|
||||||
};
|
|
||||||
acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl;
|
|
||||||
acme2.productionServerUrl = module.exports.defaults.productionServerUrl;
|
|
||||||
acme2.acmeChallengePrefix = module.exports.defaults.acmeChallengePrefix;
|
|
||||||
return acme2;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.ACME = {};
|
|
||||||
module.exports.defaults = {
|
|
||||||
productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory',
|
|
||||||
stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
|
||||||
knownEndpoints: [
|
|
||||||
'keyChange',
|
|
||||||
'meta',
|
|
||||||
'newAccount',
|
|
||||||
'newNonce',
|
|
||||||
'newOrder',
|
|
||||||
'revokeCert'
|
|
||||||
],
|
|
||||||
challengeTypes: ['http-01', 'dns-01'],
|
|
||||||
challengeType: 'http-01',
|
|
||||||
//, keyType: 'rsa' // ecdsa
|
|
||||||
//, keySize: 2048 // 256
|
|
||||||
rsaKeySize: 2048, // 256
|
|
||||||
acmeChallengePrefix: '/.well-known/acme-challenge/'
|
|
||||||
};
|
|
||||||
Object.keys(module.exports.defaults).forEach(function(key) {
|
|
||||||
module.exports.ACME[key] = module.exports.defaults[key];
|
|
||||||
});
|
|
||||||
Object.keys(ACME2).forEach(function(key) {
|
|
||||||
module.exports.ACME[key] = ACME2[key];
|
|
||||||
});
|
|
||||||
module.exports.ACME.create = create;
|
|
|
@ -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
|
ENV=DEV
|
||||||
ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com
|
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
|
||||||
DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
BASE_DOMAIN=test.example.com
|
||||||
|
CHALLENGE_TYPE=dns-01
|
||||||
|
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
|
||||||
|
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
|
||||||
|
|
|
@ -11,7 +11,10 @@ if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) {
|
||||||
var privkeyPem = RSA.exportPrivatePem(keypair);
|
var privkeyPem = RSA.exportPrivatePem(keypair);
|
||||||
console.log(privkeyPem);
|
console.log(privkeyPem);
|
||||||
|
|
||||||
fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem);
|
fs.writeFileSync(
|
||||||
|
__dirname + '/../tests/account.privkey.pem',
|
||||||
|
privkeyPem
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
"name": "@root/acme",
|
||||||
"version": "1.8.6",
|
"version": "3.0.0-wip.4",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"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": {
|
"@root/request": {
|
||||||
"version": "1.3.11",
|
"version": "1.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
|
||||||
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
|
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
|
||||||
},
|
},
|
||||||
"dotenv": {
|
"@root/x509": {
|
||||||
"version": "8.0.0",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
|
||||||
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==",
|
"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
|
"dev": true
|
||||||
},
|
},
|
||||||
"eckles": {
|
"bluebird": {
|
||||||
|
"version": "3.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz",
|
||||||
|
"integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"exit": "0.1.2",
|
||||||
|
"glob": "^7.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"dig.js": {
|
||||||
|
"version": "1.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/dig.js/-/dig.js-1.3.9.tgz",
|
||||||
|
"integrity": "sha512-O/tSWZuW7AwpjsgePPmTanwvSDL9xF+FzLTJD9byN3C6lk79iMejC/Ahz9CERAXTW4e2TXL1vtqh3T0Ug79ocA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"cli": "^1.0.1",
|
||||||
|
"dns-suite": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2",
|
||||||
|
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dns-suite": {
|
||||||
|
"version": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#092008f766540909d27c934211495c9e03705bf3",
|
||||||
|
"from": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"bluebird": "^3.5.0",
|
||||||
|
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dns-suite": {
|
||||||
|
"version": "1.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dns-suite/-/dns-suite-1.2.13.tgz",
|
||||||
|
"integrity": "sha512-veYKPHUc2RfRCe7c4G/iKxhRv0S4InJ3JsW8tEhW6Yb7dn3ac34iozC6cNX0uzHYZUw0BG5V9Fu65L1bx1GeBg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@root/hexdump": "^1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@root/hexdump": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@root/hexdump/-/hexdump-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-AmrmLOutlzctR599ittO06lINOco1TIqb0c1wu83fP2Eoi5iSvx7kVWC4mDufze8rxPewC+aQOx4e6Pw7izV4A==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dotenv": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||||
|
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "7.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
|
||||||
|
"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hexdump.js": {
|
||||||
|
"version": "git+https://git.coolaj86.com/coolaj86/hexdump.js#222fa7de5036a16397de2fe703c35ac54a3d8d0c",
|
||||||
|
"from": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"punycode": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
|
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"keypairs": {
|
"wrappy": {
|
||||||
"version": "1.2.14",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
"requires": {
|
"dev": true
|
||||||
"eckles": "^1.4.1",
|
|
||||||
"rasha": "^1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rasha": {
|
|
||||||
"version": "1.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz",
|
|
||||||
"integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw=="
|
|
||||||
},
|
|
||||||
"rsa-compat": {
|
|
||||||
"version": "2.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz",
|
|
||||||
"integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==",
|
|
||||||
"requires": {
|
|
||||||
"keypairs": "^1.2.14"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
61
package.json
61
package.json
|
@ -1,41 +1,60 @@
|
||||||
{
|
{
|
||||||
"name": "acme-v2",
|
"name": "@root/acme",
|
||||||
"version": "1.8.6",
|
"version": "3.0.0-wip.4",
|
||||||
"description": "A lightweight library for getting Free SSL certifications through Let's Encrypt, using the ACME protocol.",
|
"description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt",
|
||||||
"homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js",
|
"homepage": "https://rootprojects.org/acme/",
|
||||||
"main": "index.js",
|
"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": [
|
"files": [
|
||||||
"compat.js",
|
"*.js",
|
||||||
"lib",
|
"lib",
|
||||||
"scripts"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "node_xxx bin/bundle.js",
|
||||||
|
"lint": "jshint lib bin",
|
||||||
"postinstall": "node scripts/postinstall",
|
"postinstall": "node scripts/postinstall",
|
||||||
"test": "node ./test.js"
|
"test": "node server.js",
|
||||||
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.coolaj86.com/coolaj86/acme-v2.js.git"
|
"url": "https://git.rootprojects.org/root/acme.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Let's Encrypt",
|
|
||||||
"ACME",
|
"ACME",
|
||||||
"v02",
|
"Let's Encrypt",
|
||||||
"v2",
|
"EC",
|
||||||
"draft-11",
|
"RSA",
|
||||||
"draft-12",
|
"CSR",
|
||||||
"free ssl",
|
"browser",
|
||||||
"tls",
|
"greenlock",
|
||||||
"automated https",
|
"VanillaJS",
|
||||||
"letsencrypt"
|
"ZeroSSL"
|
||||||
],
|
],
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@root/encoding": "^1.0.1",
|
||||||
|
"@root/keypairs": "^0.9.0",
|
||||||
|
"@root/pem": "^1.0.4",
|
||||||
"@root/request": "^1.3.11",
|
"@root/request": "^1.3.11",
|
||||||
"rsa-compat": "^2.0.8"
|
"@root/x509": "^0.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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
|
#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// BG WH \u001b[47m
|
// TODO put postinstall back
|
||||||
// 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);
|
|
||||||
|
|
3
test.js
3
test.js
|
@ -1,3 +0,0 @@
|
||||||
'use strict';
|
|
||||||
require('dotenv').config();
|
|
||||||
require('./examples/dns-01-digitalocean.js');
|
|
118
tests/cb.js
118
tests/cb.js
|
@ -1,118 +0,0 @@
|
||||||
// Copyright 2018 AJ ONeal. All rights reserved
|
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports.run = function run(
|
|
||||||
directoryUrl,
|
|
||||||
RSA,
|
|
||||||
web,
|
|
||||||
chType,
|
|
||||||
email,
|
|
||||||
accountKeypair,
|
|
||||||
domainKeypair
|
|
||||||
) {
|
|
||||||
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
|
|
||||||
var acme2 = require('../').ACME.create({ RSA: RSA });
|
|
||||||
acme2.init(directoryUrl).then(function() {
|
|
||||||
var options = {
|
|
||||||
agreeToTerms: function(tosUrl, agree) {
|
|
||||||
agree(null, tosUrl);
|
|
||||||
},
|
|
||||||
setChallenge: function(opts, cb) {
|
|
||||||
var pathname;
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('identifier:');
|
|
||||||
console.log(opts.identifier);
|
|
||||||
console.log('hostname:');
|
|
||||||
console.log(opts.hostname);
|
|
||||||
console.log('type:');
|
|
||||||
console.log(opts.type);
|
|
||||||
console.log('token:');
|
|
||||||
console.log(opts.token);
|
|
||||||
console.log('thumbprint:');
|
|
||||||
console.log(opts.thumbprint);
|
|
||||||
console.log('keyAuthorization:');
|
|
||||||
console.log(opts.keyAuthorization);
|
|
||||||
console.log('dnsAuthorization:');
|
|
||||||
console.log(opts.dnsAuthorization);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
if ('http-01' === opts.type) {
|
|
||||||
pathname =
|
|
||||||
opts.hostname +
|
|
||||||
acme2.challengePrefixes['http-01'] +
|
|
||||||
'/' +
|
|
||||||
opts.token;
|
|
||||||
console.log(
|
|
||||||
"Put the string '" +
|
|
||||||
opts.keyAuthorization +
|
|
||||||
"' into a file at '" +
|
|
||||||
pathname +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"echo '" + opts.keyAuthorization + "' > '" + pathname + "'"
|
|
||||||
);
|
|
||||||
} else if ('dns-01' === opts.type) {
|
|
||||||
pathname =
|
|
||||||
acme2.challengePrefixes['dns-01'] +
|
|
||||||
'.' +
|
|
||||||
opts.hostname.replace(/^\*\./, '');
|
|
||||||
console.log(
|
|
||||||
"Put the string '" +
|
|
||||||
opts.dnsAuthorization +
|
|
||||||
"' into the TXT record '" +
|
|
||||||
pathname +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'ddig TXT ' + pathname + " '" + opts.dnsAuthorization + "'"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
cb(new Error('[acme-v2] unrecognized challenge type'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("\nThen hit the 'any' key to continue...");
|
|
||||||
|
|
||||||
function onAny() {
|
|
||||||
console.log("'any' key was hit");
|
|
||||||
process.stdin.pause();
|
|
||||||
process.stdin.removeListener('data', onAny);
|
|
||||||
process.stdin.setRawMode(false);
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdin.setRawMode(true);
|
|
||||||
process.stdin.resume();
|
|
||||||
process.stdin.on('data', onAny);
|
|
||||||
},
|
|
||||||
removeChallenge: function(opts, cb) {
|
|
||||||
// hostname, key
|
|
||||||
console.log(
|
|
||||||
'[acme-v2] remove challenge',
|
|
||||||
opts.hostname,
|
|
||||||
opts.keyAuthorization
|
|
||||||
);
|
|
||||||
setTimeout(cb, 1 * 1000);
|
|
||||||
},
|
|
||||||
challengeType: chType,
|
|
||||||
email: email,
|
|
||||||
accountKeypair: accountKeypair,
|
|
||||||
domainKeypair: domainKeypair,
|
|
||||||
domains: web
|
|
||||||
};
|
|
||||||
|
|
||||||
acme2.accounts.create(options).then(function(account) {
|
|
||||||
console.log('[acme-v2] account:');
|
|
||||||
console.log(account);
|
|
||||||
|
|
||||||
acme2.certificates.create(options).then(function(fullchainPem) {
|
|
||||||
console.log('[acme-v2] fullchain.pem:');
|
|
||||||
console.log(fullchainPem);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
106
tests/compat.js
106
tests/compat.js
|
@ -1,106 +0,0 @@
|
||||||
// Copyright 2018 AJ ONeal. All rights reserved
|
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports.run = function(
|
|
||||||
directoryUrl,
|
|
||||||
RSA,
|
|
||||||
web,
|
|
||||||
chType,
|
|
||||||
email,
|
|
||||||
accountKeypair,
|
|
||||||
domainKeypair
|
|
||||||
) {
|
|
||||||
console.log('[DEBUG] run', web, chType, email);
|
|
||||||
|
|
||||||
var acme2 = require('../compat.js').ACME.create({ RSA: RSA });
|
|
||||||
acme2.getAcmeUrls(acme2.stagingServerUrl, function(err /*, directoryUrls*/) {
|
|
||||||
if (err) {
|
|
||||||
console.log('err 1');
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
agreeToTerms: function(tosUrl, agree) {
|
|
||||||
agree(null, tosUrl);
|
|
||||||
},
|
|
||||||
setChallenge: function(hostname, token, val, cb) {
|
|
||||||
var pathname;
|
|
||||||
|
|
||||||
if ('http-01' === cb.type) {
|
|
||||||
pathname = hostname + acme2.acmeChallengePrefix + token;
|
|
||||||
console.log(
|
|
||||||
"Put the string '" +
|
|
||||||
val /*keyAuthorization*/ +
|
|
||||||
"' into a file at '" +
|
|
||||||
pathname +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'"
|
|
||||||
);
|
|
||||||
console.log("\nThen hit the 'any' key to continue...");
|
|
||||||
} else if ('dns-01' === cb.type) {
|
|
||||||
// forwards-backwards compat
|
|
||||||
pathname =
|
|
||||||
acme2.challengePrefixes['dns-01'] +
|
|
||||||
'.' +
|
|
||||||
hostname.replace(/^\*\./, '');
|
|
||||||
console.log(
|
|
||||||
"Put the string '" +
|
|
||||||
cb.dnsAuthorization +
|
|
||||||
"' into the TXT record '" +
|
|
||||||
pathname +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
console.log('dig TXT ' + pathname + " '" + cb.dnsAuthorization + "'");
|
|
||||||
console.log("\nThen hit the 'any' key to continue...");
|
|
||||||
} else {
|
|
||||||
cb(new Error('[acme-v2] unrecognized challenge type: ' + cb.type));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAny() {
|
|
||||||
console.log("'any' key was hit");
|
|
||||||
process.stdin.pause();
|
|
||||||
process.stdin.removeListener('data', onAny);
|
|
||||||
process.stdin.setRawMode(false);
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdin.setRawMode(true);
|
|
||||||
process.stdin.resume();
|
|
||||||
process.stdin.on('data', onAny);
|
|
||||||
},
|
|
||||||
removeChallenge: function(hostname, key, cb) {
|
|
||||||
console.log('[DEBUG] remove challenge', hostname, key);
|
|
||||||
setTimeout(cb, 1 * 1000);
|
|
||||||
},
|
|
||||||
challengeType: chType,
|
|
||||||
email: email,
|
|
||||||
accountKeypair: accountKeypair,
|
|
||||||
domainKeypair: domainKeypair,
|
|
||||||
domains: web
|
|
||||||
};
|
|
||||||
|
|
||||||
acme2.registerNewAccount(options, function(err, account) {
|
|
||||||
if (err) {
|
|
||||||
console.log('err 2');
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
if (options.debug) console.debug('account:');
|
|
||||||
if (options.debug) console.log(account);
|
|
||||||
|
|
||||||
acme2.getCertificate(options, function(err, fullchainPem) {
|
|
||||||
if (err) {
|
|
||||||
console.log('err 3');
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
console.log('[acme-v2] A fullchain.pem:');
|
|
||||||
console.log(fullchainPem);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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) + '例'
|
||||||
|
);
|
||||||
|
}
|
124
tests/promise.js
124
tests/promise.js
|
@ -1,124 +0,0 @@
|
||||||
// Copyright 2018 AJ ONeal. All rights reserved
|
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* global Promise */
|
|
||||||
module.exports.run = function run(
|
|
||||||
directoryUrl,
|
|
||||||
RSA,
|
|
||||||
web,
|
|
||||||
chType,
|
|
||||||
email,
|
|
||||||
accountKeypair,
|
|
||||||
domainKeypair
|
|
||||||
) {
|
|
||||||
var acme2 = require('../').ACME.create({ RSA: RSA });
|
|
||||||
// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
|
|
||||||
acme2.init(directoryUrl).then(function() {
|
|
||||||
var options = {
|
|
||||||
agreeToTerms: function(tosUrl) {
|
|
||||||
return Promise.resolve(tosUrl);
|
|
||||||
},
|
|
||||||
setChallenge: function(opts) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var pathname;
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('identifier:');
|
|
||||||
console.log(opts.identifier);
|
|
||||||
console.log('hostname:');
|
|
||||||
console.log(opts.hostname);
|
|
||||||
console.log('type:');
|
|
||||||
console.log(opts.type);
|
|
||||||
console.log('token:');
|
|
||||||
console.log(opts.token);
|
|
||||||
console.log('thumbprint:');
|
|
||||||
console.log(opts.thumbprint);
|
|
||||||
console.log('keyAuthorization:');
|
|
||||||
console.log(opts.keyAuthorization);
|
|
||||||
console.log('dnsAuthorization:');
|
|
||||||
console.log(opts.dnsAuthorization);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
if ('http-01' === opts.type) {
|
|
||||||
pathname =
|
|
||||||
opts.hostname +
|
|
||||||
acme2.challengePrefixes['http-01'] +
|
|
||||||
'/' +
|
|
||||||
opts.token;
|
|
||||||
console.log(
|
|
||||||
"Put the string '" +
|
|
||||||
opts.keyAuthorization +
|
|
||||||
"' into a file at '" +
|
|
||||||
pathname +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"echo '" + opts.keyAuthorization + "' > '" + pathname + "'"
|
|
||||||
);
|
|
||||||
} else if ('dns-01' === opts.type) {
|
|
||||||
pathname =
|
|
||||||
acme2.challengePrefixes['dns-01'] +
|
|
||||||
'.' +
|
|
||||||
opts.hostname.replace(/^\*\./, '');
|
|
||||||
console.log(
|
|
||||||
"Put the string '" +
|
|
||||||
opts.dnsAuthorization +
|
|
||||||
"' into the TXT record '" +
|
|
||||||
pathname +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'dig TXT ' + pathname + " '" + opts.dnsAuthorization + "'"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
reject(new Error('[acme-v2] unrecognized challenge type'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("\nThen hit the 'any' key to continue...");
|
|
||||||
|
|
||||||
function onAny() {
|
|
||||||
console.log("'any' key was hit");
|
|
||||||
process.stdin.pause();
|
|
||||||
process.stdin.removeListener('data', onAny);
|
|
||||||
process.stdin.setRawMode(false);
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdin.setRawMode(true);
|
|
||||||
process.stdin.resume();
|
|
||||||
process.stdin.on('data', onAny);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeChallenge: function(opts) {
|
|
||||||
console.log(
|
|
||||||
'[acme-v2] remove challenge',
|
|
||||||
opts.hostname,
|
|
||||||
opts.keyAuthorization
|
|
||||||
);
|
|
||||||
return new Promise(function(resolve) {
|
|
||||||
// hostname, key
|
|
||||||
setTimeout(resolve, 1 * 1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
challengeType: chType,
|
|
||||||
email: email,
|
|
||||||
accountKeypair: accountKeypair,
|
|
||||||
domainKeypair: domainKeypair,
|
|
||||||
domains: web
|
|
||||||
};
|
|
||||||
|
|
||||||
acme2.accounts.create(options).then(function(account) {
|
|
||||||
console.log('[acme-v2] account:');
|
|
||||||
console.log(account);
|
|
||||||
|
|
||||||
acme2.certificates.create(options).then(function(fullchainPem) {
|
|
||||||
console.log('[acme-v2] fullchain.pem:');
|
|
||||||
console.log(fullchainPem);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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