merge unrelated v2 (historical) and v3 (new from scratch)
This commit is contained in:
commit
ad42d34587
|
@ -1,4 +1,7 @@
|
|||
.env
|
||||
*.gz
|
||||
.*.sw*
|
||||
.ignore
|
||||
|
||||
*.pem
|
||||
|
||||
|
@ -14,4 +17,5 @@ coverage
|
|||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
|
||||
node_modules
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"bracketSpacing": true,
|
||||
"printWidth": 80,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"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
|
||||
==================================
|
||||
|
|
510
README.md
510
README.md
|
@ -1,239 +1,355 @@
|
|||
# ACME.js v3 on its way (Nov 1st, 2019)
|
||||
# [ACME.js](https://git.rootprojects.org/root/acme.js) v3
|
||||
|
||||
ACME.js v3 is in private beta and will be available by Nov 1st.
|
||||
| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains)
|
||||
|
||||
Follow the updates on the [campaign page](https://indiegogo.com/at/greenlock),
|
||||
and contribute to support the project and get beta access now.
|
||||
Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers
|
||||
|
||||
| **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2))
|
||||
| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js)
|
||||
| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
|
||||
| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js)
|
||||
Lightweight. Fast. Modern Crypto. Zero external dependecies.
|
||||
|
||||
# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project
|
||||
# Features
|
||||
|
||||
A **Zero (External) Dependency**\* library for building
|
||||
Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates.
|
||||
| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |
|
||||
|
||||
The primary goal of this library is to make it easy to
|
||||
get Accounts and Certificates through Let's Encrypt.
|
||||
|
||||
# Features
|
||||
- [x] Let's Encrypt v2 / ACME RFC 8555 (November 2019)
|
||||
- [x] POST-as-GET support
|
||||
- [x] Secure support for EC and RSA for account and server keys
|
||||
- [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations
|
||||
- [ ] (in-progress) StartTLS Everywhere™
|
||||
- [x] Supports International Domain Names (i.e. `.中国`)
|
||||
- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js)
|
||||
- [x] **http-01** for single or multiple domains per certificate
|
||||
- [x] **dns-01** for wildcards, localhost, private networks, etc
|
||||
- [x] VanillaJS, Zero External Dependencies
|
||||
- [x] Safe, Efficient, Maintained
|
||||
- [x] Node.js\* (v6+)
|
||||
- [x] WebPack
|
||||
- [x] Online Demo
|
||||
- See https://greenlock.domains
|
||||
|
||||
- [x] Let's Encrypt™ v2 / ACME Draft 12
|
||||
- [ ] (in-progress) Let's Encrypt™ v2.1 / ACME Draft 18
|
||||
- [ ] (in-progress) StartTLS Everywhere™
|
||||
- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js)
|
||||
- [x] **http-01** for single or multiple domains per certificate
|
||||
- [x] **dns-01** for wildcards, localhost, private networks, etc
|
||||
- [x] VanillaJS
|
||||
- [x] Zero External Dependencies
|
||||
- [x] Safe, Efficient, Maintained
|
||||
- [x] Works in Node v6+
|
||||
- [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains))
|
||||
\* Although we use `async/await` in the examples, the code is written in CommonJS,
|
||||
with Promises, so you can use it in Node.js and Browsers without transpiling.
|
||||
|
||||
\* <small>The only required dependencies were built by us, specifically for this and related libraries.
|
||||
There are some, truly optional, backwards-compatibility dependencies for node v6.</small>
|
||||
# Want Quick and Easy?
|
||||
|
||||
## Looking for Quick 'n' Easy™?
|
||||
ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers.
|
||||
|
||||
If you want something that's more "batteries included" give
|
||||
[greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
|
||||
a try.
|
||||
If you're looking for maximum convenience, try
|
||||
[Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js).
|
||||
|
||||
- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
|
||||
- <https://git.rootprojects.org/root/greenlock-express.js>
|
||||
|
||||
## v1.7+: Transitional v2 Support
|
||||
# Online Demos
|
||||
|
||||
By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18).
|
||||
- Greenlock for the Web <https://greenlock.domains>
|
||||
- ACME.js Demo <https://rootprojects.org/acme/>
|
||||
|
||||
Although the draft 18 changes themselves don't requiring breaking the API,
|
||||
we've been keeping backwards compatibility for a long time and the API has become messy.
|
||||
We expect that our hosted versions will meet all of yours needs.
|
||||
If they don't, please open an issue to let us know why.
|
||||
|
||||
We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify**
|
||||
the code with a fresh new release.
|
||||
We'd much rather improve the app than have a hundred different versions running in the wild.
|
||||
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
|
||||
|
||||
As of **v1.7** we started adding **transitional support** for the **next major version**, v2.0 of acme-v2.js.
|
||||
We've been really good about backwards compatibility for
|
||||
# QuickStart
|
||||
|
||||
## Recommended Example
|
||||
To make it easy to generate, encode, and decode keys and certificates,
|
||||
ACME.js uses [Keypairs.js](https://git.rootprojects.org/root/keypairs.js)
|
||||
and [CSR.js](https://git.rootprojects.org/root/csr.js)
|
||||
|
||||
Due to the upcoming changes we've removed the old documentation.
|
||||
|
||||
Instead we recommend that you take a look at the
|
||||
[Digital Ocean DNS-01 Example](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
|
||||
|
||||
- [examples/dns-01-digitalocean.js](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
|
||||
|
||||
That's not exactly the new API, but it's close.
|
||||
|
||||
## Let's Encrypt v02 Directory URLs
|
||||
|
||||
```
|
||||
# Production URL
|
||||
https://acme-v02.api.letsencrypt.org/directory
|
||||
```
|
||||
|
||||
```
|
||||
# Staging URL
|
||||
https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
```
|
||||
|
||||
<!--
|
||||
## How to build ACME clients
|
||||
|
||||
As this is intended to build ACME clients, there is not a simple 2-line example
|
||||
(and if you want that, see [greenlock-express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js)).
|
||||
|
||||
I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example:
|
||||
|
||||
```bash
|
||||
node examples/cli.js
|
||||
```
|
||||
|
||||
The example cli has the following prompts:
|
||||
|
||||
```
|
||||
What web address(es) would you like to get certificates for? (ex: example.com,*.example.com)
|
||||
What challenge will you be testing today? http-01 or dns-01? [http-01]
|
||||
What email should we use? (optional)
|
||||
What API style would you like to test? v1-compat or promise? [v1-compat]
|
||||
|
||||
Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
|
||||
|
||||
echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM'
|
||||
|
||||
Then hit the 'any' key to continue...
|
||||
```
|
||||
|
||||
When you've completed the challenge you can hit a key to continue the process.
|
||||
|
||||
If you place the certificate you receive back in `tests/fullchain.pem`
|
||||
you can then test it with `examples/https-server.js`.
|
||||
|
||||
```
|
||||
examples/cli.js
|
||||
examples/genkeypair.js
|
||||
tests/compat.js
|
||||
examples/https-server.js
|
||||
examples/http-server.js
|
||||
```
|
||||
|
||||
-->
|
||||
|
||||
## API
|
||||
|
||||
Status: Small, but breaking changes coming in v2
|
||||
|
||||
This API is a simple evolution of le-acme-core,
|
||||
but tries to provide a better mapping to the new draft 11 APIs.
|
||||
## Node.js
|
||||
|
||||
```js
|
||||
var ACME = require('acme-v2').ACME.create({
|
||||
// used for overriding the default user-agent
|
||||
userAgent: 'My custom UA String',
|
||||
getUserAgentString: function(deps) {
|
||||
return 'My custom UA String';
|
||||
},
|
||||
var ACME = require('@root/acme');
|
||||
```
|
||||
|
||||
// don't try to validate challenges locally
|
||||
skipChallengeTest: false,
|
||||
skipDryRun: false,
|
||||
## WebPack
|
||||
|
||||
// ask if the certificate can be issued up to 10 times before failing
|
||||
retryPoll: 8,
|
||||
// ask if the certificate has been validated up to 6 times before cancelling
|
||||
retryPending: 4,
|
||||
// Wait 1000ms between retries
|
||||
retryInterval: 1000,
|
||||
// Wait 10,000ms after deauthorizing a challenge before retrying
|
||||
deauthWait: 10 * 1000
|
||||
```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;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Generate a Certificate Private Key
|
||||
|
||||
```js
|
||||
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
|
||||
var pem = await Keypairs.export({
|
||||
jwk: certKeypair.private,
|
||||
encoding: 'pem'
|
||||
});
|
||||
|
||||
// Discover Directory URLs
|
||||
ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}>
|
||||
// This should be saved as `privkey.pem`
|
||||
console.log(pem);
|
||||
```
|
||||
|
||||
// Accounts
|
||||
ACME.accounts.create(options); // returns Promise<regr> registration data
|
||||
### Generate a CSR
|
||||
|
||||
options = {
|
||||
email: '<email>', // valid email (server checks MX records)
|
||||
accountKeypair: {
|
||||
// privateKeyPem or privateKeyJwt
|
||||
privateKeyPem: '<ASCII PEM>'
|
||||
The easiest way to generate a Certificate Signing Request will be either with `openssl` or with `@root/CSR`.
|
||||
|
||||
```js
|
||||
var CSR = require('@root/csr');
|
||||
var Enc = require('@root/encoding');
|
||||
|
||||
// 'subject' should be first in list
|
||||
var sortedDomains = ['example.com', 'www.example.com'];
|
||||
var csr = await CSR.csr({
|
||||
jwk: certKeypair.private,
|
||||
domains: sortedDomains,
|
||||
encoding: 'der'
|
||||
}).then(function(der) {
|
||||
return Enc.bufToUrlBase64(der);
|
||||
});
|
||||
```
|
||||
|
||||
### Get Free 90-day SSL Certificate
|
||||
|
||||
Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key,
|
||||
the names of domains to be secured, and a distinctly separate server private key.
|
||||
|
||||
A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record)
|
||||
is a required part of the process, which requires `set` and `remove` callbacks/promises.
|
||||
|
||||
```js
|
||||
var certinfo = await acme.certificates.create({
|
||||
agreeToTerms: function(tos) {
|
||||
return tos;
|
||||
},
|
||||
agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back
|
||||
};
|
||||
account: account,
|
||||
accountKeypair: { privateKeyJwk: accountPrivateKey },
|
||||
csr: csr,
|
||||
domains: sortedDomains,
|
||||
challenges: challenges, // must be implemented
|
||||
customerEmail: null,
|
||||
skipChallengeTests: false,
|
||||
skipDryRun: false
|
||||
});
|
||||
|
||||
// Registration
|
||||
ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }>
|
||||
console.log('Got SSL Certificate:');
|
||||
console.log(results.expires);
|
||||
|
||||
options = {
|
||||
domainKeypair: {
|
||||
privateKeyPem: '<ASCII PEM>'
|
||||
},
|
||||
accountKeypair: {
|
||||
privateKeyPem: '<ASCII PEM>'
|
||||
},
|
||||
domains: ['example.com'],
|
||||
// This should be saved as `fullchain.pem`
|
||||
console.log([results.cert, results.chain].join('\n'));
|
||||
```
|
||||
|
||||
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
|
||||
### 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();
|
||||
},
|
||||
remove: function(opts) {
|
||||
console.log('http-01 remove challenge:', opts.challengeUrl);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# Changelog
|
||||
# IDN - International Domain Names
|
||||
|
||||
- v1.8
|
||||
- more transitional prepwork for new v2 API
|
||||
- support newer (simpler) dns-01 and http-01 libraries
|
||||
- v1.5
|
||||
- perform full test challenge first (even before nonce)
|
||||
- v1.3
|
||||
- Use node RSA keygen by default
|
||||
- No non-optional external deps!
|
||||
- v1.2
|
||||
- fix some API out-of-specness
|
||||
- doc some magic numbers (status)
|
||||
- updated deps
|
||||
- v1.1.0
|
||||
- reduce dependencies (use lightweight @coolaj86/request instead of request)
|
||||
- v1.0.5 - cleanup logging
|
||||
- v1.0.4 - v6- compat use `promisify` from node's util or bluebird
|
||||
- v1.0.3 - documentation cleanup
|
||||
- v1.0.2
|
||||
- use `options.contact` to provide raw contact array
|
||||
- made `options.email` optional
|
||||
- file cleanup
|
||||
- v1.0.1
|
||||
- Compat API is ready for use
|
||||
- Eliminate debug logging
|
||||
- Apr 10, 2018 - tested backwards-compatibility using greenlock.js
|
||||
- Apr 5, 2018 - export http and dns challenge tests
|
||||
- Apr 5, 2018 - test http and dns challenges (success and failure)
|
||||
- Apr 5, 2018 - test subdomains and its wildcard
|
||||
- Apr 5, 2018 - test two subdomains
|
||||
- Apr 5, 2018 - test wildcard
|
||||
- Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js)
|
||||
- Mar 21, 2018 - _mostly_ matches le-acme-core.js API
|
||||
- Mar 21, 2018 - can now accept values (not hard coded)
|
||||
- Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded)
|
||||
- Mar 20, 2018 - download certificate
|
||||
- Mar 20, 2018 - poll for status
|
||||
- Mar 20, 2018 - finalize order (submit csr)
|
||||
- Mar 20, 2018 - generate domain keypair
|
||||
- Mar 20, 2018 - respond to challenges
|
||||
- Mar 16, 2018 - get challenges
|
||||
- Mar 16, 2018 - new order
|
||||
- Mar 15, 2018 - create account
|
||||
- Mar 15, 2018 - generate account keypair
|
||||
- Mar 15, 2018 - get nonce
|
||||
- Mar 15, 2018 - get directory
|
||||
Convert domain names to `punycode` before creating the certificate:
|
||||
|
||||
# Legal
|
||||
```js
|
||||
var punycode = require('punycode');
|
||||
|
||||
[acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) |
|
||||
acme.certificates.create({
|
||||
// ...
|
||||
domains: ['example.com', 'www.example.com'].map(function(name) {
|
||||
return punycode.toASCII(name);
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
The punycode library itself is lightweight and dependency-free.
|
||||
It is available both in node and for browsers.
|
||||
|
||||
# Testing
|
||||
|
||||
You will need to use one of the [`acme-dns-01-*` plugins](https://www.npmjs.com/search?q=acme-dns-01-)
|
||||
to run the test locally.
|
||||
|
||||
You'll also need a `.env` that looks something like the one in `examples/example.env`:
|
||||
|
||||
```bash
|
||||
ENV=DEV
|
||||
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
|
||||
BASE_DOMAIN=test.example.com
|
||||
CHALLENGE_TYPE=dns-01
|
||||
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
|
||||
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
# Get the repo and change directories into it
|
||||
git clone https://git.rootprojects.org/root/bluecrypt-acme.js
|
||||
pushd bluecrypt-acme.js/
|
||||
|
||||
# Install the challenge plugin you'll use for the tests
|
||||
npm install --save-dev acme-dns-01-digitalocean
|
||||
|
||||
# Copy the sample .env file
|
||||
rsync -av examples/example.env .env
|
||||
|
||||
# Edit the config file to use a domain in your account, and your API token
|
||||
#vim .env
|
||||
code .env
|
||||
|
||||
# Run the tests
|
||||
node tests/index.js
|
||||
```
|
||||
|
||||
# Developing
|
||||
|
||||
You can see `<script>` tags in the `index.html` in the repo, which references the original
|
||||
source files.
|
||||
|
||||
Join `@rootprojects` `#general` on [Keybase](https://keybase.io) if you'd like to chat with us.
|
||||
|
||||
# Commercial Support
|
||||
|
||||
We have both commercial support and commercial licensing available.
|
||||
|
||||
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
|
||||
Enterprise, and Internal installations, integrations, and deployments.
|
||||
|
||||
We also offer consulting for all-things-ACME and Let's Encrypt.
|
||||
|
||||
# Legal & Rules of the Road
|
||||
|
||||
Greenlock™ is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
|
||||
|
||||
The rule of thumb is "attribute, but don't confuse". For example:
|
||||
|
||||
> Built with [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) (a [Root](https://rootprojects.org) project).
|
||||
|
||||
Please [contact us](mailto:aj@therootcompany.com) if have any questions in regards to our trademark,
|
||||
attribution, and/or visible source policies. We want to build great software and a great community.
|
||||
|
||||
[ACME.js](https://git.rootprojects.org/root/acme.js) |
|
||||
MPL-2.0 |
|
||||
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||
|
|
|
@ -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
|
||||
ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com
|
||||
DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
ENV=DEV
|
||||
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
|
||||
BASE_DOMAIN=test.example.com
|
||||
CHALLENGE_TYPE=dns-01
|
||||
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
|
||||
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
|
||||
|
|
|
@ -11,7 +11,10 @@ if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) {
|
|||
var privkeyPem = RSA.exportPrivatePem(keypair);
|
||||
console.log(privkeyPem);
|
||||
|
||||
fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem);
|
||||
fs.writeFileSync(
|
||||
__dirname + '/../tests/account.privkey.pem',
|
||||
privkeyPem
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|