Compare commits

..

127 Commits

Author SHA1 Message Date
Sam Lord 0aa939a227 Bug fix: Polling status using POST-as-GET wherever possible
Avoid repeating finalize POST request and challenge POST requests by
using POST-as-GET requests instead. Allows for testing with Pebble,
and more correctly follows the spec.
2021-04-08 14:19:33 +01:00
AJ ONeal bef931f28f 3.1.0 2020-07-28 16:03:07 -06:00
AJ ONeal eb432571ca Bugfix jwk / kid mutually exclusive
See root/greenlock-express.js#38
2020-07-28 16:02:45 -06:00
AJ ONeal 29a47e8fa4 make Prettier v2 2020-07-28 15:53:50 -06:00
AJ ONeal 87e3555a5a v3.0.10: fix CSR package dep, add maintainer timeout 2020-04-21 00:05:45 -06:00
AJ ONeal 569c922eb0 add timeout to maintainer request 2020-04-21 00:04:47 -06:00
AJ ONeal d10482697b move @root/csr to regular dependencies a la root/acme.js#3 2020-04-21 00:02:10 -06:00
AJ ONeal aa324e2a29 v3.0.9: bugfix error handling 2020-01-10 16:55:44 -07:00
AJ ONeal e8c46db062 Update README 2019-10-30 18:27:11 -06:00
AJ ONeal 6352961fea v3.0.8: bump for parity with git tag 2019-10-29 05:02:58 +00:00
AJ ONeal 333605d9b8 v3.0.7: private notify function 2019-10-28 20:51:03 -06:00
AJ ONeal 86068fe015 v3.0.6: minor updates for greenlock 2019-10-28 03:21:15 -06:00
AJ ONeal cf0ee1c064 logging fixes 2019-10-28 02:26:27 -06:00
AJ ONeal 606dcf3c4f typo fix 2019-10-27 03:58:22 -06:00
AJ ONeal 0803517711 typo fix 2019-10-27 03:07:19 -06:00
AJ ONeal 0b91d9a26d add https sni example 2019-10-27 00:55:40 -06:00
AJ ONeal 0743aa5280 typo fix 2019-10-26 11:24:01 -06:00
AJ ONeal e388bc31bc v3.0.5: npm bump for docs 2019-10-26 00:45:19 -06:00
AJ ONeal 754c623cd1 v3.0.4: update docs 2019-10-26 00:41:15 -06:00
AJ ONeal 0107bc1d1f highlight example more 2019-10-26 00:33:10 -06:00
AJ ONeal 293d950d8c highlight example 2019-10-26 00:32:20 -06:00
AJ ONeal d6a3a7939b typo fix html 2019-10-26 00:30:13 -06:00
AJ ONeal fcbffdc0f9 update readme 2019-10-26 00:23:12 -06:00
AJ ONeal e447d71112 TODO 2019-10-26 00:13:48 -06:00
AJ ONeal 5490f858d9 v3.0.2: add missing dirs 2019-10-26 00:12:14 -06:00
AJ ONeal a99a0cc211 v3.0.1: documented + examples 2019-10-26 00:03:43 -06:00
AJ ONeal e0bec09e43 v3.0.0: at last! 2019-10-25 05:20:44 -06:00
AJ ONeal 148846b18a silence! 2019-10-25 04:57:09 -06:00
AJ ONeal b1c591b6ed make prettier 2019-10-25 04:55:03 -06:00
AJ ONeal 4e7ff0d9e8 add maintainer notices 2019-10-25 04:54:54 -06:00
AJ ONeal b39a3763cf request cleanup 2019-10-25 04:54:40 -06:00
AJ ONeal 54cda5a888 use correct version in UA 2019-10-24 18:53:08 -06:00
AJ ONeal 90c7154a24 API and test cleanup 2019-10-24 18:49:42 -06:00
AJ ONeal 161e9183c6 add User-Agent string as per RFC 8555 and RFC 7231 2019-10-24 18:48:34 -06:00
AJ ONeal 7f868f350b remove cruft 2019-10-24 18:48:25 -06:00
AJ ONeal 30f4306c05 add request/response examples 2019-10-24 18:45:51 -06:00
AJ ONeal 0efa94eeb0 update API and tests 2019-10-24 11:39:25 -06:00
AJ ONeal f05e9db38e backport all the things 2019-10-23 01:44:55 -06:00
AJ ONeal 7e6a66c1d8 update docs 2019-10-22 20:02:30 -06:00
AJ ONeal b1046222dc backports: POST-as-GET, error handling, etc 2019-10-22 19:50:08 -06:00
AJ ONeal d25fa6756c remove cruft 2019-10-21 17:03:26 -06:00
AJ ONeal c89e5b7882 remove cruft 2019-10-21 16:32:02 -06:00
AJ ONeal 4b79b0bb3a nix Bluecrypt branding 2019-10-21 15:23:36 -06:00
AJ ONeal ad42d34587 merge unrelated v2 (historical) and v3 (new from scratch) 2019-10-21 15:22:07 -06:00
AJ ONeal d7b3e2e1db remove v2 before awkward merging of v3 2019-10-21 13:59:58 -06:00
AJ ONeal 9139d89143 v1.8.6: Notify of upcoming ACME.js v3 2019-10-19 06:07:16 -06:00
AJ ONeal e214f5e639 v1.8.5: change package.main to due to Win 10 cmd PATHEXT 2019-09-07 02:30:46 -06:00
AJ ONeal 96a6de30a1 v1.8.4: add support banner 2019-09-03 19:31:09 -06:00
AJ ONeal a4f92e260c fix incorrect error message 2019-07-31 16:02:15 -06:00
AJ ONeal 0599acab6d support init(deps) 2019-06-15 14:31:03 -06:00
AJ ONeal 90477942d1 add type, for greenlock 2019-06-14 02:23:52 -06:00
AJ ONeal e6497fe34b v1.8: transitional support for v2.0 2019-06-14 01:32:54 -06:00
AJ ONeal dfbee8aa79 add .prettierrc, and make prettier 2019-06-13 01:55:25 -06:00
AJ ONeal 17a1535dcc update comment 2019-04-07 21:16:02 -06:00
AJ ONeal 54e9e9ec16 v1.7.7: revert v1.7.6 2019-04-07 21:08:58 -06:00
AJ ONeal a750d1b0b4 v1.7.6: add http-01 url to challenge 2019-04-07 14:54:02 -06:00
AJ ONeal 3f4e5adeef v1.7.5: bugfix unchecked property thanks to #19 2019-04-04 14:08:41 -06:00
AJ ONeal ea97f537ef v2.7.3: make dry-run properly shows wildcards 2019-04-02 21:34:38 -06:00
AJ ONeal 6deb67d740 v1.7.3: don't wait so longer on dns test 2019-04-02 20:40:07 -06:00
AJ ONeal 1195956ce1 v1.7.2: don't set challenge twice 2019-04-02 19:25:41 -06:00
AJ ONeal 6521121548 v1.7.1: don't self-poison dns cache, more consistency 2019-04-02 19:13:58 -06:00
AJ ONeal b1d566d54e v1.7.0: better error checking and challenge type handling 2019-04-02 16:04:15 -06:00
AJ ONeal 401535a5ab v1.6.0: switch to latest rsa-compat 2019-03-31 02:55:26 -06:00
AJ ONeal ddeaeb17d5 v1.5.3: merge fix for #11 2019-03-14 12:15:08 -06:00
AJ ONeal 6d34655276 #11 skip challenge when valid 2019-02-04 23:04:24 -07:00
AJ ONeal 8175a08495 v1.5.2: fix dns-01 wildcard bug 2019-02-04 22:30:55 -07:00
AJ ONeal 382ef3c95c v1.5.1: more detailed error messages 2018-12-22 15:09:19 -07:00
AJ ONeal d802fb4957 v1.5.0: perform full test challenge first 2018-12-22 05:27:22 -07:00
AJ ONeal 85a38f7b54 v1.3.1: reduce deps, update rsa-compat, fix rando JWK bug 2018-12-16 21:19:20 -07:00
AJ ONeal 2406c870e6 note need to limit download size 2018-11-04 13:42:55 -07:00
AJ ONeal 3ae21fe62a v1.2.1: made magic numbers (for status polling) configurable, updated deps 2018-08-16 18:32:14 -06:00
AJ ONeal 2051fb0e4b v1.2.0: Fix #8 Production API changed to be in-spec 2018-07-12 01:50:24 -06:00
AJ ONeal 1649b52f24 Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/acme-v2.js 2018-07-11 22:35:48 -06:00
AJ ONeal 0f20783f12 add comment on privkey and other useful pem data 2018-07-11 22:35:38 -06:00
AJ ONeal aa42853639 Add '.jshintrc' 2018-07-11 18:07:53 +00:00
AJ ONeal 8e345c09ae Delete '.jshintrc' 2018-07-11 18:06:48 +00:00
AJ ONeal e704419cdb simplify keywords, add draft-12 2018-07-07 14:00:22 -06:00
AJ ONeal 0a5a72e2fc * the other callback 2018-07-04 00:36:33 -06:00
AJ ONeal 6c811d880c add .gitignore 2018-07-04 00:29:28 -06:00
AJ ONeal 668e2bb0ac update compat to allow testing dns 2018-07-04 00:28:00 -06:00
AJ ONeal 8117b1fd66 add .jshintrc 2018-07-04 00:11:20 -06:00
AJ ONeal 2ba7db1327 update line endings (and add brackets to logs...) 2018-07-04 00:10:48 -06:00
AJ ONeal 0214d80f80 v1.1.0 2018-06-19 01:28:24 -06:00
AJ ONeal f7d1c5615e request => @coolaj86/urequest 2018-06-19 01:28:03 -06:00
John Shaver 16cbe77dff 1.0.9 2018-06-06 12:34:23 -07:00
jshaver 479f6d99e6 Merge branch 'master' of jshaver/acme-v2.js into master 2018-06-06 19:27:39 +00:00
John Shaver 119f5a9ae4 Fixed error handling for non promise setChallenge. 2018-06-05 16:07:54 -07:00
AJ ONeal 208834130d v1.0.8 2018-05-23 03:09:41 -06:00
AJ ONeal 6b78aa7cee update LICENSE 2018-05-23 03:08:58 -06:00
AJ ONeal 84c3e91ea8 v1.0.7 2018-05-18 00:58:15 -06:00
AJ ONeal bfff22f053 improve error message as per https://git.coolaj86.com/coolaj86/greenlock.js/issues/12 2018-05-18 00:55:50 -06:00
AJ ONeal d0745a6347 v1.0.6 2018-05-09 23:49:19 -06:00
AJ ONeal d54c1380f3 better error handling 2018-05-09 23:48:52 -06:00
AJ ONeal 473f373de3 better error handling 2018-05-09 23:46:43 -06:00
AJ ONeal 3cf7824bed remove TODO section (now in issues) 2018-04-24 11:58:45 -06:00
AJ ONeal 4e2649d797 update changelog 2018-04-24 11:58:14 -06:00
AJ ONeal 290251a1b0 v1.0.5 remove junk logging 2018-04-24 11:56:46 -06:00
AJ ONeal 474bb64004 v1.0.4 backcompat with node v6 2018-04-24 11:38:45 -06:00
AJ ONeal e2c3faeb82 cleanup 2018-04-20 01:48:17 -06:00
AJ ONeal deb87cdec9 changelog v1.0.3 2018-04-20 01:44:41 -06:00
AJ ONeal 8c8e3d843a v1.0.3 2018-04-20 01:42:24 -06:00
AJ ONeal 12d5518be3 cleanup 2018-04-20 07:41:46 +00:00
AJ ONeal d0a58be97d move sponsorship 2018-04-20 06:51:02 +00:00
AJ ONeal 65ff4a0feb add other examples 2018-04-16 01:10:48 +00:00
AJ ONeal a88486c313 v1.0.2 2018-04-16 01:04:06 +00:00
AJ ONeal 9cdac50dbc v1.0.1 2018-04-13 23:26:29 +00:00
AJ ONeal fdd5a88de8 typo fix 2018-04-13 23:23:08 +00:00
AJ ONeal 71e0faec95 v1 2018-04-11 18:03:40 +00:00
AJ ONeal e31e72b0b8 typo fix 2018-04-11 17:37:10 +00:00
AJ ONeal 263eed0475 closer to v1 2018-04-11 17:34:18 +00:00
AJ ONeal b630c118cc bump 2018-04-11 07:22:58 +00:00
AJ ONeal da8b49d46b working even better 2018-04-11 07:22:42 +00:00
AJ ONeal 3a6269aafa more testing 2018-04-05 05:44:02 -06:00
AJ ONeal 2e747bada2 sequence auths, more testing 2018-04-05 03:37:41 -06:00
AJ ONeal 38cefafe33 yay for wildcard test passing! 2018-04-05 02:48:10 -06:00
AJ ONeal f486bca73e yay for promise-only tests working 2018-04-05 02:28:29 -06:00
AJ ONeal ef0505ed69 yay for test with callback options working 2018-04-05 02:13:20 -06:00
AJ ONeal fe5a48764b yay for backwards compat tested and working 2018-04-05 01:31:57 -06:00
AJ ONeal 27fb85ed9c add keywords 2018-03-21 01:41:10 -06:00
AJ ONeal b70b1002b9 add homepage 2018-03-21 01:38:21 -06:00
AJ ONeal 1f71a91979 note sponsorship 2018-03-21 01:35:28 -06:00
AJ ONeal a38d751cfa use supplied directoryUrl 2018-03-21 01:33:54 -06:00
AJ ONeal 08e6fdc1a7 not hard-coded, almost backwards compatible 2018-03-21 01:26:23 -06:00
AJ ONeal afbcef688f successful test! yay! 2018-03-20 01:24:36 -06:00
AJ ONeal df022959e4 hard code more test functionality 2018-03-16 00:59:40 -06:00
AJ ONeal 2a3849cf1b add note on spec, junk version 2018-03-15 00:43:41 -06:00
AJ ONeal 4c4eaa83b7 initial commit 2018-03-15 00:41:00 -06:00
71 changed files with 4083 additions and 2625 deletions

16
.gitignore vendored
View File

@ -2,4 +2,20 @@
*.gz
.*.sw*
.ignore
*.pem
# Logs
logs
*.log
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

18
.jshintrc Normal file
View File

@ -0,0 +1,18 @@
{ "node": true
, "browser": true
, "jquery": true
, "globals": { "angular": true, "Promise": true }
, "indent": 2
, "onevar": true
, "laxcomma": true
, "laxbreak": true
, "curly": true
, "nonbsp": true
, "eqeqeq": true
, "immed": true
, "undef": true
, "unused": true
, "latedef": true
}

53
CHANGELOG.md Normal file
View File

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

506
README.md
View File

@ -1,38 +1,39 @@
# [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js)
# Let's Encrypt™ + JavaScript = [ACME.js](https://git.rootprojects.org/root/acme.js)
Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers
| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub)
Lightweight. Fast. Modern Crypto. Zero dependecies.
## Automated Certificate Management Environment
# Features
ACME ([RFC 8555](https://tools.ietf.org/html/rfc8555)) is the protocol that powers **Let's Encrypt**.
| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |
ACME.js is a _low-level_ client that speaks RFC 8555 to get Free SSL certificates through Let's Encrypt.
- [x] Let's Encrypt v2.1+ (November 2019)
- [x] ACME draft 15 (supports POST-as-GET)
- [x] Secure support for EC and RSA for account and server keys
- [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations
- [x] Supports International Domain Names (i.e. `.中国`)
- [x] VanillaJS, Zero External Dependencies
- [x] Node.js\* (v6+)
- [x] WebPack
Looking for an **easy**, _high-level_ client? Check out [Greenlock.js](https://git.rootprojects.org/root/greenlock.js).
\* 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.
# Quick Start
# Want Quick and Easy?
```js
var acme = ACME.create({ maintainerEmail, packageAgent, notify });
await acme.init(directoryUrl);
ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers.
// Create Let's Encrypt Account
var accountOptions = { subscriberEmail, agreeToTerms, accountKey };
var account = await acme.accounts.create(accountOptions);
If you're looking for maximum convenience, try
[Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js).
// Validate Domains
var certificateOptions = { account, accountKey, csr, domains, challenges };
var pems = await acme.certificates.create(certificateOptions);
- <https://git.rootprojects.org/root/greenlock-express.js>
// Get SSL Certificate
var fullchain = pems.cert + '\n' + pems.chain + '\n';
await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii');
```
# Online Demos
# Online Demo
- Greenlock for the Web <https://greenlock.domains>
- ACME.js Demo <https://rootprojects.org/acme/>
See https://greenlock.domains
<!--
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.
@ -40,61 +41,114 @@ If they don't, please open an issue to let us know why.
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.
# QuickStart
-->
To make it easy to generate, encode, and decode keys and certificates,
ACME.js embeds [Keypairs.js](https://git.rootprojects.org/root/bluecrypt-keypairs.js)
and [CSR.js](https://git.rootprojects.org/root/bluecrypt-csr.js)
# Features
## Node.js
| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |
Supports the latest (Nov 2019) release of Let's Encrypt in a small, lightweight, Vanilla JS package.
- [x] Let's Encrypt v2
- [x] ACME RFC 8555
- [x] November 2019
- [x] POST-as-GET
- [ ] StartTLS Everywhere&trade; (in-progress)
- [x] IDN (i.e. `.中国`)
- [x] ECDSA and RSA keypairs
- [x] JWK
- [x] PEM
- [x] DER
- [x] Native Crypto in Node.js
- [x] WebCrypto in Browsers
- [x] Domain Validation Plugins
- [x] tls-alpn-01
- [x] http-01
- [x] dns-01
- [x] **Wildcards**
- [x] **Localhost**
- [x] Private Networks
- [x] [Create your own](https://git.rootprojects.org/root/acme-challenge-test.js)
- [x] Vanilla JS\*
- [x] No Transpiling Necessary!
- [x] Node.js
- [x] Browsers
- [x] WebPack
- [x] Zero External Dependencies
- [x] Commercial Support
- [x] Safe, Efficient, Maintained
\* Although we use `async/await` in the examples,
the codebase is written entirely in Common JS.
# Use Cases
- Home Servers
- IoT
- Enterprise On-Prem
- Web Hosting
- Cloud Services
- Localhost Development
# API
The public API encapsulates the three high-level steps of the ACME protocol:
1. API Discovery
2. Account Creation
- Subscriber Agreement
3. Certificate Issuance
- Certificate Request
- Authorization Challenges
- Challenge Presentation
- Certificate Redemption
## API Overview
The core API can be show in just four functions:
```js
var ACME = require('@root/acme');
ACME.create({ maintainerEmail, packageAgent, notify });
acme.init(directoryUrl);
acme.accounts.create({ subscriberEmail, agreeToTerms, accountKey });
acme.certificates.create({
customerEmail, // do not use
account,
accountKey,
csr,
domains,
challenges
});
```
## WebPack
```html
<meta charset="UTF-8" />
```
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
Helper Functions
```js
var ACME = require('@root/acme');
ACME.computeChallenge({
accountKey,
hostname: 'example.com',
challenge: { type: 'dns-01', token: 'xxxx' }
});
```
## Vanilla JS
| Parameter | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------- |
| account | an object containing the Let's Encrypt Account ID as "kid" (misnomer, not actually a key id/thumbprint) |
| accountKey | an RSA or EC public/private keypair in JWK format |
| agreeToTerms | set to `true` to agree to the Let's Encrypt Subscriber Agreement |
| challenges | the 'http-01', 'alpn-01', and/or 'dns-01' challenge plugins (`get`, `set`, and `remove` callbacks) to use |
| csr | a Certificate Signing Request (CSR), which may be generated with `@root/csr`, openssl, or another |
| customerEmail | Don't use this. Given as an example to differentiate between Maintainer, Subscriber, and End-User |
| directoryUrl | should be the Let's Encrypt Directory URL<br>`https://acme-staging-v02.api.letsencrypt.org/directory` |
| domains | the list of altnames (subject first) that are listed in the CSR and will be listed on the certificate |
| maintainerEmail | should be a contact for the author of the code to receive critical bug and security notices |
| notify | all callback for logging events and errors in the form `function (ev, args) { ... }` |
| packageAgent | should be an RFC72321-style user-agent string to append to the ACME client (ex: mypackage/v1.1.1) |
| skipChallengeTests | do not do a self-check that the ACME-issued challenges will pass (not recommended) |
| skipDryRun: false | do not do a self-check with self-issued challenges (not recommended) |
| subscriberEmail | should be a contact for the service provider to receive renewal failure notices and manage the ACME account |
```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
**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.
@ -112,176 +166,182 @@ 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
## Events
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).
These `notify` events are intended for _logging_ and debugging, NOT as a data API.
The `init()` method takes a _directory url_ and initializes internal state according to its response.
| Event Name | Example Message |
| -------------------- | --------------------------------------------------------------------------------- |
| `certificate_order` | `{ subject: 'example.com', altnames: ['...'], account: { key: { kid: '...' } } }` |
| `challenge_select` | `{ altname: '*.example.com', type: 'dns-01' }` |
| `challenge_status` | `{ altname: '*.example.com', type: 'dns-01', status: 'pending' }` |
| `challenge_remove` | `{ altname: '*.example.com', type: 'dns-01' }` |
| `certificate_status` | `{ subject: 'example.com', status: 'valid' }` |
| `warning` | `{ message: 'what went wrong', description: 'what action to take about it' }` |
| `error` | `{ message: 'a background process failed, and it may have side-effects' }` |
Note: DO NOT rely on **undocumented properties**. They are experimental and **will break**.
If you have a use case for a particular property **open an issue** - we can lock it down and document it.
# Example (Full Walkthrough)
### See [examples/README.md](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/README.md)
A basic example includes the following:
1. Initialization
- maintainer contact
- package user-agent
- log events
2. Discover API
- retrieves Terms of Service and API endpoints
3. Get Subscriber Account
- create an ECDSA (or RSA) Account key in JWK format
- agree to terms
- register account by the key
4. Prepare a Certificate Signing Request
- create a RSA (or ECDSA) Server key in PEM format
- select domains
- choose challenges
- sign CSR
- order certificate
[examples/README.md](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/README.md)
covers all of these steps, with comments.
# Install
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)
<details>
<summary>Node.js</summary>
```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;
}
);
npm install --save @root/acme
```
### 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;
});
});
var ACME = require('@root/acme');
```
### Generate a Certificate Private Key
</details>
```js
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
<details>
<summary>WebPack</summary>
// This should be saved as `privkey.pem`
console.log(pem);
```html
<meta charset="UTF-8" />
```
### Generate a CSR
The easiest way to generate a Certificate Signing Request will be either with `openssl` or with `@root/CSR`.
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
```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);
});
var ACME = require('@root/acme');
```
### Get Free 90-day SSL Certificate
</details>
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.
<details>
<summary>Vanilla JS</summary>
A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record)
is a required part of the process, which requires `set` and `remove` callbacks/promises.
```js
var certinfo = await acme.certificates.create({
agreeToTerms: function(tos) {
return tos;
},
account: account,
accountKeypair: { privateKeyJwk: accountPrivateKey },
csr: csr,
domains: sortedDomains,
challenges: challenges, // must be implemented
customerEmail: null,
skipChallengeTests: false,
skipDryRun: false
});
console.log('Got SSL Certificate:');
console.log(results.expires);
// This should be saved as `fullchain.pem`
console.log([results.cert, results.chain].join('\n'));
```html
<meta charset="UTF-8" />
```
### Example "Challenge" Implementation
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
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.
```html
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.all.js"></script>
```
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.
`acme.min.js`
```html
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.all.min.js"></script>
```
Use
```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();
var ACME = window['@root/acme'];
```
</details>
# Challenge Callbacks
The challenge callbacks are documented in the [test suite](https://git.rootprojects.org/root/acme-dns-01-test.js),
essentially:
```js
function create(options) {
var plugin = {
init: async function(deps) {
// for http requests
plugin.request = deps.request;
},
remove: function(opts) {
console.log('http-01 remove challenge:', opts.challengeUrl);
return Promise.resolve();
}
}
};
zones: async function(args) {
// list zones relevant to the altnames
},
set: async function(args) {
// set TXT record
},
get: async function(args) {
// get TXT records
},
remove: async function(args) {
// remove TXT record
},
// how long to wait after *all* TXT records are set
// before presenting them for validation
propagationDelay: 5000
};
return plugin;
}
```
# IDN - International Domain Names
The `http-01` plugin is similar, but without `zones` or `propagationDelay`.
Convert domain names to `punycode` before creating the certificate:
Many challenge plugins are already available for popular platforms.
```js
var punycode = require('punycode');
Search `acme-http-01-` or `acme-dns-01-` on npm to find more.
acme.certificates.create({
// ...
domains: ['example.com', 'www.example.com'].map(function(name) {
return punycode.toASCII(name);
})
});
| Type | Service | Plugin |
| ----------- | ----------------------------------------------------------------------------------- | ------------------------ |
| dns-01 | CloudFlare | acme-dns-01-cloudflare |
| dns-01 | [Digital Ocean](https://git.rootprojects.org/root/acme-dns-01-digitalocean.js) | acme-dns-01-digitalocean |
| dns-01 | [DNSimple](https://git.rootprojects.org/root/acme-dns-01-dnsimple.js) | acme-dns-01-dnsimple |
| dns-01 | [DuckDNS](https://git.rootprojects.org/root/acme-dns-01-duckdns.js) | acme-dns-01-duckdns |
| http-01 | File System / [Web Root](https://git.rootprojects.org/root/acme-http-01-webroot.js) | acme-http-01-webroot |
| dns-01 | [GoDaddy](https://git.rootprojects.org/root/acme-dns-01-godaddy.js) | acme-dns-01-godaddy |
| dns-01 | [Gandi](https://git.rootprojects.org/root/acme-dns-01-gandi.js) | acme-dns-01-gandi |
| dns-01 | [NameCheap](https://git.rootprojects.org/root/acme-dns-01-namecheap.js) | acme-dns-01-namecheap |
| dns-01 | [Name&#46;com](https://git.rootprojects.org/root/acme-dns-01-namedotcom.js) | acme-dns-01-namedotcom |
| dns-01 | Route53 (AWS) | acme-dns-01-route53 |
| http-01 | S3 (AWS, Digital Ocean, Scaleway) | acme-http-01-s3 |
| dns-01 | [Vultr](https://git.rootprojects.org/root/acme-dns-01-vultr.js) | acme-dns-01-vultr |
| dns-01 | [Build your own](https://git.rootprojects.org/root/acme-dns-01-test.js) | acme-dns-01-test |
| http-01 | [Build your own](https://git.rootprojects.org/root/acme-http-01-test.js) | acme-http-01-test |
| tls-alpn-01 | [Contact us](mailto:support@therootcompany.com) | - |
# Running the Tests
```bash
npm test
```
The punycode library itself is lightweight and dependency-free.
It is available both in node and for browsers.
## Usa a dns-01 challenge
# Testing
Although you can run the tests from a public facing server, its easiest to do so using a dns-01 challenge.
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
MAINTAINER_EMAIL=letsencrypt+staging@example.com
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
BASE_DOMAIN=test.example.com
CHALLENGE_TYPE=dns-01
@ -289,16 +349,22 @@ CHALLENGE_PLUGIN=acme-dns-01-digitalocean
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
```
For example:
### 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/
git clone https://git.rootprojects.org/root/acme.js
pushd acme.js/
# Install the challenge plugin you'll use for the tests
npm install --save-dev acme-dns-01-digitalocean
```
## Create a `.env` config
You'll need a `.env` in the project root that looks something like the one in `examples/example.env`:
```bash
# Copy the sample .env file
rsync -av examples/example.env .env
@ -312,27 +378,55 @@ 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.
# Contributions
Did this project save you some time? Maybe make your day? Even save the day?
Please say "thanks" via Paypal or Patreon:
- Paypal: [\$5](https://paypal.me/rootprojects/5) | [\$10](https://paypal.me/rootprojects/10) | Any amount: <paypal@therootcompany.com>
- Patreon: <https://patreon.com/rootprojects>
Where does your contribution go?
[Root](https://therootcompany.com) is a collection of experts
who trust each other and enjoy working together on deep-tech,
Indie Web projects.
Our goal is to operate as a sustainable community.
Your contributions - both in code and _especially_ financially -
help to not just this project, but also our broader work
of [projects](https://rootprojects.org) that fuel the **Indie Web**.
Also, we chat on [Keybase](https://keybase.io)
in [#rootprojects](https://keybase.io/team/rootprojects)
# Commercial Support
We have both commercial support and commercial licensing available.
Do you need...
- more features?
- bugfixes, on _your_ timeline?
- custom code, built by experts?
- commercial support and licensing?
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
Enterprise, and Internal installations, integrations, and deployments.
We have both commercial support and commercial licensing available.
We also offer consulting for all-things-ACME and Let's Encrypt.
# Legal &amp; Rules of the Road
Greenlock&trade; is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
ACME.js&trade; is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
The rule of thumb is "attribute, but don't confuse". For example:
> Built with [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) (a [Root](https://rootprojects.org) project).
> Built with [ACME.js](https://git.rootprojects.org/root/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.

175
account.js Normal file
View File

@ -0,0 +1,175 @@
'use strict';
var A = module.exports;
var U = require('./utils.js');
var Keypairs = require('@root/keypairs');
var Enc = require('@root/encoding/bytes');
var agreers = {};
A._getAccountKid = function (me, options) {
// It's just fine if there's no account, we'll go get the key id we need via the existing key
var kid =
options.kid ||
(options.account && options.account.key && options.account.key.kid);
if (kid) {
return Promise.resolve(kid);
}
//return Promise.reject(new Error("must include KeyID"));
// This is an idempotent request. It'll return the same account for the same public key.
return A._registerAccount(me, options).then(function (account) {
return account.key.kid;
});
};
// ACME RFC Section 7.3 Account Creation
/*
{
"protected": base64url({
"alg": "ES256",
"jwk": {...},
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
"url": "https://example.com/acme/new-account"
}),
"payload": base64url({
"termsOfServiceAgreed": true,
"onlyReturnExisting": false,
"contact": [
"mailto:cert-admin@example.com",
"mailto:admin@example.com"
]
}),
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
}
*/
A._registerAccount = function (me, options) {
//#console.debug('[ACME.js] accounts.create');
function agree(agreed) {
var err;
if (!agreed) {
err = new Error("must agree to '" + me._tos + "'");
err.code = 'E_AGREE_TOS';
throw err;
}
return true;
}
function getAccount() {
return U._importKeypair(options.accountKey).then(function (pair) {
var contact;
if (options.contact) {
contact = options.contact.slice(0);
} else if (options.subscriberEmail) {
contact = ['mailto:' + options.subscriberEmail];
}
var accountRequest = {
termsOfServiceAgreed: true,
onlyReturnExisting: false,
contact: contact
};
var pub = pair.public;
return attachExtAcc(pub, accountRequest).then(function (accReq) {
var payload = JSON.stringify(accReq);
return U._jwsRequest(me, {
accountKey: options.accountKey,
url: me._directoryUrls.newAccount,
protected: { kid: false, jwk: pair.public },
payload: Enc.strToBuf(payload)
}).then(function (resp) {
var account = resp.body;
if (resp.statusCode < 200 || resp.statusCode >= 300) {
if ('string' !== typeof account) {
account = JSON.stringify(account);
}
throw new Error(
'account error: ' +
resp.statusCode +
' ' +
account +
'\n' +
payload
);
}
// the account id url is the "kid"
var kid = resp.headers.location;
if (!account) {
account = { _emptyResponse: true };
}
if (!account.key) {
account.key = {};
}
account.key.kid = kid;
return account;
});
});
});
}
// for external accounts (probably useless, but spec'd)
function attachExtAcc(pubkey, accountRequest) {
if (!options.externalAccount) {
return Promise.resolve(accountRequest);
}
return Keypairs.signJws({
// TODO is HMAC the standard, or is this arbitrary?
secret: options.externalAccount.secret,
protected: {
alg: options.externalAccount.alg || 'HS256',
kid: options.externalAccount.id,
url: me._directoryUrls.newAccount
},
payload: Enc.strToBuf(JSON.stringify(pubkey))
}).then(function (jws) {
accountRequest.externalAccountBinding = jws;
return accountRequest;
});
}
return Promise.resolve()
.then(function () {
//#console.debug('[ACME.js] agreeToTerms');
var agreeToTerms = options.agreeToTerms;
if (!agreeToTerms) {
agreeToTerms = function (terms) {
if (agreers[options.subscriberEmail]) {
return true;
}
agreers[options.subscriberEmail] = true;
console.info();
console.info(
'By using this software you (' +
options.subscriberEmail +
') are agreeing to the following:'
);
console.info(
'ACME Subscriber Agreement:',
terms.acmeSubscriberTermsUrl
);
console.info(
'Greenlock/ACME.js Terms of Use:',
terms.acmeJsTermsUrl
);
console.info();
return true;
};
} else if (true === agreeToTerms) {
agreeToTerms = function (terms) {
return terms && true;
};
}
return agreeToTerms({
acmeSubscriberTermsUrl: me._tos,
acmeJsTermsUrl: 'https://rootprojects.org/legal/#terms'
});
})
.then(agree)
.then(getAccount);
};

2413
acme.js

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
#!/usr/bin/env node
(async function() {
(async function () {
'use strict';
var UglifyJS = require('uglify-js');
@ -22,7 +22,7 @@
'../lib/asn1-parser.js',
'../lib/csr.js',
'../lib/acme.js'
].map(async function(file) {
].map(async function (file) {
return (await readFile(path.join(__dirname, file), 'utf8')).trim();
})
);

View File

@ -1,50 +0,0 @@
'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;
});
};

328
dist/acme.js vendored

File diff suppressed because one or more lines are too long

340
dist/app.js vendored

File diff suppressed because one or more lines are too long

81
errors.js Normal file
View File

@ -0,0 +1,81 @@
'use strict';
var E = module.exports;
E.NO_SUITABLE_CHALLENGE = function (domain, challenges, presenters) {
// Bail with a descriptive message if no usable challenge could be selected
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
var enabled = presenters.join(', ') || 'none';
var suitable =
challenges
.map(function (r) {
return r.type;
})
.join(', ') || 'none';
return new Error(
"None of the challenge types that you've enabled ( " +
enabled +
' )' +
" are suitable for validating the domain you've selected (" +
domain +
').' +
' You must enable one of ( ' +
suitable +
' ).'
);
};
E.UNHANDLED_ORDER_STATUS = function (options, domains, resp) {
return new Error(
"Didn't finalize order: Unhandled status '" +
resp.body.status +
"'." +
' This is not one of the known statuses...\n' +
"Requested: '" +
options.domains.join(', ') +
"'\n" +
"Validated: '" +
domains.join(', ') +
"'\n" +
JSON.stringify(resp.body, null, 2) +
'\n\n' +
'Please open an issue at https://git.rootprojects.org/root/acme.js'
);
};
E.DOUBLE_READY_ORDER = function (options, domains, resp) {
return new Error(
"Did not finalize order: status 'ready'." +
" Hmmm... this state shouldn't be possible here. That was the last state." +
" This one should at least be 'processing'.\n" +
"Requested: '" +
options.domains.join(', ') +
"'\n" +
"Validated: '" +
domains.join(', ') +
"'\n" +
JSON.stringify(resp.body, null, 2) +
'\n\n' +
'Please open an issue at https://git.rootprojects.org/root/acme.js'
);
};
E.ORDER_INVALID = function (options, domains, resp) {
return new Error(
"Did not finalize order: status 'invalid'." +
' Best guess: One or more of the domain challenges could not be verified' +
' (or the order was canceled).\n' +
"Requested: '" +
options.domains.join(', ') +
"'\n" +
"Validated: '" +
domains.join(', ') +
"'\n" +
JSON.stringify(resp.body, null, 2)
);
};
E.NO_AUTHORIZATIONS = function (options, resp) {
return new Error(
"[acme-v2.js] authorizations were not fetched for '" +
options.domains.join() +
"':\n" +
JSON.stringify(resp.body)
);
};

317
examples/README.md Normal file
View File

@ -0,0 +1,317 @@
# Example [ACME.js](https://git.rootprojects.org/root/acme.js) Usage
| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub)
ACME.js is a _low-level_ client for Let's Encrypt.
Looking for an **easy**, _high-level_ client? Check out [Greenlock.js](https://git.rootprojects.org/root/greenlock.js).
# Overview
A basic example includes the following:
1. Initialization
- maintainer contact
- package user-agent
- log events
2. Discover API
- retrieves Terms of Service and API endpoints
3. Get Subscriber Account
- create an ECDSA (or RSA) Account key in JWK format
- agree to terms
- register account by the key
4. Prepare a Certificate Signing Request
- create a RSA (or ECDSA) Server key in PEM format
- select domains (as punycode)
- choose challenges
- sign CSR
- order certificate
# Code
The tested-working code for this is in [examples/get-certificate-full.js](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/get-certificate-full.js)
# Walkthrough
Whereas [Greenlock.js](https://git.rootprojects.org/root/greenlock.js) is very much "batteries included",
the goal of ACME.js is to be lightweight and over more control.
## 1. Create an `acme` instance
The maintainer contact is used by Root to notify you of security notices and
bugfixes to ACME.js.
The subscriber contact is used by Let's Encrypt to manage your account and
notify you of renewal failures. In the future we plan to enable some of that,
but allowing for your own branding.
The customer email is provided as an example of what NOT to use as either of the other two.
Typically your customers are NOT directly Let's Encrypt subscribers.
```js
// In many cases all three of these are the same (your email)
// However, this is what they may look like when different:
var maintainerEmail = 'security@devshop.com';
var subscriberEmail = 'support@hostingcompany.com';
var customerEmail = 'jane.doe@gmail.com';
```
The ACME spec requires clients to have RFC 7231 style User Agent.
This will be contstructed automatically using your package name.
```js
var pkg = require('../package.json');
var packageAgent = 'test-' + pkg.name + '/' + pkg.version;
```
Set up your logging facility. It's fine to ignore the logs,
but you'll probably want to log `warning` and `error` at least.
```js
// This is intended to get at important messages without
// having to use even lower-level APIs in the code
function notify(ev, msg) {
if ('error' === ev || 'warning' === ev) {
errors.push(ev.toUpperCase() + ' ' + msg.message);
return;
}
// be brief on all others
console.log(ev, msg.altname || '', msg.status || ''');
}
```
```js
var ACME = require('acme');
var acme = ACME.create({ maintainerEmail, packageAgent, notify });
```
## 2. Fetch the API Directory
ACME defines an API discovery mechanism.
For Let's Encrypt specifically, these are the _production_ and _staging_ URLs:
```js
// Choose either the production or staging URL
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
//var directoryUrl = 'https://acme-v02.api.letsencrypt.org/directory'
```
The init function will fetch the API and set internal urls and such accordingly.
```js
await acme.init(directoryUrl);
```
## 3. Create (or import) an Account Keypair
You must create a Subscriber Account using a public/private keypair.
The Account key MUST be different from the server key.
Keypairs.js will use native node crypto or WebCrypto to generate the key, and a lightweight parser and packer to translate between formats.
```js
var Keypairs = require('@root/keypairs');
```
Unless you're multi-tenanted, you only ever need ONE account key. Save it.
```js
// You only need ONE account key, ever, in most cases
// save this and keep it safe. ECDSA is preferred.
var accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' });
var accountKey = accountKeypair.private;
```
If you already have a key you would like to use, you can import it (as shown in the server key section below).
## 4. Create an ACME Subscriber Account
In order to use Let's Encrypt and ACME.js, you must agree to the respective Subscriber Agreement and Terms.
```js
// This can be `true` or an async function which presents the terms of use
var agreeToTerms = true;
// If you are multi-tenanted or white-labled and need to present the terms of
// use to the Subscriber running the service, you can do so with a function.
var agreeToTerms = async function() {
return true;
};
```
You create an account with a signed JWS message including your public key, which ACME.js handles for you with your account key.
All messages must be signed with your account key.
```js
console.info('registering new ACME account...');
var account = await acme.accounts.create({
subscriberEmail,
agreeToTerms,
accountKey
});
console.info('created account with id', account.key.kid);
```
## 5. Create (or import) a Server Keypair
You must have a SERVER keypair, which is different from your account keypair.
This isn't part of the ACME protocol, but rather something your Web Server uses and which you must use to sign the request for an SSL certificate, the same as with paid issuers in the days of yore.
In many situations you only ever need ONE of these.
```js
// This is the key used by your WEBSERVER, typically named `privkey.pem`,
// `key.crt`, or `bundle.pem`. RSA may be preferrable for legacy compatibility.
// You can generate it fresh
var serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' });
var serverKey = serverKeypair.private;
var serverPem = await Keypairs.export({ jwk: serverKey });
await fs.promises.writeFile('./privkey.pem', serverPem, 'ascii');
// Or you can load it from a file
var serverPem = await fs.promises.readFile('./privkey.pem', 'ascii');
console.info('wrote ./privkey.pem');
var serverKey = await Keypairs.import({ pem: serverPem });
```
## 6. Create a Signed Certificate Request (CSR)
Your domains must be `punycode`-encoded:
```js
var punycode = require('punycode');
var domains = ['example.com', '*.example.com', '你好.example.com'];
domains = domains.map(function(name) {
return punycode.toASCII(name);
});
```
```js
var CSR = require('@root/csr');
var PEM = require('@root/pem');
var encoding = 'der';
var typ = 'CERTIFICATE REQUEST';
var csrDer = await CSR.csr({ jwk: serverKey, domains, encoding });
var csr = PEM.packBlock({ type: typ, bytes: csrDer });
```
## 7. Choose Domain Validation Strategies
You can use one of the existing http-01 or dns-01 plugins, or you can build your own.
There's a test suite that makes this very easy to do:
- [acme-dns-01-test](https://git.rootprojects.org/root/acme-dns-01-test.js)
- [acme-http-01-test](https://git.rootprojects.org/root/acme-http-01-test.js)
```js
// You can pick from existing challenge modules
// which integrate with a variety of popular services
// or you can create your own.
//
// The order of priority will be http-01, tls-alpn-01, dns-01
// dns-01 will always be used for wildcards
// dns-01 should be the only option given for local/private domains
var webroot = require('acme-http-01-webroot').create({});
var challenges = {
'http-01': webroot,
'dns-01': {
init: async function(deps) {
// includes the http request object to use
},
zones: async function(args) {
// return a list of zones
},
set: async function(args) {
// set a TXT record with the lowest allowable TTL
},
get: async function(args) {
// check the TXT record exists
},
remove: async function(args) {
// remove the TXT record
},
// how long to wait after *all* TXTs are set
// before presenting them for validation
// (for most this is seconds, for some it may be minutes)
propagationDelay: 5000
}
};
```
## 8. Verify Domains & Get an SSL Certificate
```js
console.info('validating domain authorization for ' + domains.join(' '));
var pems = await acme.certificates.create({
account,
accountKey,
csr,
domains,
challenges
});
```
## 9. Save the Certificate
```js
var fullchain = pems.cert + '\n' + pems.chain + '\n';
await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii');
console.info('wrote ./fullchain.pem');
```
## 10. Test Drive Your Cert
```js
'use strict';
var https = require('http2');
var fs = require('fs');
var key = fs.readFileSync('./privkey.pem');
var cert = fs.readFileSync('./fullchain.pem');
var server = https.createSecureServer({ key, cert }, function(req, res) {
res.end('Hello, Encrypted World!');
});
server.listen(443, function() {
console.info('Listening on', server.address());
});
```
Note: You can allow non-root `node` processes to bind to port 443 using `setcap`:
```bash
sudo setcap 'cap_net_bind_service=+ep' $(which node)
```
You can also set your domain to localhost by editing your `/etc/hosts`:
`/etc/hosts`:
```txt
127.0.0.1 test.example.com
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
```

View File

@ -1,5 +1,5 @@
/*global Promise*/
(function() {
(function () {
'use strict';
var Keypairs = require('@root/keypairs');
@ -29,8 +29,8 @@
console.log('hello');
// Show different options for ECDSA vs RSA
$$('input[name="kty"]').forEach(function($el) {
$el.addEventListener('change', function(ev) {
$$('input[name="kty"]').forEach(function ($el) {
$el.addEventListener('change', function (ev) {
console.log(this);
console.log(ev);
if ('RSA' === ev.target.value) {
@ -44,20 +44,20 @@
});
// Generate a key on submit
$('form.js-keygen').addEventListener('submit', function(ev) {
$('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) {
$$('.js-toc-pem').forEach(function ($el) {
$el.hidden = true;
});
$$('input').map(function($el) {
$$('input').map(function ($el) {
$el.disabled = true;
});
$$('button').map(function($el) {
$$('button').map(function ($el) {
$el.disabled = true;
});
var opts = {
@ -67,7 +67,7 @@
};
var then = Date.now();
console.log('opts', opts);
Keypairs.generate(opts).then(function(results) {
Keypairs.generate(opts).then(function (results) {
console.log('Key generation time:', Date.now() - then + 'ms');
var pubDer;
var privDer;
@ -77,19 +77,19 @@
Eckles.export({
jwk: results.private,
format: 'sec1'
}).then(function(pem) {
}).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) {
}).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) {
function (pem) {
$('.js-input-pem-spki-public').innerText = pem;
$('.js-toc-pem-spki-public').hidden = false;
}
@ -100,25 +100,25 @@
Rasha.export({
jwk: results.private,
format: 'pkcs1'
}).then(function(pem) {
}).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) {
}).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) {
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) {
function (pem) {
$('.js-input-pem-spki-public').innerText = pem;
$('.js-toc-pem-spki-public').hidden = false;
}
@ -132,10 +132,10 @@
$('.js-jwk').innerText = JSON.stringify(results, null, 2);
$('.js-loading').hidden = true;
$('.js-jwk').hidden = false;
$$('input').map(function($el) {
$$('input').map(function ($el) {
$el.disabled = false;
});
$$('button').map(function($el) {
$$('button').map(function ($el) {
$el.disabled = false;
});
$('.js-toc-jwk').hidden = false;
@ -145,7 +145,7 @@
});
});
$('form.js-acme-account').addEventListener('submit', function(ev) {
$('form.js-acme-account').addEventListener('submit', function (ev) {
ev.preventDefault();
ev.stopPropagation();
$('.js-loading').hidden = false;
@ -155,7 +155,7 @@
});
acme.init(
'https://acme-staging-v02.api.letsencrypt.org/directory'
).then(function(result) {
).then(function (result) {
console.log('acme result', result);
var privJwk = JSON.parse($('.js-jwk').innerText).private;
var email = $('.js-email').value;
@ -165,7 +165,7 @@
agreeToTerms: checkTos,
accountKeypair: { privateKeyJwk: privJwk }
})
.then(function(account) {
.then(function (account) {
console.log('account created result:', account);
accountStuff.account = account;
accountStuff.privateJwk = privJwk;
@ -177,7 +177,7 @@
'.js-acme-account-response'
).innerText = JSON.stringify(account, null, 2);
})
.catch(function(err) {
.catch(function (err) {
console.error('A bad thing happened:');
console.error(err);
window.alert(
@ -187,13 +187,13 @@
});
});
$('form.js-csr').addEventListener('submit', function(ev) {
$('form.js-csr').addEventListener('submit', function (ev) {
ev.preventDefault();
ev.stopPropagation();
generateCsr();
});
$('form.js-acme-order').addEventListener('submit', function(ev) {
$('form.js-acme-order').addEventListener('submit', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var account = accountStuff.account;
@ -204,7 +204,7 @@
var domains = ($('.js-domains').value || 'example.com').split(
/[, ]+/g
);
return getDomainPrivkey().then(function(domainPrivJwk) {
return getDomainPrivkey().then(function (domainPrivJwk) {
console.log('Has CSR already?');
console.log(accountStuff.csr);
return acme.certificates
@ -219,11 +219,11 @@
agreeToTerms: checkTos,
challenges: {
'dns-01': {
set: function(opts) {
set: function (opts) {
console.info('dns-01 set challenge:');
console.info('TXT', opts.dnsHost);
console.info(opts.dnsAuthorization);
return new Promise(function(resolve) {
return new Promise(function (resolve) {
while (
!window.confirm(
'Did you set the challenge?'
@ -232,11 +232,11 @@
resolve();
});
},
remove: function(opts) {
remove: function (opts) {
console.log('dns-01 remove challenge:');
console.info('TXT', opts.dnsHost);
console.info(opts.dnsAuthorization);
return new Promise(function(resolve) {
return new Promise(function (resolve) {
while (
!window.confirm(
'Did you delete the challenge?'
@ -247,11 +247,11 @@
}
},
'http-01': {
set: function(opts) {
set: function (opts) {
console.info('http-01 set challenge:');
console.info(opts.challengeUrl);
console.info(opts.keyAuthorization);
return new Promise(function(resolve) {
return new Promise(function (resolve) {
while (
!window.confirm(
'Did you set the challenge?'
@ -260,11 +260,11 @@
resolve();
});
},
remove: function(opts) {
remove: function (opts) {
console.log('http-01 remove challenge:');
console.info(opts.challengeUrl);
console.info(opts.keyAuthorization);
return new Promise(function(resolve) {
return new Promise(function (resolve) {
while (
!window.confirm(
'Did you delete the challenge?'
@ -279,7 +279,7 @@
$('input[name="acme-challenge-type"]:checked').value
]
})
.then(function(results) {
.then(function (results) {
console.log('Got Certificates:');
console.log(results);
$('.js-toc-acme-order-response').hidden = false;
@ -289,7 +289,7 @@
2
);
})
.catch(function(err) {
.catch(function (err) {
console.error('challenge failed:');
console.error(err);
window.alert(
@ -310,7 +310,7 @@
kty: $('input[name="kty"]:checked').value,
namedCurve: $('input[name="ec-crv"]:checked').value,
modulusLength: $('input[name="rsa-len"]:checked').value
}).then(function(pair) {
}).then(function (pair) {
console.log('domain keypair:', pair);
accountStuff.domainPrivateJwk = pair.private;
return pair.private;
@ -320,9 +320,9 @@
function generateCsr() {
var domains = ($('.js-domains').value || 'example.com').split(/[, ]+/g);
//var privJwk = JSON.parse($('.js-jwk').innerText).private;
return getDomainPrivkey().then(function(privJwk) {
return getDomainPrivkey().then(function (privJwk) {
accountStuff.domainPrivateJwk = privJwk;
return CSR({ jwk: privJwk, domains: domains }).then(function(pem) {
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:');

View File

@ -1 +0,0 @@
../dist

View File

@ -1,6 +1,13 @@
ENV=DEV
MAINTAINER_EMAIL=letsencrypt+staging@example.com
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
# for example
DOMAINS=test.example.com,www.test.example.com
# for tests
BASE_DOMAIN=test.example.com
CHALLENGE_TYPE=dns-01
CHALLENGE_PLUGIN=digitalocean
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'

View File

@ -0,0 +1,151 @@
async function main() {
'use strict';
require('dotenv').config();
var fs = require('fs');
// just to trigger the warning message out of the way
await fs.promises.readFile().catch(function () {});
console.warn('\n');
var MY_DOMAINS = process.env.DOMAINS.split(/[,\s]+/);
// In many cases all three of these are the same (your email)
// However, this is what they may look like when different:
var maintainerEmail = process.env.MAINTAINER_EMAIL;
var subscriberEmail = process.env.SUBSCRIBER_EMAIL;
//var customerEmail = 'jane.doe@gmail.com';
var pkg = require('../package.json');
var packageAgent = 'test-' + pkg.name + '/' + pkg.version;
// Choose either the production or staging URL
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
//var directoryUrl = 'https://acme-v02.api.letsencrypt.org/directory'
// This is intended to get at important messages without
// having to use even lower-level APIs in the code
var errors = [];
function notify(ev, msg) {
if ('error' === ev || 'warning' === ev) {
errors.push(ev.toUpperCase() + ' ' + msg.message);
return;
}
// ignore all for now
console.log(ev, msg.altname || '', msg.status || '');
}
var Keypairs = require('@root/keypairs');
var ACME = require('../');
var acme = ACME.create({ maintainerEmail, packageAgent, notify });
await acme.init(directoryUrl);
// You only need ONE account key, ever, in most cases
// save this and keep it safe. ECDSA is preferred.
var accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' });
var accountKey = accountKeypair.private;
// This can be `true` or an async function which presents the terms of use
var agreeToTerms = true;
// If you are multi-tenanted or white-labled and need to present the terms of
// use to the Subscriber running the service, you can do so with a function.
var agreeToTerms = async function () {
return true;
};
console.info('registering new ACME account...');
var account = await acme.accounts.create({
subscriberEmail,
agreeToTerms,
accountKey
});
console.info('created account with id', account.key.kid);
// This is the key used by your WEBSERVER, typically named `privkey.pem`,
// `key.crt`, or `bundle.pem`. RSA may be preferrable for legacy compatibility.
// You can generate it fresh
var serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' });
var serverKey = serverKeypair.private;
var serverPem = await Keypairs.export({ jwk: serverKey });
await fs.promises.writeFile('./privkey.pem', serverPem, 'ascii');
console.info('wrote ./privkey.pem');
// Or you can load it from a file
var serverPem = await fs.promises.readFile('./privkey.pem', 'ascii');
var serverKey = await Keypairs.import({ pem: serverPem });
var CSR = require('@root/csr');
var PEM = require('@root/pem');
var Enc = require('@root/encoding/base64');
var encoding = 'der';
var typ = 'CERTIFICATE REQUEST';
var domains = MY_DOMAINS;
var csrDer = await CSR.csr({ jwk: serverKey, domains, encoding });
//var csr64 = Enc.bufToBase64(csrDer);
var csr = PEM.packBlock({ type: typ, bytes: csrDer });
// You can pick from existing challenge modules
// which integrate with a variety of popular services
// or you can create your own.
//
// The order of priority will be http-01, tls-alpn-01, dns-01
// dns-01 will always be used for wildcards
// dns-01 should be the only option given for local/private domains
var challenges = {
'dns-01': loadDns01()
};
console.info('validating domain authorization for ' + domains.join(' '));
var pems = await acme.certificates.create({
account,
accountKey,
csr,
domains,
challenges
});
var fullchain = pems.cert + '\n' + pems.chain + '\n';
await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii');
console.info('wrote ./fullchain.pem');
if (errors.length) {
console.warn();
console.warn('[Warning]');
console.warn('The following warnings and/or errors were encountered:');
console.warn(errors.join('\n'));
}
}
main().catch(function (e) {
console.error(e.stack);
});
function loadDns01() {
var pluginName = process.env.CHALLENGE_PLUGIN;
var pluginOptions = process.env.CHALLENGE_OPTIONS;
var plugin;
if (!pluginOptions) {
console.error(
'Please create a .env in the format of examples/example.env to run the tests'
);
process.exit(1);
}
try {
plugin = require(pluginName);
} catch (err) {
console.error("Couldn't find '" + pluginName + "'. Is it installed?");
console.error("\tnpm install --save-dev '" + pluginName + "'");
process.exit(1);
}
return plugin.create(JSON.parse(pluginOptions));
}

15
examples/https-server.js Normal file
View File

@ -0,0 +1,15 @@
'use strict';
var https = require('http2');
var fs = require('fs');
var key = fs.readFileSync('./privkey.pem');
var cert = fs.readFileSync('./fullchain.pem');
var server = https
.createSecureServer({ key, cert }, function (req, res) {
res.end('Hello, Encrypted World!');
})
.listen(443, function () {
console.info('Listening on', server.address());
});

View File

@ -0,0 +1,21 @@
'use strict';
var https = require('http2');
var tls = require('tls');
var fs = require('fs');
var key = fs.readFileSync('./privkey.pem');
var cert = fs.readFileSync('./fullchain.pem');
function SNICallback(servername, cb) {
console.log('sni:', servername);
cb(null, tls.createSecureContext({ key, cert }));
}
var server = https
.createSecureServer({ SNICallback: SNICallback }, function (req, res) {
res.end('Hello, Encrypted World!');
})
.listen(443, function () {
console.info('Listening on', server.address());
});

View File

@ -1,6 +1,6 @@
<html>
<head>
<title>Bluecrypt ACME - A Root Project</title>
<title>ACME.js - A Root Project</title>
<meta charset="UTF-8" />
<style>
textarea {
@ -20,17 +20,15 @@
</head>
<body>
<h1>
@bluecrypt/acme: Let's&nbsp;Encrypt&nbsp;for&nbsp;the&nbsp;Browser
@root/acme: Let's&nbsp;Encrypt&nbsp;for&nbsp;the&nbsp;Browser
</h1>
<p>
This is intended to be explored with your JavaScript console open.
</p>
<pre><code>&lt;script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.js">https://rootprojects.org/acme/bluecrypt-acme.js</a>"&gt;&lt;/script&gt;</code></pre>
<pre><code>&lt;script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.min.js">https://rootprojects.org/acme/bluecrypt-acme.min.js</a>"&gt;&lt;/script&gt;</code></pre>
<a href="https://git.rootprojects.org/root/bluecrypt-acme.js"
>Documentation</a
>
<pre><code>&lt;script src="<a href="https://unpkg.com/@root/acme@3.0.0/dist/acme.js">https://unpkg.com/@root/acme@3.0.0/dist/acme.js</a>"&gt;&lt;/script&gt;</code></pre>
<pre><code>&lt;script src="<a href="https://unpkg.com/@root/acme@3.0.0/dist/acme.min.js">https://unpkg.com/@root/acme@3.0.0/dist/acme.min.js</a>"&gt;&lt;/script&gt;</code></pre>
<a href="https://git.rootprojects.org/root/acme.js">Documentation</a>
<h2>1. Keypair Generation</h2>
<form class="js-keygen">
@ -211,21 +209,20 @@
<br />
<p>
Bluecrypt&trade; is a collection of lightweight, zero-dependency,
libraries written in VanillaJS. They are fast, tiny, and secure,
using the native features of modern browsers where possible. This
means it's easy-to-use crypto in kilobytes, not megabytes.
[Root](https://rootprojects.org) has built 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
>
<a href="https://git.rootprojects.org/root/acme.js">@root/acme</a>
</footer>
<!-- script src="../dist/acme.js"></script>
<script src="./app.js"></script -->
<script src="../dist/app.js"></script>
<!-- script src="../dist/acme.js"></script -->
<!-- script src="../dist/app.js"></script -->
<script src="./app.js"></script>
</body>
</html>

View File

@ -13,12 +13,12 @@ var nameserver = nameservers[index];
app.use('/', express.static(__dirname));
app.use('/api', express.json());
app.get('/api/dns/:domain', function(req, res, next) {
app.get('/api/dns/:domain', function (req, res, next) {
var domain = req.params.domain;
var casedDomain = domain
.toLowerCase()
.split('')
.map(function(ch) {
.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();
@ -46,10 +46,10 @@ app.get('/api/dns/:domain', function(req, res, next) {
]
};
var opts = {
onError: function(err) {
onError: function (err) {
next(err);
},
onMessage: function(packet) {
onMessage: function (packet) {
var fail0x20;
if (packet.id !== query.id) {
@ -62,17 +62,17 @@ app.get('/api/dns/:domain', function(req, res, next) {
return;
}
packet.question.forEach(function(q) {
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(
['question', 'answer', 'authority', 'additional'].forEach(function (
group
) {
(packet[group] || []).forEach(function(a) {
(packet[group] || []).forEach(function (a) {
var an = a.name;
var i = domain
.toLowerCase()
@ -133,13 +133,13 @@ app.get('/api/dns/:domain', function(req, res, next) {
edns_options: packet.edns_options
});
},
onListening: function() {},
onSent: function(/*res*/) {},
onTimeout: function(res) {
onListening: function () {},
onSent: function (/*res*/) {},
onTimeout: function (res) {
console.error('dns timeout:', res);
next(new Error('DNS timeout - no response'));
},
onClose: function() {},
onClose: function () {},
//, mdns: cli.mdns
nameserver: nameserver,
port: 53,
@ -148,13 +148,13 @@ app.get('/api/dns/:domain', function(req, res, next) {
dig.resolveJson(query, opts);
});
app.get('/api/http', function(req, res) {
app.get('/api/http', function (req, res) {
var url = req.query.url;
return request({ method: 'GET', url: url }).then(function(resp) {
return request({ method: 'GET', url: url }).then(function (resp) {
res.send(resp.body);
});
});
app.get('/api/_acme_api_', function(req, res) {
app.get('/api/_acme_api_', function (req, res) {
res.send({ success: true });
});

View File

@ -0,0 +1,14 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:19:57 GMT",
"content-type": "application/json",
"content-length": "341",
"connection": "close",
"boulder-requester": "11407977",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\", <https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel=\"terms-of-service\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11407977",
"replay-nonce": "0001pgbsovQitzg1gDmvpxu18MOh_lsxRyV8cDC19YozinE",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -2,12 +2,11 @@
"key": {
"kty": "EC",
"crv": "P-256",
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
"kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11265299"
"x": "9JZE7ZMAAQ-26oP-_pzd9gy2CbuEvgvrB42R1rP2Pb0",
"y": "8yvSYK5sAx30upYpqVknnPPQlK1T3zGTLbJRC-DH_qw"
},
"contact": [],
"contact": ["mailto:letsencrypt+staging@therootcompany.com"],
"initialIp": "66.219.236.169",
"createdAt": "2019-10-04T22:54:28.569489074Z",
"createdAt": "2019-10-24T23:19:57.480171297Z",
"status": "valid"
}

View File

@ -0,0 +1,15 @@
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct",
"json": {
"protected": "eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJCa0ZsYVBQUi1JTmJfcHVvWHNlbkpGUWJLcFJQM2RraUJuQ0Y4TlNNX3lZIiwieSI6IlZCMEhjM2JoYXlJS2s4QlFiRGJSTDBJZC1LS1hoVkFhRFhLd0RENk1EMjgifSwibm9uY2UiOiIwMDAxSVBlQzN0YV91S29lLTVHanBxUVlGUjFDLVFjS0pzVFVac0daTVFPSzY5ZyIsInVybCI6Imh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvbmV3LWFjY3QiLCJhbGciOiJFUzI1NiJ9",
"payload": "eyJ0ZXJtc09mU2VydmljZUFncmVlZCI6dHJ1ZSwib25seVJldHVybkV4aXN0aW5nIjpmYWxzZSwiY29udGFjdCI6WyJtYWlsdG86bGV0c2VuY3J5cHQrc3RhZ2luZ0B0aGVyb290Y29tcGFueS5jb20iXX0",
"signature": "nuwft1-d349OZoQOH5lsgWCCFYsbciUFrGspiYkd630z_AZU_z0BdNXU5oT2NdaFJJXdqOJkePvEtmTFhAPCEg"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJCa0ZsYVBQUi1JTmJfcHVvWHNlbkpGUWJLcFJQM2RraUJuQ0Y4TlNNX3lZIiwieSI6IlZCMEhjM2JoYXlJS2s4QlFiRGJSTDBJZC1LS1hoVkFhRFhLd0RENk1EMjgifSwibm9uY2UiOiIwMDAxSVBlQzN0YV91S29lLTVHanBxUVlGUjFDLVFjS0pzVFVac0daTVFPSzY5ZyIsInVybCI6Imh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvbmV3LWFjY3QiLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"eyJ0ZXJtc09mU2VydmljZUFncmVlZCI6dHJ1ZSwib25seVJldHVybkV4aXN0aW5nIjpmYWxzZSwiY29udGFjdCI6WyJtYWlsdG86bGV0c2VuY3J5cHQrc3RhZ2luZ0B0aGVyb290Y29tcGFueS5jb20iXX0\",\"signature\":\"nuwft1-d349OZoQOH5lsgWCCFYsbciUFrGspiYkd630z_AZU_z0BdNXU5oT2NdaFJJXdqOJkePvEtmTFhAPCEg\"}",
"method": "POST"
}

View File

@ -0,0 +1,14 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:24 GMT",
"content-type": "application/json",
"content-length": "340",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\", <https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel=\"terms-of-service\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11408075",
"replay-nonce": "0002O1dowqaEQWEHtP2Cz9BYJuOU91uRvRM1uPFbcdwaj-0",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,12 @@
{
"key": {
"kty": "EC",
"crv": "P-256",
"x": "BkFlaPPR-INb_puoXsenJFQbKpRP3dkiBnCF8NSM_yY",
"y": "VB0Hc3bhayIKk8BQbDbRL0Id-KKXhVAaDXKwDD6MD28"
},
"contact": ["mailto:letsencrypt+staging@therootcompany.com"],
"initialIp": "66.219.236.169",
"createdAt": "2019-10-24T23:41:24.38248946Z",
"status": "valid"
}

View File

@ -0,0 +1,177 @@
[
[
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603342",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFvU05Bd25ZVjJRWlB0cGNCZHlNUWd1cXB4MFI1SzhFd0txYzJPeWxVYm5vIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MiIsImFsZyI6IkVTMjU2In0",
"payload": "",
"signature": "mgxpomAxc-a2zEbVuyDxncZvoJTbEWwSRb3aE9W-d8TU_9iIK7jKo6RTL6jTZfgM4ToUET7F19NIqWMnQmoREw"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFvU05Bd25ZVjJRWlB0cGNCZHlNUWd1cXB4MFI1SzhFd0txYzJPeWxVYm5vIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MiIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"mgxpomAxc-a2zEbVuyDxncZvoJTbEWwSRb3aE9W-d8TU_9iIK7jKo6RTL6jTZfgM4ToUET7F19NIqWMnQmoREw\"}",
"method": "POST"
},
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:32 GMT",
"content-type": "application/json",
"content-length": "838",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"replay-nonce": "0002t2JSKyWPm0PEBFrttckiXqIrSEf0PoLdhv24P_QGbrw",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
},
{
"identifier": {
"type": "dns",
"value": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com"
},
"status": "pending",
"expires": "2019-10-31T23:41:32Z",
"challenges": [
{
"type": "http-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/SX06Rw",
"token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA"
},
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ",
"token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA"
},
{
"type": "tls-alpn-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/E-EFfg",
"token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA"
}
]
}
],
[
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603343",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJ0MkpTS3lXUG0wUEVCRnJ0dGNraVhxSXJTRWYwUG9MZGh2MjRQX1FHYnJ3IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MyIsImFsZyI6IkVTMjU2In0",
"payload": "",
"signature": "equGw3S_17IjiavHk25D3l3g48nE6kIhcN6bvgUdBofh1kfsc-kpPVwkZrBMndqWTh-_WHmQtfg01fkP3xzVGg"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJ0MkpTS3lXUG0wUEVCRnJ0dGNraVhxSXJTRWYwUG9MZGh2MjRQX1FHYnJ3IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MyIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"equGw3S_17IjiavHk25D3l3g48nE6kIhcN6bvgUdBofh1kfsc-kpPVwkZrBMndqWTh-_WHmQtfg01fkP3xzVGg\"}",
"method": "POST"
},
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:32 GMT",
"content-type": "application/json",
"content-length": "838",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"replay-nonce": "0002quWdcKvS2smvRV2Dl98tTHjPUS9sRC4ZDzjXpuyeGhc",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
},
{
"identifier": {
"type": "dns",
"value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com"
},
"status": "pending",
"expires": "2019-10-31T23:41:32Z",
"challenges": [
{
"type": "http-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/bSRwrg",
"token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8"
},
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w",
"token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8"
},
{
"type": "tls-alpn-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/awV7qQ",
"token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8"
}
]
}
],
[
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603344",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxdVdkY0t2UzJzbXZSVjJEbDk4dFRIalBVUzlzUkM0WkR6alhwdXllR2hjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0NCIsImFsZyI6IkVTMjU2In0",
"payload": "",
"signature": "UzOSs2HvxN_mErU-wjrffbFp3JZOu6Earsq3ssj49Qcw3Bf5uyXPKO5DF7iseuL2Qammqofvh70pCka6tD_knQ"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxdVdkY0t2UzJzbXZSVjJEbDk4dFRIalBVUzlzUkM0WkR6alhwdXllR2hjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0NCIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"UzOSs2HvxN_mErU-wjrffbFp3JZOu6Earsq3ssj49Qcw3Bf5uyXPKO5DF7iseuL2Qammqofvh70pCka6tD_knQ\"}",
"method": "POST"
},
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:32 GMT",
"content-type": "application/json",
"content-length": "838",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"replay-nonce": "0001kREyyuaaIacPhD7-j73BHzyQnhfPiBM3PEwnXDFVgTc",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
},
{
"identifier": {
"type": "dns",
"value": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com"
},
"status": "pending",
"expires": "2019-10-31T23:41:32Z",
"challenges": [
{
"type": "http-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/usH89w",
"token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs"
},
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw",
"token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs"
},
{
"type": "tls-alpn-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/6C26qQ",
"token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs"
}
]
}
]
]

View File

@ -0,0 +1,15 @@
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFqNEF6c2Qwa2s2aTYwTlN6Um9aY3ZMaWRtTG81QjBzRzFsTUtUcVdyMzg4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MSIsImFsZyI6IkVTMjU2In0",
"payload": "",
"signature": "qjrQyqKRskdhF7DVUymZdHhm9neC9vgH9UUc6D-vtXtS8T2QW9C82qsyghZdGGJLWeKeZLRsADjmZSh5XCAa4g"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFqNEF6c2Qwa2s2aTYwTlN6Um9aY3ZMaWRtTG81QjBzRzFsTUtUcVdyMzg4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xNjYwMzM0MSIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"qjrQyqKRskdhF7DVUymZdHhm9neC9vgH9UUc6D-vtXtS8T2QW9C82qsyghZdGGJLWeKeZLRsADjmZSh5XCAa4g\"}",
"method": "POST"
}

View File

@ -0,0 +1,13 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:32 GMT",
"content-type": "application/json",
"content-length": "420",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"replay-nonce": "0001oSNAwnYV2QZPtpcBdyMQguqpx0R5K8EwKqc2OylUbno",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,17 @@
{
"identifier": {
"type": "dns",
"value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com"
},
"status": "pending",
"expires": "2019-10-31T23:41:32Z",
"challenges": [
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI"
}
],
"wildcard": true
}

View File

@ -0,0 +1,15 @@
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/cert/fa78326c21c0c7f06c03931900bead4fe3ee",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDExLW5qUV91MWp4N1dqVEdfY1Blam05UUxLZWxFcUVFdEpEa3JlVHJ5OVI4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jZXJ0L2ZhNzgzMjZjMjFjMGM3ZjA2YzAzOTMxOTAwYmVhZDRmZTNlZSIsImFsZyI6IkVTMjU2In0",
"payload": "",
"signature": "639Q5Eo2_xWh3ylRy3olXJVXz_4JTrpVFkUmz9-h1l8Hrsmg47I0HFgMrHslfKEJfj86zGUh9XY-VtBF2IFcIQ"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDExLW5qUV91MWp4N1dqVEdfY1Blam05UUxLZWxFcUVFdEpEa3JlVHJ5OVI4IiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jZXJ0L2ZhNzgzMjZjMjFjMGM3ZjA2YzAzOTMxOTAwYmVhZDRmZTNlZSIsImFsZyI6IkVTMjU2In0\",\"payload\":\"\",\"signature\":\"639Q5Eo2_xWh3ylRy3olXJVXz_4JTrpVFkUmz9-h1l8Hrsmg47I0HFgMrHslfKEJfj86zGUh9XY-VtBF2IFcIQ\"}",
"method": "POST"
}

View File

@ -0,0 +1,12 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:44 GMT",
"content-type": "application/pem-certificate-chain",
"content-length": "3806",
"connection": "close",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"replay-nonce": "0002vmpuKxQvokCGu5-cbVhsXkBHweBkdFnNrIpufnVn8mc",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,64 @@
// Note: I may have added or truncated a beginning or ending
// newline here in the process of copy/paste
-----BEGIN CERTIFICATE-----
MIIF9TCCBN2gAwIBAgITAPp4MmwhwMfwbAOTGQC+rU/j7jANBgkqhkiG9w0BAQsF
ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xOTEwMjQy
MjQxNDRaFw0yMDAxMjIyMjQxNDRaMDUxMzAxBgNVBAMTKnhuLS1mb28tYWNtZWpz
LTJlYTQtems4eC50ZXN0LnV0YWhydXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAOXgIzVvJzQRuGkomoKQzswNyMaFB7MmCHNOW98yYxfHpLqj
KKddplJpvHQ/R8I15+38QfqT9kvj9vQ7i3gU6AUya56Sg6TSSmUE5PBP7WfEn/2O
+iHzZ/Devq/Oq0fHQoF+TtEFgnMVZZL4gnEyciSzQs5ftn+HejLGYmBH5uJlPGCp
9lMOe+ziweWKbmZYDu4Qrqf3TEHbFOpBPgJUna4tz0xmISdxzuR9Q/tie3a+cCjV
4xtxCblN9W37KC1VnEkLtQwgm6zjZAVSUWOLZUqMVL2H+/jR5Z9r1XYevEDlAl35
sW0kaEf/FdLfr8tfbbnPUsVvRL5I5gdLmyonJccCAwEAAaOCAw8wggMLMA4GA1Ud
DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T
AQH/BAIwADAdBgNVHQ4EFgQUJqGfhDoxM99m3HZUhlME4JMg+zQwHwYDVR0jBBgw
FoAUwMwDRrlYIMxccnDz4S7LIKb1aDowdwYIKwYBBQUHAQEEazBpMDIGCCsGAQUF
BzABhiZodHRwOi8vb2NzcC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZzAzBggr
BgEFBQcwAoYnaHR0cDovL2NlcnQuc3RnLWludC14MS5sZXRzZW5jcnlwdC5vcmcv
MIG9BgNVHREEgbUwgbKCLCoueG4tLWJhei1hY21lanMtMmVhNC16azh4LnRlc3Qu
dXRhaHJ1c3QuY29tgip4bi0tYmFyLWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFo
cnVzdC5jb22CKnhuLS1iYXotYWNtZWpzLTJlYTQtems4eC50ZXN0LnV0YWhydXN0
LmNvbYIqeG4tLWZvby1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29t
MEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUH
AgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB
9ASB8QDvAHYAxj8iGMN9VqaqBrWW2o5T1NcVbR6brI5E0iAt5k1p2dwAAAFuACW/
/QAABAMARzBFAiB/xTPuBFV2+yfovKBiru29WQ+j3wjTGE1Urcn1Rn+5nQIhALH+
5N4A0TiK04romA8Nb/R5X0sNM68HGK/KRCICdYOxAHUAsMyD5aX5fWuvfAnMKEkE
hyrH6IsTLGNQt8b9JuFsbHcAAAFuACW//gAABAMARjBEAiAcL3cjhbwAOV34v3vK
svbb9yIK36vRucq3hu/Vs1B3ZAIgfTwjAHDE6GqfZEW2e9MjuULEvMdF2QHVh7WB
Bp5A48wwDQYJKoZIhvcNAQELBQADggEBAFxbkUt0QOZNAKnTqdYnBP2FlxezjFPq
P4pD/G2/JFKi86VDg2vLVfPMGd7jv+e8Ao0+G9rgC3vtQE817T5d9XFlJ8p7dMjK
TbTmSlKHxM9Dal8fqC7kbqqx/gdpzzPyBoDYlKWvhr3qXsxB/hGI3OX+d42R1wsr
zcQKaG2HpJcerZ1au2Jm/YOCJPpDHMAFKK5wuCmOIBfNQ+ULyStPZLQWPdMI04S2
Y8eIQgS6q9OX1CtvuehVFwyO8TNi53do88wFDdHF7lNZEjz7NvpNqi3qeZgSRuAb
/fTMCULMjDghh+xpTLRzSROB6YJbU8uXtSZ6Xn04SZ6ZSuvbCYmHlsU=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
-----END CERTIFICATE-----

View File

@ -0,0 +1,121 @@
[
[
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDIybXdZUUhpR0NMMVRacUViYkNBZ1N1djJYMXctSGhkMWR0TV9zRllXRGlNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0Mi9JNHVoR1EiLCJhbGciOiJFUzI1NiJ9",
"payload": "e30",
"signature": "90XygqCrKMhqsoFD4-J56yYgEKuevnw7V-4MaP_lZKzMn9vnhK_CtWh0k5kRuePhJzopTRrWkRzXz9OExlt9WQ"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDIybXdZUUhpR0NMMVRacUViYkNBZ1N1djJYMXctSGhkMWR0TV9zRllXRGlNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0Mi9JNHVoR1EiLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"90XygqCrKMhqsoFD4-J56yYgEKuevnw7V-4MaP_lZKzMn9vnhK_CtWh0k5kRuePhJzopTRrWkRzXz9OExlt9WQ\"}",
"method": "POST"
},
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:42 GMT",
"content-type": "application/json",
"content-length": "292",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\", <https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603342>;rel=\"up\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ",
"replay-nonce": "0001XZufnGiSHfABU10B8FWCxHzvqPN991zSEO3-uQnNZqI",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
},
{
"type": "dns-01",
"status": "valid",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603342/I4uhGQ",
"token": "NsJOLEHJONiQqkADO_lecQ2J0u-g6I2tvkgqevUBbUA",
"validationRecord": [
{ "hostname": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com" }
]
}
],
[
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFYWnVmbkdpU0hmQUJVMTBCOEZXQ3hIenZxUE45OTF6U0VPMy11UW5OWnFJIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0My9VbXZfNXciLCJhbGciOiJFUzI1NiJ9",
"payload": "e30",
"signature": "I5p1OLU52W7m-oHeRWAuZQyf5saBlm1Mv5UV8kqRLVxxt-kMEJLXwKgP0kgfz-rXjnZheYnrKiKERZX1wt7RdA"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFYWnVmbkdpU0hmQUJVMTBCOEZXQ3hIenZxUE45OTF6U0VPMy11UW5OWnFJIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0My9VbXZfNXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"I5p1OLU52W7m-oHeRWAuZQyf5saBlm1Mv5UV8kqRLVxxt-kMEJLXwKgP0kgfz-rXjnZheYnrKiKERZX1wt7RdA\"}",
"method": "POST"
},
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:43 GMT",
"content-type": "application/json",
"content-length": "292",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\", <https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603343>;rel=\"up\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w",
"replay-nonce": "00012YkSGH0-3llPNZT_hV8Ovw11jJU9YyppuJ--gJldLTo",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
},
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603343/Umv_5w",
"token": "Cc3I3F1Pvc_aweOeRdtzR1h2C_uhseAbiWMQkwb6Kf8"
}
],
[
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxT0s4WGhNcWtCVWgzYk1LUV9ZMUo2QXJUbEVOR01BTUQ4bHc3WjNtT2JvIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0NC9IcXZsaXciLCJhbGciOiJFUzI1NiJ9",
"payload": "e30",
"signature": "ltAp1E52XSMMZpleycguLlo4Hii0FxAbiXcmZBdA-vTjqJb8S1X4CVYQ-qebmYFlCipRhe9Juaj6zpvX7UbTnQ"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJxT0s4WGhNcWtCVWgzYk1LUV9ZMUo2QXJUbEVOR01BTUQ4bHc3WjNtT2JvIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0NC9IcXZsaXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"ltAp1E52XSMMZpleycguLlo4Hii0FxAbiXcmZBdA-vTjqJb8S1X4CVYQ-qebmYFlCipRhe9Juaj6zpvX7UbTnQ\"}",
"method": "POST"
},
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:44 GMT",
"content-type": "application/json",
"content-length": "292",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\", <https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603344>;rel=\"up\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw",
"replay-nonce": "0001RZo7OXhCjsG_9mtrLylmz443TVc9FOsyhfergGWmkDM",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
},
{
"type": "dns-01",
"status": "valid",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603344/Hqvliw",
"token": "uv7D1YC7oWvMY8-EC2blKxmSFExYHwjCcKjGpuodwWs",
"validationRecord": [
{ "hostname": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com" }
]
}
]
]

View File

@ -0,0 +1,15 @@
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFrUkV5eXVhYUlhY1BoRDctajczQkh6eVFuaGZQaUJNM1BFd25YREZWZ1RjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9",
"payload": "e30",
"signature": "QZKdMroSf-qrno2UBHf_L2nL9VrvDtDEb0uLL2fp1yKkwX8u0sELLOYfIu8YqeSwcmPZ1LQHWbXLx5SQ0Lv3Pw"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFrUkV5eXVhYUlhY1BoRDctajczQkh6eVFuaGZQaUJNM1BFd25YREZWZ1RjIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"QZKdMroSf-qrno2UBHf_L2nL9VrvDtDEb0uLL2fp1yKkwX8u0sELLOYfIu8YqeSwcmPZ1LQHWbXLx5SQ0Lv3Pw\"}",
"method": "POST"
}

View File

@ -0,0 +1,14 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:39 GMT",
"content-type": "application/json",
"content-length": "190",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\", <https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341>;rel=\"up\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"replay-nonce": "0001In5LKCnj27k3uNTzl19vqQ5oHlroIJJI-U1daaxNd-Y",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,6 @@
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI"
}

View File

@ -0,0 +1,15 @@
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFJbjVMS0NuajI3azN1TlR6bDE5dnFRNW9IbHJvSUpKSS1VMWRhYXhOZC1ZIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9",
"payload": "e30",
"signature": "3SVtWvRXGFirW198sM4bWErA5M_GplWkI_duSKLHtdGLe-R2D2r0VK1_Xn4exfk6MGIBSkaeeYV6RJfnsLgYLg"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFJbjVMS0NuajI3azN1TlR6bDE5dnFRNW9IbHJvSUpKSS1VMWRhYXhOZC1ZIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xNjYwMzM0MS95Q3RoVXciLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"e30\",\"signature\":\"3SVtWvRXGFirW198sM4bWErA5M_GplWkI_duSKLHtdGLe-R2D2r0VK1_Xn4exfk6MGIBSkaeeYV6RJfnsLgYLg\"}",
"method": "POST"
}

View File

@ -0,0 +1,9 @@
{
"type": "dns-01",
"status": "valid",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI",
"validationRecord": [
{ "hostname": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com" }
]
}

View File

@ -0,0 +1,14 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:40 GMT",
"content-type": "application/json",
"content-length": "292",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\", <https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341>;rel=\"up\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
"replay-nonce": "0001P9ksMrD-4xaHyRPUVR2pq6PMQSG7T-ELjWBWXsLROv0",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,9 @@
{
"method": "GET",
"url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"json": true,
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Accept": "application/json"
}
}

View File

@ -0,0 +1,10 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:24 GMT",
"content-type": "application/json",
"content-length": "724",
"connection": "close",
"cache-control": "public, max-age=0, no-cache",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,13 @@
{
"Uw5jwSdQL_Q": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
"keyChange": "https://acme-staging-v02.api.letsencrypt.org/acme/key-change",
"meta": {
"caaIdentities": ["letsencrypt.org"],
"termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
"website": "https://letsencrypt.org/docs/staging-environment/"
},
"newAccount": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct",
"newNonce": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce",
"newOrder": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order",
"revokeCert": "https://acme-staging-v02.api.letsencrypt.org/acme/revoke-cert"
}

View File

@ -0,0 +1,15 @@
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/11408075/57799471",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFSWm83T1hoQ2pzR185bXRyTHlsbXo0NDNUVmM5Rk9zeWhmZXJnR1dta0RNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9maW5hbGl6ZS8xMTQwODA3NS81Nzc5OTQ3MSIsImFsZyI6IkVTMjU2In0",
"payload": "eyJjc3IiOiJNSUlEVHpDQ0FqY0NBUUF3TlRFek1ERUdBMVVFQXd3cWVHNHRMV1p2YnkxaFkyMWxhbk10TW1WaE5DMTZhemg0TG5SbGMzUXVkWFJoYUhKMWMzUXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE1ZUFqTlc4bk5CRzRhU2lhZ3BET3pBM0l4b1VIc3lZSWMwNWIzekpqRjhla3VxTW9wMTJtVW1tOGREOUh3alhuN2Z4Qi1wUDJTLVAyOUR1TGVCVG9CVEpybnBLRHBOSktaUVRrOEVfdFo4U2ZfWTc2SWZObjhONi1yODZyUjhkQ2dYNU8wUVdDY3hWbGt2aUNjVEp5SkxOQ3psLTJmNGQ2TXNaaVlFZm00bVU4WUtuMlV3NTc3T0xCNVlwdVpsZ083aEN1cF9kTVFkc1U2a0UtQWxTZHJpM1BUR1loSjNITzVIMUQtMko3ZHI1d0tOWGpHM0VKdVUzMWJmc29MVldjU1F1MURDQ2JyT05rQlZKUlk0dGxTb3hVdllmNy1OSGxuMnZWZGg2OFFPVUNYZm14YlNSb1JfOFYwdC12eTE5dHVjOVN4VzlFdmtqbUIwdWJLaWNseHdJREFRQUJvSUhVTUlIUkJna3Foa2lHOXcwQkNRNHhnY013Z2NBd2diMEdBMVVkRVFTQnRUQ0Jzb0lxZUc0dExXWnZieTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRnlMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMkNMQ291ZUc0dExXSmhlaTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRjZMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBTV9idU84N0YtMkd2aThKZmlaZ3ZYNGNvUnllLVhSSDVnbTJ6enRjNW1KS0ZxRmRBdkV5Z0IxbE82NmJaTG5uZjk5bWRROFk5UnQ0R1RiU3N5N1djQ1NMVF91MVNGX0h1REU5SnZ2ek43MnU1VmtlLW1KelB0cG1OcTlRODZpRWNVQnVEMmNfVVVCQ0Y2ZEFsTHhUZmRQRkJWdXBPSnVCRmQ4azdBNlhhbTl0UjFKV3p4RGdrSHM1cTdmSWo1dXVLcmdjSlhWc19lWHA0QkNONEcyM2hKX01YR1RidDhqeHU1MTFOaDE0Z18wT3JlWkw1bHd5MWR5ZE9mN0pLdGpUdmtyQWE1YjJDVXlLa293NHlaLTNoUmVRcHZjVnIzcnRaTWtKdndMMHI5WjcxcENHRjViUVEweDBIVk04VzYtVkotTWJpLVlhTC04TjNyNEpTbWdDN09VIn0",
"signature": "_X0X-Wg86dr5mF0eS0GOYNSmO0HCenlIGQeMygRVoH7BpYO0AMK_mgRQlNR3MWNMULC_aQ-oEMtsXGMXrTa7VA"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDFSWm83T1hoQ2pzR185bXRyTHlsbXo0NDNUVmM5Rk9zeWhmZXJnR1dta0RNIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9maW5hbGl6ZS8xMTQwODA3NS81Nzc5OTQ3MSIsImFsZyI6IkVTMjU2In0\",\"payload\":\"eyJjc3IiOiJNSUlEVHpDQ0FqY0NBUUF3TlRFek1ERUdBMVVFQXd3cWVHNHRMV1p2YnkxaFkyMWxhbk10TW1WaE5DMTZhemg0TG5SbGMzUXVkWFJoYUhKMWMzUXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE1ZUFqTlc4bk5CRzRhU2lhZ3BET3pBM0l4b1VIc3lZSWMwNWIzekpqRjhla3VxTW9wMTJtVW1tOGREOUh3alhuN2Z4Qi1wUDJTLVAyOUR1TGVCVG9CVEpybnBLRHBOSktaUVRrOEVfdFo4U2ZfWTc2SWZObjhONi1yODZyUjhkQ2dYNU8wUVdDY3hWbGt2aUNjVEp5SkxOQ3psLTJmNGQ2TXNaaVlFZm00bVU4WUtuMlV3NTc3T0xCNVlwdVpsZ083aEN1cF9kTVFkc1U2a0UtQWxTZHJpM1BUR1loSjNITzVIMUQtMko3ZHI1d0tOWGpHM0VKdVUzMWJmc29MVldjU1F1MURDQ2JyT05rQlZKUlk0dGxTb3hVdllmNy1OSGxuMnZWZGg2OFFPVUNYZm14YlNSb1JfOFYwdC12eTE5dHVjOVN4VzlFdmtqbUIwdWJLaWNseHdJREFRQUJvSUhVTUlIUkJna3Foa2lHOXcwQkNRNHhnY013Z2NBd2diMEdBMVVkRVFTQnRUQ0Jzb0lxZUc0dExXWnZieTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRnlMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMkNMQ291ZUc0dExXSmhlaTFoWTIxbGFuTXRNbVZoTkMxNmF6aDRMblJsYzNRdWRYUmhhSEoxYzNRdVkyOXRnaXA0YmkwdFltRjZMV0ZqYldWcWN5MHlaV0UwTFhwck9IZ3VkR1Z6ZEM1MWRHRm9jblZ6ZEM1amIyMHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBTV9idU84N0YtMkd2aThKZmlaZ3ZYNGNvUnllLVhSSDVnbTJ6enRjNW1KS0ZxRmRBdkV5Z0IxbE82NmJaTG5uZjk5bWRROFk5UnQ0R1RiU3N5N1djQ1NMVF91MVNGX0h1REU5SnZ2ek43MnU1VmtlLW1KelB0cG1OcTlRODZpRWNVQnVEMmNfVVVCQ0Y2ZEFsTHhUZmRQRkJWdXBPSnVCRmQ4azdBNlhhbTl0UjFKV3p4RGdrSHM1cTdmSWo1dXVLcmdjSlhWc19lWHA0QkNONEcyM2hKX01YR1RidDhqeHU1MTFOaDE0Z18wT3JlWkw1bHd5MWR5ZE9mN0pLdGpUdmtyQWE1YjJDVXlLa293NHlaLTNoUmVRcHZjVnIzcnRaTWtKdndMMHI5WjcxcENHRjViUVEweDBIVk04VzYtVkotTWJpLVlhTC04TjNyNEpTbWdDN09VIn0\",\"signature\":\"_X0X-Wg86dr5mF0eS0GOYNSmO0HCenlIGQeMygRVoH7BpYO0AMK_mgRQlNR3MWNMULC_aQ-oEMtsXGMXrTa7VA\"}",
"method": "POST"
}

View File

@ -0,0 +1,14 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:44 GMT",
"content-type": "application/json",
"content-length": "993",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/order/11408075/57799471",
"replay-nonce": "00011-njQ_u1jx7WjTG_cPejm9QLKelEqEEtJDkreTry9R8",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,27 @@
{
"status": "valid",
"expires": "2019-10-31T23:41:32Z",
"identifiers": [
{
"type": "dns",
"value": "*.xn--baz-acmejs-2ea4-zk8x.test.utahrust.com"
},
{
"type": "dns",
"value": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com"
},
{
"type": "dns",
"value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com"
},
{ "type": "dns", "value": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com" }
],
"authorizations": [
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603342",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603343",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603344"
],
"finalize": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/11408075/57799471",
"certificate": "https://acme-staging-v02.api.letsencrypt.org/acme/cert/fa78326c21c0c7f06c03931900bead4fe3ee"
}

View File

@ -0,0 +1,7 @@
{
"method": "HEAD",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce",
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 Darwin darwin/17.7.0 Darwin/x64"
}
}

View File

@ -0,0 +1,10 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:24 GMT",
"connection": "close",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"replay-nonce": "0001IPeC3ta_uKoe-5GjpqQYFR1C-QcKJsTUZsGZMQOK69g",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1 @@
// there is no nonce response body, see the headers

View File

@ -0,0 +1,15 @@
{
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order",
"json": {
"protected": "eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJPMWRvd3FhRVFXRUh0UDJDejlCWUp1T1U5MXVSdlJNMXVQRmJjZHdhai0wIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctb3JkZXIiLCJhbGciOiJFUzI1NiJ9",
"payload": "eyJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoieG4tLWZvby1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmFyLWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifSx7InR5cGUiOiJkbnMiLCJ2YWx1ZSI6IioueG4tLWJhei1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmF6LWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifV19",
"signature": "Bw8cjSwQj_rFooUFL61gqiuLXec-8x4anHNF1ueVt_LvoCO70bYt0fM26W4hOJ9Es6fibmYazFKSTPwdgnLm2Q"
},
"headers": {
"User-Agent": "ACME.js/v3 node/v10.13.0 darwin/17.7.0 Darwin/x64",
"Content-Type": "application/jose+json",
"Accept": "application/json"
},
"body": "{\"protected\":\"eyJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMTE0MDgwNzUiLCJub25jZSI6IjAwMDJPMWRvd3FhRVFXRUh0UDJDejlCWUp1T1U5MXVSdlJNMXVQRmJjZHdhai0wIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctb3JkZXIiLCJhbGciOiJFUzI1NiJ9\",\"payload\":\"eyJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoieG4tLWZvby1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmFyLWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifSx7InR5cGUiOiJkbnMiLCJ2YWx1ZSI6IioueG4tLWJhei1hY21lanMtMmVhNC16azh4LnRlc3QudXRhaHJ1c3QuY29tIn0seyJ0eXBlIjoiZG5zIiwidmFsdWUiOiJ4bi0tYmF6LWFjbWVqcy0yZWE0LXprOHgudGVzdC51dGFocnVzdC5jb20ifV19\",\"signature\":\"Bw8cjSwQj_rFooUFL61gqiuLXec-8x4anHNF1ueVt_LvoCO70bYt0fM26W4hOJ9Es6fibmYazFKSTPwdgnLm2Q\"}",
"method": "POST"
}

View File

@ -0,0 +1,14 @@
{
"server": "nginx",
"date": "Thu, 24 Oct 2019 23:41:32 GMT",
"content-type": "application/json",
"content-length": "893",
"connection": "close",
"boulder-requester": "11408075",
"cache-control": "public, max-age=0, no-cache",
"link": "<https://acme-staging-v02.api.letsencrypt.org/directory>;rel=\"index\"",
"location": "https://acme-staging-v02.api.letsencrypt.org/acme/order/11408075/57799471",
"replay-nonce": "0001j4Azsd0kk6i60NSzRoZcvLidmLo5B0sG1lMKTqWr388",
"x-frame-options": "DENY",
"strict-transport-security": "max-age=604800"
}

View File

@ -0,0 +1,26 @@
{
"status": "pending",
"expires": "2019-10-31T23:41:32.669736375Z",
"identifiers": [
{
"type": "dns",
"value": "*.xn--baz-acmejs-2ea4-zk8x.test.utahrust.com"
},
{
"type": "dns",
"value": "xn--bar-acmejs-2ea4-zk8x.test.utahrust.com"
},
{
"type": "dns",
"value": "xn--baz-acmejs-2ea4-zk8x.test.utahrust.com"
},
{ "type": "dns", "value": "xn--foo-acmejs-2ea4-zk8x.test.utahrust.com" }
],
"authorizations": [
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603341",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603342",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603343",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/16603344"
],
"finalize": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/11408075/57799471"
}

54
lib/browser.js Normal file
View File

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

View File

@ -0,0 +1,6 @@
'use strict';
var UserAgent = module.exports;
UserAgent.get = function () {
return false;
};

View File

@ -2,29 +2,30 @@
var http = module.exports;
http.request = function(opts) {
return window.fetch(opts.url, opts).then(function(resp) {
http.request = function (opts) {
opts.cors = true;
return window.fetch(opts.url, opts).then(function (resp) {
var headers = {};
var result = {
statusCode: resp.status,
headers: headers,
toJSON: function() {
toJSON: function () {
return this;
}
};
Array.from(resp.headers.entries()).forEach(function(h) {
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) {
return resp.json().then(function (json) {
result.body = json;
return result;
});
}
return resp.text().then(function(txt) {
return resp.text().then(function (txt) {
result.body = txt;
return result;
});

View File

@ -3,7 +3,7 @@
var sha2 = module.exports;
var encoder = new TextEncoder();
sha2.sum = function(alg, str) {
sha2.sum = function (alg, str) {
var data = str;
if ('string' === typeof data) {
data = encoder.encode(str);

88
lib/native.js Normal file
View File

@ -0,0 +1,88 @@
'use strict';
var native = module.exports;
var promisify = require('util').promisify;
var resolveTxt = promisify(require('dns').resolveTxt);
var crypto = require('crypto');
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;
});
};
// the hashcash here is for browser parity only
// basically we ask the client to find a needle in a haystack
// (very similar to CloudFlare's api protection)
native._hashcash = function (ch) {
if (!ch || !ch.nonce) {
ch = { nonce: 'xxx' };
}
return Promise.resolve()
.then(function () {
// only get easy answers
var len = ch.needle.length;
var start = ch.start || 0;
var end = ch.end || Math.ceil(len / 2);
var window = parseInt(end - start, 10) || 0;
var maxLen = 6;
var maxTries = Math.pow(2, maxLen * 8);
if (
len > maxLen ||
window < Math.ceil(len / 2) ||
ch.needle.toLowerCase() !== ch.needle ||
ch.alg !== 'SHA-256'
) {
// bail unless the server is issuing very easy challenges
throw new Error('possible and easy answers only, please');
}
var haystack;
var i;
var answer;
var needle = Buffer.from(ch.needle, 'hex');
for (i = 0; i < maxTries; i += 1) {
answer = i.toString(16);
if (answer.length % 2) {
answer = '0' + answer;
}
haystack = crypto
.createHash('sha256')
.update(Buffer.from(ch.nonce + answer, 'hex'))
.digest()
.slice(ch.start, ch.end);
if (-1 !== haystack.indexOf(needle)) {
return ch.nonce + ':' + answer;
}
}
return ch.nonce + ':xxx';
})
.catch(function () {
//console.log('[debug]', err);
// ignore any error
return ch.nonce + ':xxx';
});
};

View File

@ -0,0 +1,37 @@
'use strict';
var os = require('os');
var ver = require('../../package.json').version;
var UserAgent = module.exports;
UserAgent.get = function (me) {
// ACME clients MUST have an RFC7231-compliant User-Agent
// ex: Greenlock/v3 ACME.js/v3 node/v12.0.0 darwin/17.7.0 Darwin/x64
//
// See https://tools.ietf.org/html/rfc8555#section-6.1
// And https://tools.ietf.org/html/rfc7231#section-5.5.3
// And https://community.letsencrypt.org/t/user-agent-flag-explained/3843/2
var ua =
'ACME.js/' +
ver +
' ' +
process.release.name +
'/' +
process.version +
' ' +
os.platform() +
'/' +
os.release() +
' ' +
os.type() +
'/' +
process.arch;
var pkg = me.packageAgent;
if (pkg) {
ua = pkg + ' ' + ua;
}
return ua;
};

View File

@ -4,16 +4,6 @@ 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';
}
http.request = function (opts) {
return request(opts);
};

View File

@ -4,14 +4,11 @@
var sha2 = module.exports;
var crypto = require('crypto');
sha2.sum = function(alg, str) {
return Promise.resolve().then(function() {
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();
return crypto.createHash(sha).update(buf).digest();
});
};

90
maintainers.js Normal file
View File

@ -0,0 +1,90 @@
'use strict';
var M = module.exports;
var native = require('./lib/native.js');
// Keep track of active maintainers so that we know who to inform if
// something breaks or has a serious bug or flaw.
var oldCollegeTries = {};
M.init = function (me) {
if (oldCollegeTries[me.maintainerEmail]) {
return;
}
var tz = '';
try {
// Use timezone to stagger messages to maintainers
tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch (e) {
// ignore node versions with no or incomplete Intl
}
// Use locale to know what language to use
var env = process.env;
var locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE;
try {
M._init(me, tz, locale);
} catch (e) {
//console.log(e);
// ignore
}
};
M._init = function (me, tz, locale) {
setTimeout(function () {
// prevent a stampede from misconfigured clients in an eternal loop
me.request({
timeout: 3000,
method: 'GET',
url: 'https://api.rootprojects.org/api/nonce',
json: true
})
.then(function (resp) {
// in the browser this will work until solved, but in
// node this will bail unless the challenge is trivial
return native._hashcash(resp.body || {});
})
.then(function (hashcash) {
var req = {
timeout: 3000,
headers: {
'x-root-nonce-v1': hashcash
},
method: 'POST',
url:
'https://api.rootprojects.org/api/projects/ACME.js/dependents',
json: {
maintainer: me.maintainerEmail,
package: me.packageAgent,
tz: tz,
locale: locale
}
};
return me.request(req);
})
.catch(function (err) {
if (me.debug) {
console.error(
'error adding maintainer to support notices:'
);
console.error(err);
}
})
.then(function (/*resp*/) {
oldCollegeTries[me.maintainerEmail] = true;
//console.log(resp);
});
}, me.__timeout || 3000);
};
if (require.main === module) {
var ACME = require('./');
var acme = ACME.create({
maintainerEmail: 'aj+acme-test@rootprojects.org',
packageAgent: 'test/v0',
__timeout: 100
});
M.init(acme);
}

View File

@ -1,33 +0,0 @@
'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;
});
};

39
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "@root/acme",
"version": "3.0.0-wip.4",
"version": "3.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -16,7 +16,6 @@
"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",
@ -29,9 +28,9 @@
"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==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.10.0.tgz",
"integrity": "sha512-t8VocY46Mtb0NTsxzyLLf5tsgfw0BXLYVADAyiRdEdqHcvPFGJdjkXNtHVQuSV/FMaC65iTOHVP4E6X8iT3Ikg==",
"requires": {
"@root/encoding": "^1.0.1",
"@root/pem": "^1.0.4",
@ -44,9 +43,9 @@
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
},
"@root/request": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.6.1.tgz",
"integrity": "sha512-8wrWyeBLRp7T8J36GkT3RODJ6zYmL0/maWlAUD5LOXT28D3TDquUepyYDKYANNA3Gc8R5ZCgf+AXvSTYpJEWwQ=="
},
"@root/x509": {
"version": "0.7.2",
@ -64,9 +63,9 @@
"dev": true
},
"bluebird": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
"integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==",
"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": {
@ -135,9 +134,9 @@
}
},
"dotenv": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz",
"integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==",
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
},
"exit": {
@ -153,9 +152,9 @@
"dev": true
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
@ -182,9 +181,9 @@
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"minimatch": {

View File

@ -1,28 +1,32 @@
{
"name": "@root/acme",
"version": "3.0.0-wip.4",
"version": "3.1.0",
"description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt",
"homepage": "https://rootprojects.org/acme/",
"main": "acme.js",
"browser": {
"./native.js": "./browser.js",
"./lib/native.js": "./lib/browser.js",
"./lib/node/sha2.js": "./lib/browser/sha2.js",
"./lib/node/http.js": "./lib/browser/http.js"
"./lib/node/http.js": "./lib/browser/http.js",
"./lib/node/client-user-agent.js": "./lib/browser/client-user-agent.js"
},
"files": [
"*.js",
"lib",
"bin",
"scripts",
"dist"
],
"scripts": {
"build": "node_xxx bin/bundle.js",
"lint": "jshint lib bin",
"postinstall": "node scripts/postinstall",
"test": "node server.js",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://git.coolaj86.com/coolaj86/bluecrypt-acme.js.git"
"url": "https://git.rootprojects.org/root/acme.js.git"
},
"keywords": [
"ACME",
@ -38,14 +42,14 @@
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0",
"dependencies": {
"@root/csr": "^0.8.1",
"@root/encoding": "^1.0.1",
"@root/keypairs": "^0.9.0",
"@root/keypairs": "^0.10.0",
"@root/pem": "^1.0.4",
"@root/request": "^1.3.11",
"@root/request": "^1.6.1",
"@root/x509": "^0.7.2"
},
"devDependencies": {
"@root/csr": "^0.8.1",
"dig.js": "^1.3.9",
"dns-suite": "^1.2.13",
"dotenv": "^8.1.0",

4
scripts/postinstall Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
'use strict';
// TODO put postinstall message

View File

@ -0,0 +1,111 @@
'use strict';
var ACME = require('../');
var accountKey = require('../fixtures/account.jwk.json').private;
var authorization = {
identifier: {
type: 'dns',
value: 'example.com'
},
status: 'pending',
expires: '2018-04-25T00:23:57Z',
challenges: [
{
type: 'dns-01',
status: 'pending',
url:
'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755342',
token: 'LZdlUiZ-kWPs6q5WTmQFYQHZKpz9szn2vxEUu0XhyyM'
},
{
type: 'http-01',
status: 'pending',
url:
'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755343',
token: '1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU'
}
]
};
var expectedChallengeUrl =
'http://example.com/.well-known/acme-challenge/1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU';
var expectedKeyAuth =
'1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU.UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs';
var expectedKeyAuthDigest = 'iQiMcQUDiAeD0TJV1RHJuGnI5D2-PuSpxKz9JqUaZ2M';
var expectedDnsHost = '_test-challenge.example.com';
async function main() {
console.info('\n[Test] computing challenge authorizatin responses');
var challenges = authorization.challenges.slice(0);
function next() {
var ch = challenges.shift();
if (!ch) {
return null;
}
var hostname = authorization.identifier.value;
return ACME.computeChallenge({
accountKey: accountKey,
hostname: hostname,
challenge: ch,
dnsPrefix: '_test-challenge'
})
.then(function (auth) {
if ('dns-01' === ch.type) {
if (auth.keyAuthorizationDigest !== expectedKeyAuthDigest) {
console.error('[keyAuthorizationDigest]');
console.error(auth.keyAuthorizationDigest);
console.error(expectedKeyAuthDigest);
throw new Error('bad keyAuthDigest');
}
if (auth.dnsHost !== expectedDnsHost) {
console.error('[dnsHost]');
console.error(auth.dnsHost);
console.error(expectedDnsHost);
throw new Error('bad dnsHost');
}
} else if ('http-01' === ch.type) {
if (auth.challengeUrl !== expectedChallengeUrl) {
console.error('[challengeUrl]');
console.error(auth.challengeUrl);
console.error(expectedChallengeUrl);
throw new Error('bad challengeUrl');
}
if (auth.challengeUrl !== expectedChallengeUrl) {
console.error('[keyAuthorization]');
console.error(auth.keyAuthorization);
console.error(expectedKeyAuth);
throw new Error('bad keyAuth');
}
} else {
throw new Error('bad authorization inputs');
}
console.info('PASS', hostname, ch.type);
return next();
})
.catch(function (err) {
err.message =
'Error computing ' +
ch.type +
' for ' +
hostname +
':' +
err.message;
throw err;
});
}
return next();
}
module.exports = function () {
return main(authorization)
.then(function () {
console.info('PASS');
})
.catch(function (err) {
console.error(err.stack);
process.exit(1);
});
};

View File

@ -0,0 +1,80 @@
// 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';
/*
-----BEGIN CERTIFICATE-----LF
xxxLF
yyyLF
-----END CERTIFICATE-----LF
LF
-----BEGIN CERTIFICATE-----LF
xxxLF
yyyLF
-----END CERTIFICATE-----LF
Rules
* Only Unix LF (\n) Line endings
* Each PEM's lines are separated with \n
* Each PEM ends with \n
* Each PEM is separated with a \n (just like commas separating an array)
*/
// https://github.com/certbot/certbot/issues/5721#issuecomment-402362709
var expected = '----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n';
var tests = [
'----\r\nxxxx\r\nyyyy\r\n----\r\n\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n',
'----\r\nxxxx\r\nyyyy\r\n----\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n',
'----\nxxxx\nyyyy\n----\n\n----\r\nxxxx\r\nyyyy\r\n----',
'----\nxxxx\nyyyy\n----\n----\r\nxxxx\r\nyyyy\r\n----',
'----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----',
'----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----\n',
'----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n',
'----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n'
];
var ACME = require('../');
module.exports = function () {
console.info('\n[Test] can split and format PEM chain properly');
tests.forEach(function (str) {
var actual = ACME.formatPemChain(str);
if (expected !== actual) {
console.error('input: ', JSON.stringify(str));
console.error('expected:', JSON.stringify(expected));
console.error('actual: ', JSON.stringify(actual));
throw new Error('did not pass');
}
});
if (
'----\nxxxx\nyyyy\n----\n' !==
ACME.formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n')
) {
throw new Error('Not proper for single cert in chain');
}
if (
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !==
ACME.formatPemChain(
'\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n'
)
) {
throw new Error('Not proper for three certs in chain');
}
ACME.splitPemChain(
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n'
).forEach(function (str) {
if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) {
throw new Error('bad thingy');
}
});
console.info('PASS');
return Promise.resolve();
};

View File

@ -1,15 +1,27 @@
'use strict';
async function run() {
module.exports = async function () {
console.log('[Test] can generate, export, and import key');
var Keypairs = require('@root/keypairs');
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
console.log(certKeypair);
//console.log(certKeypair);
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
console.log(pem);
}
var jwk = await Keypairs.import({
pem: pem
});
['kty', 'd', 'n', 'e'].forEach(function (k) {
if (!jwk[k] || jwk[k] !== certKeypair.private[k]) {
throw new Error('bad export/import');
}
});
//console.log(pem);
console.log('PASS');
};
run();
if (require.main === module) {
module.exports();
}

View File

@ -1,225 +1,10 @@
'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);
}
async function main() {
await require('./generate-cert-key.js')();
await require('./format-pem-chains.js')();
await require('./compute-authorization-response.js')();
await require('./issue-certificates.js')();
}
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) + '例'
);
}
main();

255
tests/issue-certificates.js Normal file
View File

@ -0,0 +1,255 @@
'use strict';
require('dotenv').config();
var pkg = require('../package.json');
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 ecJwk = require('../fixtures/account.jwk.json');
// TODO exec npm install --save-dev CHALLENGE_MODULE
if (!process.env.CHALLENGE_OPTIONS) {
console.error(
'Please create a .env in the format of examples/example.env to run the tests'
);
process.exit(1);
}
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;
module.exports = function () {
console.info('\n[Test] end-to-end issue certificates');
var acme = ACME.create({
// debug: true
maintainerEmail: config.email,
packageAgent: 'test-' + pkg.name + '/' + pkg.version,
notify: function (ev, params) {
console.info(
'\t' + ev,
params.subject || params.altname || params.domain || '',
params.status || ''
);
if ('error' === ev) {
console.error(params.action || params.type || '');
console.error(params);
}
}
});
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 (/EC/i.test(accKty)) {
// to test that an existing account gets back data
accountKeypair = ecJwk;
}
var accountKey = accountKeypair.private;
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKey, null, 2));
console.info();
console.info();
}
var account = await acme.accounts.create({
agreeToTerms: agree,
// TODO detect jwk/pem/der?
accountKey: accountKey,
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(agreed);
}
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,
accountKey: accountKey,
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 () {
console.info('PASS: ECDSA account key with RSA server key');
// Now try RSA + EC
rnd = random();
return happyPath('RSA', 'EC', rnd).then(function () {
console.info('PASS: RSA account key with ECDSA server key');
});
})
.then(function () {
console.info('PASS');
})
.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) + '例'
);
}
};

71
tests/maintainer.js Normal file
View File

@ -0,0 +1,71 @@
'use strict';
var native = require('../lib/native.js');
var crypto = require('crypto');
native
._hashcash({
alg: 'SHA-256',
nonce: '00',
needle: '0000',
start: 0,
end: 2
})
.then(function (hashcash) {
if ('00:76de' !== hashcash) {
throw new Error('hashcash algorthim changed');
}
console.info('PASS: known hash solves correctly');
return native
._hashcash({
alg: 'SHA-256',
nonce: '10',
needle: '',
start: 0,
end: 2
})
.then(function (hashcash) {
if ('10:00' !== hashcash) {
throw new Error('hashcash algorthim changed');
}
console.info('PASS: empty hash solves correctly');
var now = Date.now();
var nonce = '20';
var needle = crypto.randomBytes(3).toString('hex').slice(0, 5);
native
._hashcash({
alg: 'SHA-256',
nonce: nonce,
needle: needle,
start: 0,
end: Math.ceil(needle.length / 2)
})
.then(function (hashcash) {
var later = Date.now();
var parts = hashcash.split(':');
var answer = parts[1];
if (parts[0] !== nonce) {
throw new Error('incorrect nonce');
}
var haystack = crypto
.createHash('sha256')
.update(Buffer.from(nonce + answer, 'hex'))
.digest()
.slice(0, Math.ceil(needle.length / 2));
if (
-1 === haystack.indexOf(Buffer.from(needle, 'hex'))
) {
throw new Error('incorrect solution');
}
if (later - now > 2000) {
throw new Error('took too long to solve');
}
console.info(
'PASS: rando hash solves correctly (and in good time - %dms)',
later - now
);
});
});
});

174
utils.js Normal file
View File

@ -0,0 +1,174 @@
'use strict';
var U = module.exports;
var Keypairs = require('@root/keypairs');
var UserAgent = require('./lib/node/client-user-agent.js');
// Handle nonce, signing, and request altogether
U._jwsRequest = function (me, bigopts) {
return U._getNonce(me).then(function (nonce) {
bigopts.protected.nonce = nonce;
bigopts.protected.url = bigopts.url;
// protected.alg: added by Keypairs.signJws
if (bigopts.protected.jwk) {
bigopts.protected.kid = false;
} else if (!('kid' in bigopts.protected)) {
// protected.kid must be provided according to ACME's interpretation of the spec
// (using the provided URL rather than the Key's Thumbprint as Key ID)
bigopts.protected.kid = bigopts.kid;
}
// this will shasum the thumbprint the 2nd time
return Keypairs.signJws({
jwk: bigopts.accountKey,
protected: bigopts.protected,
payload: bigopts.payload
})
.then(function (jws) {
//#console.debug('[ACME.js] url: ' + bigopts.url + ':');
//#console.debug(jws);
return U._request(me, { url: bigopts.url, json: jws });
})
.catch(function (e) {
if (/badNonce$/.test(e.urn)) {
// retry badNonces
var retryable = bigopts._retries >= 2;
if (!retryable) {
bigopts._retries = (bigopts._retries || 0) + 1;
return U._jwsRequest(me, bigopts);
}
}
throw e;
});
});
};
U._getNonce = function (me) {
var nonce;
while (true) {
nonce = me._nonces.shift();
if (!nonce) {
break;
}
if (Date.now() - nonce.createdAt > 15 * 60 * 1000) {
nonce = null;
} else {
break;
}
}
if (nonce) {
return Promise.resolve(nonce.nonce);
}
// HEAD-as-HEAD ok
return U._request(me, {
method: 'HEAD',
url: me._directoryUrls.newNonce
}).then(function (resp) {
return resp.headers['replay-nonce'];
});
};
// Handle some ACME-specific defaults
U._request = function (me, opts) {
// no-op on browser
var ua = UserAgent.get(me, opts);
// Note: the required User-Agent string will be set in node, but not browsers
if (!opts.headers) {
opts.headers = {};
}
if (ua && !opts.headers['User-Agent']) {
opts.headers['User-Agent'] = ua;
}
if (opts.json) {
opts.headers.Accept = 'application/json';
if (true !== opts.json) {
opts.body = JSON.stringify(opts.json);
}
if (/*opts.jose ||*/ opts.json.protected) {
opts.headers['Content-Type'] = 'application/jose+json';
}
}
if (!opts.method) {
opts.method = 'GET';
if (opts.body) {
opts.method = 'POST';
}
}
//console.log('\n[debug] REQUEST');
//console.log(opts);
return me.__request(opts).then(function (resp) {
if (resp.toJSON) {
resp = resp.toJSON();
}
if (resp.headers['replay-nonce']) {
U._setNonce(me, resp.headers['replay-nonce']);
}
//console.log('[debug] RESPONSE:');
//console.log(resp.headers);
//console.log(resp.body);
var e;
var err;
if (resp.body) {
err = resp.body.error;
e = new Error('');
if (400 === resp.body.status) {
err = { type: resp.body.type, detail: resp.body.detail };
}
if (err) {
e.status = resp.body.status;
e.code = 'E_ACME';
if (e.status) {
e.message = '[' + e.status + '] ';
}
e.detail = err.detail;
e.message += err.detail || JSON.stringify(err);
e.urn = err.type;
e.uri = resp.body.url;
e._rawError = err;
e._rawBody = resp.body;
throw e;
}
}
return resp;
});
};
U._setNonce = function (me, nonce) {
me._nonces.unshift({ nonce: nonce, createdAt: Date.now() });
};
U._importKeypair = function (key) {
var p;
var pub;
if (key && key.kty) {
// nix the browser jwk extras
key.key_ops = undefined;
key.ext = undefined;
pub = Keypairs.neuter({ jwk: key });
p = Promise.resolve({
private: key,
public: pub
});
} else if ('string' === typeof key) {
p = Keypairs.import({ pem: key });
} else {
throw new Error('no private key given');
}
return p.then(function (pair) {
if (pair.public.kid) {
pair = JSON.parse(JSON.stringify(pair));
delete pair.public.kid;
delete pair.private.kid;
}
return pair;
});
};