Compare commits
146 Commits
browser-v2
...
master
Author | SHA1 | Date |
---|---|---|
AJ ONeal | bef931f28f | |
AJ ONeal | eb432571ca | |
AJ ONeal | 29a47e8fa4 | |
AJ ONeal | 87e3555a5a | |
AJ ONeal | 569c922eb0 | |
AJ ONeal | d10482697b | |
AJ ONeal | aa324e2a29 | |
AJ ONeal | e8c46db062 | |
AJ ONeal | 6352961fea | |
AJ ONeal | 333605d9b8 | |
AJ ONeal | 86068fe015 | |
AJ ONeal | cf0ee1c064 | |
AJ ONeal | 606dcf3c4f | |
AJ ONeal | 0803517711 | |
AJ ONeal | 0b91d9a26d | |
AJ ONeal | 0743aa5280 | |
AJ ONeal | e388bc31bc | |
AJ ONeal | 754c623cd1 | |
AJ ONeal | 0107bc1d1f | |
AJ ONeal | 293d950d8c | |
AJ ONeal | d6a3a7939b | |
AJ ONeal | fcbffdc0f9 | |
AJ ONeal | e447d71112 | |
AJ ONeal | 5490f858d9 | |
AJ ONeal | a99a0cc211 | |
AJ ONeal | e0bec09e43 | |
AJ ONeal | 148846b18a | |
AJ ONeal | b1c591b6ed | |
AJ ONeal | 4e7ff0d9e8 | |
AJ ONeal | b39a3763cf | |
AJ ONeal | 54cda5a888 | |
AJ ONeal | 90c7154a24 | |
AJ ONeal | 161e9183c6 | |
AJ ONeal | 7f868f350b | |
AJ ONeal | 30f4306c05 | |
AJ ONeal | 0efa94eeb0 | |
AJ ONeal | f05e9db38e | |
AJ ONeal | 7e6a66c1d8 | |
AJ ONeal | b1046222dc | |
AJ ONeal | d25fa6756c | |
AJ ONeal | c89e5b7882 | |
AJ ONeal | 4b79b0bb3a | |
AJ ONeal | ad42d34587 | |
AJ ONeal | d7b3e2e1db | |
AJ ONeal | f363f5ef02 | |
AJ ONeal | 1f2169c78c | |
AJ ONeal | c5757d2650 | |
AJ ONeal | 9139d89143 | |
AJ ONeal | f7fd435443 | |
AJ ONeal | 20c11d5df7 | |
AJ ONeal | 21f7e87606 | |
AJ ONeal | 5623ed1914 | |
AJ ONeal | cd35f26e95 | |
AJ ONeal | 83cf96f074 | |
AJ ONeal | edc830696e | |
AJ ONeal | d17a373a89 | |
AJ ONeal | 0d26a42bc7 | |
AJ ONeal | 8e2763ecd6 | |
AJ ONeal | 76f98f7c7e | |
AJ ONeal | 080497bf4c | |
AJ ONeal | 2b0fce0869 | |
AJ ONeal | 96b491a9c0 | |
AJ ONeal | 499ac7f8ea | |
AJ ONeal | 24c3633d75 | |
AJ ONeal | e75c503356 | |
AJ ONeal | e214f5e639 | |
AJ ONeal | 96a6de30a1 | |
AJ ONeal | a4f92e260c | |
AJ ONeal | 0599acab6d | |
AJ ONeal | 90477942d1 | |
AJ ONeal | e6497fe34b | |
AJ ONeal | dfbee8aa79 | |
AJ ONeal | 17a1535dcc | |
AJ ONeal | 54e9e9ec16 | |
AJ ONeal | a750d1b0b4 | |
AJ ONeal | 3f4e5adeef | |
AJ ONeal | ea97f537ef | |
AJ ONeal | 6deb67d740 | |
AJ ONeal | 1195956ce1 | |
AJ ONeal | 6521121548 | |
AJ ONeal | b1d566d54e | |
AJ ONeal | 401535a5ab | |
AJ ONeal | ddeaeb17d5 | |
AJ ONeal | 6d34655276 | |
AJ ONeal | 8175a08495 | |
AJ ONeal | 382ef3c95c | |
AJ ONeal | d802fb4957 | |
AJ ONeal | 85a38f7b54 | |
AJ ONeal | 2406c870e6 | |
AJ ONeal | 3ae21fe62a | |
AJ ONeal | 2051fb0e4b | |
AJ ONeal | 1649b52f24 | |
AJ ONeal | 0f20783f12 | |
AJ ONeal | aa42853639 | |
AJ ONeal | 8e345c09ae | |
AJ ONeal | e704419cdb | |
AJ ONeal | 0a5a72e2fc | |
AJ ONeal | 6c811d880c | |
AJ ONeal | 668e2bb0ac | |
AJ ONeal | 8117b1fd66 | |
AJ ONeal | 2ba7db1327 | |
AJ ONeal | 0214d80f80 | |
AJ ONeal | f7d1c5615e | |
John Shaver | 16cbe77dff | |
jshaver | 479f6d99e6 | |
John Shaver | 119f5a9ae4 | |
AJ ONeal | 208834130d | |
AJ ONeal | 6b78aa7cee | |
AJ ONeal | 84c3e91ea8 | |
AJ ONeal | bfff22f053 | |
AJ ONeal | d0745a6347 | |
AJ ONeal | d54c1380f3 | |
AJ ONeal | 473f373de3 | |
AJ ONeal | 3cf7824bed | |
AJ ONeal | 4e2649d797 | |
AJ ONeal | 290251a1b0 | |
AJ ONeal | 474bb64004 | |
AJ ONeal | e2c3faeb82 | |
AJ ONeal | deb87cdec9 | |
AJ ONeal | 8c8e3d843a | |
AJ ONeal | 12d5518be3 | |
AJ ONeal | d0a58be97d | |
AJ ONeal | 65ff4a0feb | |
AJ ONeal | a88486c313 | |
AJ ONeal | 9cdac50dbc | |
AJ ONeal | fdd5a88de8 | |
AJ ONeal | 71e0faec95 | |
AJ ONeal | e31e72b0b8 | |
AJ ONeal | 263eed0475 | |
AJ ONeal | b630c118cc | |
AJ ONeal | da8b49d46b | |
AJ ONeal | 3a6269aafa | |
AJ ONeal | 2e747bada2 | |
AJ ONeal | 38cefafe33 | |
AJ ONeal | f486bca73e | |
AJ ONeal | ef0505ed69 | |
AJ ONeal | fe5a48764b | |
AJ ONeal | 27fb85ed9c | |
AJ ONeal | b70b1002b9 | |
AJ ONeal | 1f71a91979 | |
AJ ONeal | a38d751cfa | |
AJ ONeal | 08e6fdc1a7 | |
AJ ONeal | afbcef688f | |
AJ ONeal | df022959e4 | |
AJ ONeal | 2a3849cf1b | |
AJ ONeal | 4c4eaa83b7 |
|
@ -1,5 +1,21 @@
|
||||||
dist/
|
.env
|
||||||
*.gz
|
*.gz
|
||||||
.*.sw*
|
.*.sw*
|
||||||
.ignore
|
.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
|
node_modules
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
491
README.md
491
README.md
|
@ -1,25 +1,39 @@
|
||||||
# Bluecrypt™ [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) | A [Root](https://rootprojects.org/acme/) project
|
# Let's Encrypt™ + JavaScript = [ACME.js](https://git.rootprojects.org/root/acme.js)
|
||||||
|
|
||||||
Free SSL Certificates from Let's Encrypt, right in your Web Browser
|
| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub)
|
||||||
|
|
||||||
Lightweight. Fast. Modern Crypto. Zero dependecies.
|
## Automated Certificate Management Environment
|
||||||
|
|
||||||
(a port of [acme.js](https://git.coolaj86.com/coolaj86/acme-v2.js) to the browser)
|
ACME ([RFC 8555](https://tools.ietf.org/html/rfc8555)) is the protocol that powers **Let's Encrypt**.
|
||||||
|
|
||||||
# Features
|
ACME.js is a _low-level_ client that speaks RFC 8555 to get Free SSL certificates through Let's Encrypt.
|
||||||
|
|
||||||
| 15k gzipped | 55k minified | 88k (2,500 loc) source with comments |
|
Looking for an **easy**, _high-level_ client? Check out [Greenlock.js](https://git.rootprojects.org/root/greenlock.js).
|
||||||
|
|
||||||
* [x] Let's Encrypt
|
# Quick Start
|
||||||
* [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] VanillaJS, Zero Dependencies
|
|
||||||
|
|
||||||
# Online Demos
|
```js
|
||||||
|
var acme = ACME.create({ maintainerEmail, packageAgent, notify });
|
||||||
|
await acme.init(directoryUrl);
|
||||||
|
|
||||||
* Greenlock for the Web <https://greenlock.domains>
|
// Create Let's Encrypt Account
|
||||||
* Bluecrypt ACME Demo <https://rootprojects.org/acme/>
|
var accountOptions = { subscriberEmail, agreeToTerms, accountKey };
|
||||||
|
var account = await acme.accounts.create(accountOptions);
|
||||||
|
|
||||||
|
// Validate Domains
|
||||||
|
var certificateOptions = { account, accountKey, csr, domains, challenges };
|
||||||
|
var pems = await acme.certificates.create(certificateOptions);
|
||||||
|
|
||||||
|
// Get SSL Certificate
|
||||||
|
var fullchain = pems.cert + '\n' + pems.chain + '\n';
|
||||||
|
await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii');
|
||||||
|
```
|
||||||
|
|
||||||
|
# Online Demo
|
||||||
|
|
||||||
|
See https://greenlock.domains
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
We expect that our hosted versions will meet all of yours needs.
|
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.
|
If they don't, please open an issue to let us know why.
|
||||||
|
@ -27,164 +41,397 @@ 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.
|
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.
|
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
|
||||||
|
|
||||||
# QuickStart
|
-->
|
||||||
|
|
||||||
Bluecrypt ACME embeds [Keypairs.js](https://git.rootprojects.org/root/bluecrypt-keypairs.js)
|
# Features
|
||||||
and [CSR.js](https://git.rootprojects.org/root/bluecrypt-csr.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™ (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
|
||||||
|
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
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Helper Functions
|
||||||
|
|
||||||
|
```js
|
||||||
|
ACME.computeChallenge({
|
||||||
|
accountKey,
|
||||||
|
hostname: 'example.com',
|
||||||
|
challenge: { type: 'dns-01', token: 'xxxx' }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
**Maintainer vs Subscriber vs Customer**
|
||||||
|
|
||||||
|
- `maintainerEmail` should be the email address of the **author of the code**.
|
||||||
|
This person will receive critical security and API change notifications.
|
||||||
|
- `subscriberEmail` should be the email of the **admin of the hosting service**.
|
||||||
|
This person agrees to the Let's Encrypt Terms of Service and will be notified
|
||||||
|
when a certificate fails to renew.
|
||||||
|
- `customerEmail` should be the email of individual who owns the domain.
|
||||||
|
This is optional (not currently implemented).
|
||||||
|
|
||||||
|
Generally speaking **YOU** are the _maintainer_ and you **or your employer** is the _subscriber_.
|
||||||
|
|
||||||
|
If you (or your employer) is running any type of service
|
||||||
|
you **SHOULD NOT** pass the _customer_ email as the subscriber email.
|
||||||
|
|
||||||
|
If you are not running a service (you may be building a CLI, for example),
|
||||||
|
then you should prompt the user for their email address, and they are the subscriber.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
These `notify` events are intended for _logging_ and debugging, NOT as a data API.
|
||||||
|
|
||||||
|
| 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
|
||||||
|
npm install --save @root/acme
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ACME = require('@root/acme');
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>WebPack</summary>
|
||||||
|
|
||||||
`bluecrypt-acme.js`
|
|
||||||
```html
|
```html
|
||||||
<script src="https://rootprojects.org/acme/bluecrypt-acme.js"></script>
|
<meta charset="UTF-8" />
|
||||||
```
|
```
|
||||||
|
|
||||||
`bluecrypt-acme.min.js`
|
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ACME = require('@root/acme');
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Vanilla JS</summary>
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://rootprojects.org/acme/bluecrypt-acme.min.js"></script>
|
<meta charset="UTF-8" />
|
||||||
```
|
```
|
||||||
|
|
||||||
You can see `index.html` and `app.js` in the repo for full example usage.
|
(necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`)
|
||||||
|
|
||||||
### Instantiate Bluecrypt ACME
|
```html
|
||||||
|
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.all.js"></script>
|
||||||
Although built for Let's Encrypt, Bluecrypt ACME will work with any server
|
|
||||||
that supports draft-15 of the ACME spec (includes POST-as-GET support).
|
|
||||||
|
|
||||||
The `init()` method takes a _directory url_ and initializes internal state according to its response.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var acme = ACME.create({});
|
|
||||||
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function () {
|
|
||||||
// Ready to use, show page
|
|
||||||
$('body').hidden = false;
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create ACME Account with Let's Encrypt
|
`acme.min.js`
|
||||||
|
|
||||||
ACME Accounts are key and device based, with an email address as a backup identifier.
|
```html
|
||||||
|
<script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.all.min.js"></script>
|
||||||
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 Bluecrypt and Let's Encrypt Terms of Service?")) {
|
|
||||||
return Promise.resolve(tos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, accountKeypair: { privateKeyJwk: pair.private }
|
|
||||||
, email: $('.js-email-input').value
|
|
||||||
}).then(function (_account) {
|
|
||||||
account = _account;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Get Free 90-day SSL Certificate
|
Use
|
||||||
|
|
||||||
Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key,
|
|
||||||
the names of domains to be secured, and a distinctly separate server private key.
|
|
||||||
|
|
||||||
A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record)
|
|
||||||
is a required part of the process, which requires `set` and `remove` callbacks/promises.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var serverPrivateKey;
|
var ACME = window['@root/acme'];
|
||||||
|
|
||||||
Keypairs.generate({ kty: 'EC' }).then(function (pair) {
|
|
||||||
serverPrivateKey = pair.private;
|
|
||||||
|
|
||||||
return acme.certificates.create({
|
|
||||||
agreeToTerms: function (tos) {
|
|
||||||
return tos;
|
|
||||||
}
|
|
||||||
, account: account
|
|
||||||
, accountKeypair: { privateKeyJwk: accountPrivateKey }
|
|
||||||
, serverKeypair: { privateKeyJwk: serverPrivateKey }
|
|
||||||
, domains: ['example.com','www.example.com']
|
|
||||||
, challenges: challenges // must be implemented
|
|
||||||
, skipDryRun: true
|
|
||||||
}).then(function (results) {
|
|
||||||
console.log('Got SSL Certificate:');
|
|
||||||
console.log(results.expires);
|
|
||||||
console.log(results.cert);
|
|
||||||
console.log(results.chain);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example "Challenge" Implementation
|
</details>
|
||||||
|
|
||||||
Typically here you're just presenting some sort of dialog to the user to ask them to
|
# Challenge Callbacks
|
||||||
upload a file or set a DNS record.
|
|
||||||
|
|
||||||
It may be possible to do something fancy like using OAuth2 to login to Google Domanis
|
The challenge callbacks are documented in the [test suite](https://git.rootprojects.org/root/acme-dns-01-test.js),
|
||||||
to set a DNS address, etc, but it seems like that sort of fanciness is probably best
|
essentially:
|
||||||
reserved for server-side plugins.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var challenges = {
|
function create(options) {
|
||||||
'http-01': {
|
var plugin = {
|
||||||
set: function (opts) {
|
init: async function(deps) {
|
||||||
console.info('http-01 set challenge:');
|
// for http requests
|
||||||
console.info(opts.challengeUrl);
|
plugin.request = deps.request;
|
||||||
console.info(opts.keyAuthorization);
|
},
|
||||||
while (!window.confirm("Upload the challenge file before continuing.")) {}
|
zones: async function(args) {
|
||||||
return Promise.resolve();
|
// list zones relevant to the altnames
|
||||||
}
|
},
|
||||||
, remove: function (opts) {
|
set: async function(args) {
|
||||||
console.log('http-01 remove challenge:', opts.challengeUrl);
|
// set TXT record
|
||||||
return Promise.resolve();
|
},
|
||||||
}
|
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;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Full Documentation
|
The `http-01` plugin is similar, but without `zones` or `propagationDelay`.
|
||||||
|
|
||||||
See [acme.js](https://git.coolaj86.com/coolaj86/acme-v2.js).
|
Many challenge plugins are already available for popular platforms.
|
||||||
|
|
||||||
Aside from the loading instructions (`npm` and `require` instead of `script` tags),
|
Search `acme-http-01-` or `acme-dns-01-` on npm to find more.
|
||||||
the usage is identical to the node version.
|
|
||||||
|
|
||||||
That said, the two may leap-frog a little from time to time
|
| Type | Service | Plugin |
|
||||||
(for example, the browser version is just a touch ahead at the moment).
|
| ----------- | ----------------------------------------------------------------------------------- | ------------------------ |
|
||||||
|
| 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.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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usa a dns-01 challenge
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENV=DEV
|
||||||
|
MAINTAINER_EMAIL=letsencrypt+staging@example.com
|
||||||
|
SUBSCRIBER_EMAIL=letsencrypt+staging@example.com
|
||||||
|
BASE_DOMAIN=test.example.com
|
||||||
|
CHALLENGE_TYPE=dns-01
|
||||||
|
CHALLENGE_PLUGIN=acme-dns-01-digitalocean
|
||||||
|
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the repo and change directories into it
|
||||||
|
git clone https://git.rootprojects.org/root/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
|
||||||
|
|
||||||
|
# Edit the config file to use a domain in your account, and your API token
|
||||||
|
#vim .env
|
||||||
|
code .env
|
||||||
|
|
||||||
|
# Run the tests
|
||||||
|
node tests/index.js
|
||||||
|
```
|
||||||
|
|
||||||
# Developing
|
# 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.
|
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
|
# 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,
|
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
|
||||||
Enterprise, and Internal installations, integrations, and deployments.
|
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.
|
We also offer consulting for all-things-ACME and Let's Encrypt.
|
||||||
|
|
||||||
# Legal & Rules of the Road
|
# Legal & Rules of the Road
|
||||||
|
|
||||||
Bluecrypt™ and Greenlock™ are [trademarks](https://rootprojects.org/legal/#trademark) of AJ ONeal
|
ACME.js™ is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
|
||||||
|
|
||||||
The rule of thumb is "attribute, but don't confuse". For example:
|
The rule of thumb is "attribute, but don't confuse". For example:
|
||||||
|
|
||||||
> Built with [Bluecrypt ACME](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,
|
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.
|
attribution, and/or visible source policies. We want to build great software and a great community.
|
||||||
|
|
||||||
[bluecrypt.js](https://git.coolaj86.com/coolaj86/bluecrypt.js) |
|
[ACME.js](https://git.rootprojects.org/root/acme.js) |
|
||||||
MPL-2.0 |
|
MPL-2.0 |
|
||||||
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||||
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||||
|
|
|
@ -0,0 +1,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);
|
||||||
|
};
|
274
app.js
274
app.js
|
@ -1,274 +0,0 @@
|
||||||
/*global Promise*/
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var Keypairs = window.Keypairs;
|
|
||||||
var Rasha = window.Rasha;
|
|
||||||
var Eckles = window.Eckles;
|
|
||||||
var x509 = window.x509;
|
|
||||||
var CSR = window.CSR;
|
|
||||||
var ACME = window.ACME;
|
|
||||||
var accountStuff = {};
|
|
||||||
|
|
||||||
function $(sel) {
|
|
||||||
return document.querySelector(sel);
|
|
||||||
}
|
|
||||||
function $$(sel) {
|
|
||||||
return Array.prototype.slice.call(document.querySelectorAll(sel));
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkTos(tos) {
|
|
||||||
if ($('input[name="tos"]:checked')) {
|
|
||||||
return tos;
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
console.log('hello');
|
|
||||||
|
|
||||||
// Show different options for ECDSA vs RSA
|
|
||||||
$$('input[name="kty"]').forEach(function ($el) {
|
|
||||||
$el.addEventListener('change', function (ev) {
|
|
||||||
console.log(this);
|
|
||||||
console.log(ev);
|
|
||||||
if ("RSA" === ev.target.value) {
|
|
||||||
$('.js-rsa-opts').hidden = false;
|
|
||||||
$('.js-ec-opts').hidden = true;
|
|
||||||
} else {
|
|
||||||
$('.js-rsa-opts').hidden = true;
|
|
||||||
$('.js-ec-opts').hidden = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate a key on submit
|
|
||||||
$('form.js-keygen').addEventListener('submit', function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
$('.js-loading').hidden = false;
|
|
||||||
$('.js-jwk').hidden = true;
|
|
||||||
$('.js-toc-der-public').hidden = true;
|
|
||||||
$('.js-toc-der-private').hidden = true;
|
|
||||||
$$('.js-toc-pem').forEach(function ($el) {
|
|
||||||
$el.hidden = true;
|
|
||||||
});
|
|
||||||
$$('input').map(function ($el) { $el.disabled = true; });
|
|
||||||
$$('button').map(function ($el) { $el.disabled = true; });
|
|
||||||
var opts = {
|
|
||||||
kty: $('input[name="kty"]:checked').value
|
|
||||||
, namedCurve: $('input[name="ec-crv"]:checked').value
|
|
||||||
, modulusLength: $('input[name="rsa-len"]:checked').value
|
|
||||||
};
|
|
||||||
var then = Date.now();
|
|
||||||
console.log('opts', opts);
|
|
||||||
Keypairs.generate(opts).then(function (results) {
|
|
||||||
console.log("Key generation time:", (Date.now() - then) + "ms");
|
|
||||||
var pubDer;
|
|
||||||
var privDer;
|
|
||||||
if (/EC/i.test(opts.kty)) {
|
|
||||||
privDer = x509.packPkcs8(results.private);
|
|
||||||
pubDer = x509.packSpki(results.public);
|
|
||||||
Eckles.export({ jwk: results.private, format: 'sec1' }).then(function (pem) {
|
|
||||||
$('.js-input-pem-sec1-private').innerText = pem;
|
|
||||||
$('.js-toc-pem-sec1-private').hidden = false;
|
|
||||||
});
|
|
||||||
Eckles.export({ jwk: results.private, format: 'pkcs8' }).then(function (pem) {
|
|
||||||
$('.js-input-pem-pkcs8-private').innerText = pem;
|
|
||||||
$('.js-toc-pem-pkcs8-private').hidden = false;
|
|
||||||
});
|
|
||||||
Eckles.export({ jwk: results.public, public: true }).then(function (pem) {
|
|
||||||
$('.js-input-pem-spki-public').innerText = pem;
|
|
||||||
$('.js-toc-pem-spki-public').hidden = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
privDer = x509.packPkcs8(results.private);
|
|
||||||
pubDer = x509.packSpki(results.public);
|
|
||||||
Rasha.export({ jwk: results.private, format: 'pkcs1' }).then(function (pem) {
|
|
||||||
$('.js-input-pem-pkcs1-private').innerText = pem;
|
|
||||||
$('.js-toc-pem-pkcs1-private').hidden = false;
|
|
||||||
});
|
|
||||||
Rasha.export({ jwk: results.private, format: 'pkcs8' }).then(function (pem) {
|
|
||||||
$('.js-input-pem-pkcs8-private').innerText = pem;
|
|
||||||
$('.js-toc-pem-pkcs8-private').hidden = false;
|
|
||||||
});
|
|
||||||
Rasha.export({ jwk: results.public, format: 'pkcs1' }).then(function (pem) {
|
|
||||||
$('.js-input-pem-pkcs1-public').innerText = pem;
|
|
||||||
$('.js-toc-pem-pkcs1-public').hidden = false;
|
|
||||||
});
|
|
||||||
Rasha.export({ jwk: results.public, format: 'spki' }).then(function (pem) {
|
|
||||||
$('.js-input-pem-spki-public').innerText = pem;
|
|
||||||
$('.js-toc-pem-spki-public').hidden = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.js-der-public').innerText = pubDer;
|
|
||||||
$('.js-toc-der-public').hidden = false;
|
|
||||||
$('.js-der-private').innerText = privDer;
|
|
||||||
$('.js-toc-der-private').hidden = false;
|
|
||||||
$('.js-jwk').innerText = JSON.stringify(results, null, 2);
|
|
||||||
$('.js-loading').hidden = true;
|
|
||||||
$('.js-jwk').hidden = false;
|
|
||||||
$$('input').map(function ($el) { $el.disabled = false; });
|
|
||||||
$$('button').map(function ($el) { $el.disabled = false; });
|
|
||||||
$('.js-toc-jwk').hidden = false;
|
|
||||||
|
|
||||||
$('.js-create-account').hidden = false;
|
|
||||||
$('.js-create-csr').hidden = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('form.js-acme-account').addEventListener('submit', function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
$('.js-loading').hidden = false;
|
|
||||||
var acme = ACME.create({
|
|
||||||
Keypairs: Keypairs
|
|
||||||
, CSR: CSR
|
|
||||||
});
|
|
||||||
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) {
|
|
||||||
console.log('acme result', result);
|
|
||||||
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
|
||||||
var email = $('.js-email').value;
|
|
||||||
return acme.accounts.create({
|
|
||||||
email: email
|
|
||||||
, agreeToTerms: checkTos
|
|
||||||
, accountKeypair: { privateKeyJwk: privJwk }
|
|
||||||
}).then(function (account) {
|
|
||||||
console.log("account created result:", account);
|
|
||||||
accountStuff.account = account;
|
|
||||||
accountStuff.privateJwk = privJwk;
|
|
||||||
accountStuff.email = email;
|
|
||||||
accountStuff.acme = acme;
|
|
||||||
$('.js-create-order').hidden = false;
|
|
||||||
$('.js-toc-acme-account-response').hidden = false;
|
|
||||||
$('.js-acme-account-response').innerText = JSON.stringify(account, null, 2);
|
|
||||||
}).catch(function (err) {
|
|
||||||
console.error("A bad thing happened:");
|
|
||||||
console.error(err);
|
|
||||||
window.alert(err.message || JSON.stringify(err, null, 2));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('form.js-csr').addEventListener('submit', function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
generateCsr();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('form.js-acme-order').addEventListener('submit', function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
var account = accountStuff.account;
|
|
||||||
var privJwk = accountStuff.privateJwk;
|
|
||||||
var email = accountStuff.email;
|
|
||||||
var acme = accountStuff.acme;
|
|
||||||
|
|
||||||
|
|
||||||
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
|
||||||
return getDomainPrivkey().then(function (domainPrivJwk) {
|
|
||||||
console.log('Has CSR already?');
|
|
||||||
console.log(accountStuff.csr);
|
|
||||||
return acme.certificates.create({
|
|
||||||
accountKeypair: { privateKeyJwk: privJwk }
|
|
||||||
, account: account
|
|
||||||
, serverKeypair: { privateKeyJwk: domainPrivJwk }
|
|
||||||
, csr: accountStuff.csr
|
|
||||||
, domains: domains
|
|
||||||
, skipDryRun: $('input[name="skip-dryrun"]:checked') && true
|
|
||||||
, agreeToTerms: checkTos
|
|
||||||
, challenges: {
|
|
||||||
'dns-01': {
|
|
||||||
set: function (opts) {
|
|
||||||
console.info('dns-01 set challenge:');
|
|
||||||
console.info('TXT', opts.dnsHost);
|
|
||||||
console.info(opts.dnsAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you set the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
, remove: function (opts) {
|
|
||||||
console.log('dns-01 remove challenge:');
|
|
||||||
console.info('TXT', opts.dnsHost);
|
|
||||||
console.info(opts.dnsAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you delete the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, 'http-01': {
|
|
||||||
set: function (opts) {
|
|
||||||
console.info('http-01 set challenge:');
|
|
||||||
console.info(opts.challengeUrl);
|
|
||||||
console.info(opts.keyAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you set the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
, remove: function (opts) {
|
|
||||||
console.log('http-01 remove challenge:');
|
|
||||||
console.info(opts.challengeUrl);
|
|
||||||
console.info(opts.keyAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you delete the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, challengeTypes: [$('input[name="acme-challenge-type"]:checked').value]
|
|
||||||
}).then(function (results) {
|
|
||||||
console.log('Got Certificates:');
|
|
||||||
console.log(results);
|
|
||||||
$('.js-toc-acme-order-response').hidden = false;
|
|
||||||
$('.js-acme-order-response').innerText = JSON.stringify(results, null, 2);
|
|
||||||
}).catch(function (err) {
|
|
||||||
console.error("challenge failed:");
|
|
||||||
console.error(err);
|
|
||||||
window.alert("failed! " + err.message || JSON.stringify(err));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.js-generate').hidden = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDomainPrivkey() {
|
|
||||||
if (accountStuff.domainPrivateJwk) { return Promise.resolve(accountStuff.domainPrivateJwk); }
|
|
||||||
return Keypairs.generate({
|
|
||||||
kty: $('input[name="kty"]:checked').value
|
|
||||||
, namedCurve: $('input[name="ec-crv"]:checked').value
|
|
||||||
, modulusLength: $('input[name="rsa-len"]:checked').value
|
|
||||||
}).then(function (pair) {
|
|
||||||
console.log('domain keypair:', pair);
|
|
||||||
accountStuff.domainPrivateJwk = pair.private;
|
|
||||||
return pair.private;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateCsr() {
|
|
||||||
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
|
||||||
//var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
|
||||||
return getDomainPrivkey().then(function (privJwk) {
|
|
||||||
accountStuff.domainPrivateJwk = privJwk;
|
|
||||||
return CSR({ jwk: privJwk, domains: domains }).then(function (pem) {
|
|
||||||
// Verify with https://www.sslshopper.com/csr-decoder.html
|
|
||||||
accountStuff.csr = pem;
|
|
||||||
console.log('Created CSR:');
|
|
||||||
console.log(pem);
|
|
||||||
|
|
||||||
console.log('CSR info:');
|
|
||||||
console.log(CSR._info(pem));
|
|
||||||
|
|
||||||
return pem;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', run);
|
|
||||||
}());
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
};
|
|
@ -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
|
||||||
|
```
|
|
@ -0,0 +1,340 @@
|
||||||
|
/*global Promise*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Keypairs = require('@root/keypairs');
|
||||||
|
var Rasha = require('@root/acme/rsa');
|
||||||
|
var Eckles = require('@root/acme/ecdsa');
|
||||||
|
var x509 = require('@root/acme/x509');
|
||||||
|
var CSR = require('@root/csr');
|
||||||
|
var ACME = require('@root/acme');
|
||||||
|
var accountStuff = {};
|
||||||
|
|
||||||
|
function $(sel) {
|
||||||
|
return document.querySelector(sel);
|
||||||
|
}
|
||||||
|
function $$(sel) {
|
||||||
|
return Array.prototype.slice.call(document.querySelectorAll(sel));
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTos(tos) {
|
||||||
|
if ($('input[name="tos"]:checked')) {
|
||||||
|
return tos;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
console.log('hello');
|
||||||
|
|
||||||
|
// Show different options for ECDSA vs RSA
|
||||||
|
$$('input[name="kty"]').forEach(function ($el) {
|
||||||
|
$el.addEventListener('change', function (ev) {
|
||||||
|
console.log(this);
|
||||||
|
console.log(ev);
|
||||||
|
if ('RSA' === ev.target.value) {
|
||||||
|
$('.js-rsa-opts').hidden = false;
|
||||||
|
$('.js-ec-opts').hidden = true;
|
||||||
|
} else {
|
||||||
|
$('.js-rsa-opts').hidden = true;
|
||||||
|
$('.js-ec-opts').hidden = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate a key on submit
|
||||||
|
$('form.js-keygen').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
$('.js-loading').hidden = false;
|
||||||
|
$('.js-jwk').hidden = true;
|
||||||
|
$('.js-toc-der-public').hidden = true;
|
||||||
|
$('.js-toc-der-private').hidden = true;
|
||||||
|
$$('.js-toc-pem').forEach(function ($el) {
|
||||||
|
$el.hidden = true;
|
||||||
|
});
|
||||||
|
$$('input').map(function ($el) {
|
||||||
|
$el.disabled = true;
|
||||||
|
});
|
||||||
|
$$('button').map(function ($el) {
|
||||||
|
$el.disabled = true;
|
||||||
|
});
|
||||||
|
var opts = {
|
||||||
|
kty: $('input[name="kty"]:checked').value,
|
||||||
|
namedCurve: $('input[name="ec-crv"]:checked').value,
|
||||||
|
modulusLength: $('input[name="rsa-len"]:checked').value
|
||||||
|
};
|
||||||
|
var then = Date.now();
|
||||||
|
console.log('opts', opts);
|
||||||
|
Keypairs.generate(opts).then(function (results) {
|
||||||
|
console.log('Key generation time:', Date.now() - then + 'ms');
|
||||||
|
var pubDer;
|
||||||
|
var privDer;
|
||||||
|
if (/EC/i.test(opts.kty)) {
|
||||||
|
privDer = x509.packPkcs8(results.private);
|
||||||
|
pubDer = x509.packSpki(results.public);
|
||||||
|
Eckles.export({
|
||||||
|
jwk: results.private,
|
||||||
|
format: 'sec1'
|
||||||
|
}).then(function (pem) {
|
||||||
|
$('.js-input-pem-sec1-private').innerText = pem;
|
||||||
|
$('.js-toc-pem-sec1-private').hidden = false;
|
||||||
|
});
|
||||||
|
Eckles.export({
|
||||||
|
jwk: results.private,
|
||||||
|
format: 'pkcs8'
|
||||||
|
}).then(function (pem) {
|
||||||
|
$('.js-input-pem-pkcs8-private').innerText = pem;
|
||||||
|
$('.js-toc-pem-pkcs8-private').hidden = false;
|
||||||
|
});
|
||||||
|
Eckles.export({ jwk: results.public, public: true }).then(
|
||||||
|
function (pem) {
|
||||||
|
$('.js-input-pem-spki-public').innerText = pem;
|
||||||
|
$('.js-toc-pem-spki-public').hidden = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
privDer = x509.packPkcs8(results.private);
|
||||||
|
pubDer = x509.packSpki(results.public);
|
||||||
|
Rasha.export({
|
||||||
|
jwk: results.private,
|
||||||
|
format: 'pkcs1'
|
||||||
|
}).then(function (pem) {
|
||||||
|
$('.js-input-pem-pkcs1-private').innerText = pem;
|
||||||
|
$('.js-toc-pem-pkcs1-private').hidden = false;
|
||||||
|
});
|
||||||
|
Rasha.export({
|
||||||
|
jwk: results.private,
|
||||||
|
format: 'pkcs8'
|
||||||
|
}).then(function (pem) {
|
||||||
|
$('.js-input-pem-pkcs8-private').innerText = pem;
|
||||||
|
$('.js-toc-pem-pkcs8-private').hidden = false;
|
||||||
|
});
|
||||||
|
Rasha.export({ jwk: results.public, format: 'pkcs1' }).then(
|
||||||
|
function (pem) {
|
||||||
|
$('.js-input-pem-pkcs1-public').innerText = pem;
|
||||||
|
$('.js-toc-pem-pkcs1-public').hidden = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Rasha.export({ jwk: results.public, format: 'spki' }).then(
|
||||||
|
function (pem) {
|
||||||
|
$('.js-input-pem-spki-public').innerText = pem;
|
||||||
|
$('.js-toc-pem-spki-public').hidden = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.js-der-public').innerText = pubDer;
|
||||||
|
$('.js-toc-der-public').hidden = false;
|
||||||
|
$('.js-der-private').innerText = privDer;
|
||||||
|
$('.js-toc-der-private').hidden = false;
|
||||||
|
$('.js-jwk').innerText = JSON.stringify(results, null, 2);
|
||||||
|
$('.js-loading').hidden = true;
|
||||||
|
$('.js-jwk').hidden = false;
|
||||||
|
$$('input').map(function ($el) {
|
||||||
|
$el.disabled = false;
|
||||||
|
});
|
||||||
|
$$('button').map(function ($el) {
|
||||||
|
$el.disabled = false;
|
||||||
|
});
|
||||||
|
$('.js-toc-jwk').hidden = false;
|
||||||
|
|
||||||
|
$('.js-create-account').hidden = false;
|
||||||
|
$('.js-create-csr').hidden = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form.js-acme-account').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
$('.js-loading').hidden = false;
|
||||||
|
var acme = ACME.create({
|
||||||
|
Keypairs: Keypairs,
|
||||||
|
CSR: CSR
|
||||||
|
});
|
||||||
|
acme.init(
|
||||||
|
'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||||
|
).then(function (result) {
|
||||||
|
console.log('acme result', result);
|
||||||
|
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||||
|
var email = $('.js-email').value;
|
||||||
|
return acme.accounts
|
||||||
|
.create({
|
||||||
|
email: email,
|
||||||
|
agreeToTerms: checkTos,
|
||||||
|
accountKeypair: { privateKeyJwk: privJwk }
|
||||||
|
})
|
||||||
|
.then(function (account) {
|
||||||
|
console.log('account created result:', account);
|
||||||
|
accountStuff.account = account;
|
||||||
|
accountStuff.privateJwk = privJwk;
|
||||||
|
accountStuff.email = email;
|
||||||
|
accountStuff.acme = acme;
|
||||||
|
$('.js-create-order').hidden = false;
|
||||||
|
$('.js-toc-acme-account-response').hidden = false;
|
||||||
|
$(
|
||||||
|
'.js-acme-account-response'
|
||||||
|
).innerText = JSON.stringify(account, null, 2);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error('A bad thing happened:');
|
||||||
|
console.error(err);
|
||||||
|
window.alert(
|
||||||
|
err.message || JSON.stringify(err, null, 2)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form.js-csr').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
generateCsr();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form.js-acme-order').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
var account = accountStuff.account;
|
||||||
|
var privJwk = accountStuff.privateJwk;
|
||||||
|
var email = accountStuff.email;
|
||||||
|
var acme = accountStuff.acme;
|
||||||
|
|
||||||
|
var domains = ($('.js-domains').value || 'example.com').split(
|
||||||
|
/[, ]+/g
|
||||||
|
);
|
||||||
|
return getDomainPrivkey().then(function (domainPrivJwk) {
|
||||||
|
console.log('Has CSR already?');
|
||||||
|
console.log(accountStuff.csr);
|
||||||
|
return acme.certificates
|
||||||
|
.create({
|
||||||
|
accountKeypair: { privateKeyJwk: privJwk },
|
||||||
|
account: account,
|
||||||
|
serverKeypair: { privateKeyJwk: domainPrivJwk },
|
||||||
|
csr: accountStuff.csr,
|
||||||
|
domains: domains,
|
||||||
|
skipDryRun:
|
||||||
|
$('input[name="skip-dryrun"]:checked') && true,
|
||||||
|
agreeToTerms: checkTos,
|
||||||
|
challenges: {
|
||||||
|
'dns-01': {
|
||||||
|
set: function (opts) {
|
||||||
|
console.info('dns-01 set challenge:');
|
||||||
|
console.info('TXT', opts.dnsHost);
|
||||||
|
console.info(opts.dnsAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (
|
||||||
|
!window.confirm(
|
||||||
|
'Did you set the challenge?'
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove: function (opts) {
|
||||||
|
console.log('dns-01 remove challenge:');
|
||||||
|
console.info('TXT', opts.dnsHost);
|
||||||
|
console.info(opts.dnsAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (
|
||||||
|
!window.confirm(
|
||||||
|
'Did you delete the challenge?'
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'http-01': {
|
||||||
|
set: function (opts) {
|
||||||
|
console.info('http-01 set challenge:');
|
||||||
|
console.info(opts.challengeUrl);
|
||||||
|
console.info(opts.keyAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (
|
||||||
|
!window.confirm(
|
||||||
|
'Did you set the challenge?'
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove: function (opts) {
|
||||||
|
console.log('http-01 remove challenge:');
|
||||||
|
console.info(opts.challengeUrl);
|
||||||
|
console.info(opts.keyAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (
|
||||||
|
!window.confirm(
|
||||||
|
'Did you delete the challenge?'
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
challengeTypes: [
|
||||||
|
$('input[name="acme-challenge-type"]:checked').value
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(function (results) {
|
||||||
|
console.log('Got Certificates:');
|
||||||
|
console.log(results);
|
||||||
|
$('.js-toc-acme-order-response').hidden = false;
|
||||||
|
$('.js-acme-order-response').innerText = JSON.stringify(
|
||||||
|
results,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error('challenge failed:');
|
||||||
|
console.error(err);
|
||||||
|
window.alert(
|
||||||
|
'failed! ' + err.message || JSON.stringify(err)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.js-generate').hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDomainPrivkey() {
|
||||||
|
if (accountStuff.domainPrivateJwk) {
|
||||||
|
return Promise.resolve(accountStuff.domainPrivateJwk);
|
||||||
|
}
|
||||||
|
return Keypairs.generate({
|
||||||
|
kty: $('input[name="kty"]:checked').value,
|
||||||
|
namedCurve: $('input[name="ec-crv"]:checked').value,
|
||||||
|
modulusLength: $('input[name="rsa-len"]:checked').value
|
||||||
|
}).then(function (pair) {
|
||||||
|
console.log('domain keypair:', pair);
|
||||||
|
accountStuff.domainPrivateJwk = pair.private;
|
||||||
|
return pair.private;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCsr() {
|
||||||
|
var domains = ($('.js-domains').value || 'example.com').split(/[, ]+/g);
|
||||||
|
//var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||||
|
return getDomainPrivkey().then(function (privJwk) {
|
||||||
|
accountStuff.domainPrivateJwk = privJwk;
|
||||||
|
return CSR({ jwk: privJwk, domains: domains }).then(function (pem) {
|
||||||
|
// Verify with https://www.sslshopper.com/csr-decoder.html
|
||||||
|
accountStuff.csr = pem;
|
||||||
|
console.log('Created CSR:');
|
||||||
|
console.log(pem);
|
||||||
|
|
||||||
|
console.log('CSR info:');
|
||||||
|
console.log(CSR._info(pem));
|
||||||
|
|
||||||
|
return pem;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', run);
|
||||||
|
})();
|
|
@ -0,0 +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=acme-dns-01-digitalocean
|
||||||
|
CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}'
|
|
@ -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));
|
||||||
|
}
|
|
@ -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());
|
||||||
|
});
|
|
@ -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());
|
||||||
|
});
|
|
@ -0,0 +1,228 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>ACME.js - A Root Project</title>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<style>
|
||||||
|
textarea {
|
||||||
|
width: 42em;
|
||||||
|
height: 10em;
|
||||||
|
}
|
||||||
|
/* need to word wrap the binary no space der */
|
||||||
|
.js-der-public,
|
||||||
|
.js-der-private {
|
||||||
|
white-space: pre-wrap; /* CSS3 */
|
||||||
|
white-space: -moz-pre-wrap; /* Firefox */
|
||||||
|
white-space: -pre-wrap; /* Opera <7 */
|
||||||
|
white-space: -o-pre-wrap; /* Opera 7 */
|
||||||
|
word-wrap: break-word; /* IE */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>
|
||||||
|
@root/acme: Let's Encrypt for the Browser
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is intended to be explored with your JavaScript console open.
|
||||||
|
</p>
|
||||||
|
<pre><code><script src="<a href="https://unpkg.com/@root/acme@3.0.0/dist/acme.js">https://unpkg.com/@root/acme@3.0.0/dist/acme.js</a>"></script></code></pre>
|
||||||
|
<pre><code><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>"></script></code></pre>
|
||||||
|
<a href="https://git.rootprojects.org/root/acme.js">Documentation</a>
|
||||||
|
|
||||||
|
<h2>1. Keypair Generation</h2>
|
||||||
|
<form class="js-keygen">
|
||||||
|
<p>Key Type:</p>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="-ktyEC" name="kty" value="EC" checked />
|
||||||
|
<label for="-ktyEC">ECDSA</label>
|
||||||
|
<input type="radio" id="-ktyRSA" name="kty" value="RSA" />
|
||||||
|
<label for="-ktyRSA">RSA</label>
|
||||||
|
</div>
|
||||||
|
<div class="js-ec-opts">
|
||||||
|
<p>EC Options:</p>
|
||||||
|
<label for="-crv2"
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
id="-crv2"
|
||||||
|
name="ec-crv"
|
||||||
|
value="P-256"
|
||||||
|
checked
|
||||||
|
/>P-256</label
|
||||||
|
>
|
||||||
|
<label for="-crv3"
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
id="-crv3"
|
||||||
|
name="ec-crv"
|
||||||
|
value="P-384"
|
||||||
|
/>P-384</label
|
||||||
|
>
|
||||||
|
<!-- label for="-crv5"><input type="radio" id="-crv5"
|
||||||
|
name="ec-crv" value="P-521">P-521</label -->
|
||||||
|
</div>
|
||||||
|
<div class="js-rsa-opts" hidden>
|
||||||
|
<p>RSA Options:</p>
|
||||||
|
<label for="-modlen2"
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
id="-modlen2"
|
||||||
|
name="rsa-len"
|
||||||
|
value="2048"
|
||||||
|
checked
|
||||||
|
/>2048</label
|
||||||
|
>
|
||||||
|
<label for="-modlen3"
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
id="-modlen3"
|
||||||
|
name="rsa-len"
|
||||||
|
value="3072"
|
||||||
|
/>3072</label
|
||||||
|
>
|
||||||
|
<label for="-modlen5"
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
id="-modlen5"
|
||||||
|
name="rsa-len"
|
||||||
|
value="4096"
|
||||||
|
/>4096</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button class="js-generate" hidden>Generate</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>2. ACME Account</h2>
|
||||||
|
<form class="js-acme-account">
|
||||||
|
<label for="-acmeEmail">Email:</label>
|
||||||
|
<input
|
||||||
|
class="js-email"
|
||||||
|
type="email"
|
||||||
|
id="-acmeEmail"
|
||||||
|
value="john.doe@gmail.com"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<label for="-acmeTos"
|
||||||
|
><input
|
||||||
|
class="js-tos"
|
||||||
|
name="tos"
|
||||||
|
type="checkbox"
|
||||||
|
id="-acmeTos"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
Agree to Let's Encrypt Terms of Service</label
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<button class="js-create-account" hidden>Create Account</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>3. (optional) Certificate Signing Request</h2>
|
||||||
|
<form class="js-csr">
|
||||||
|
<label for="-acmeDomains">Domains:</label>
|
||||||
|
<input
|
||||||
|
class="js-domains"
|
||||||
|
type="text"
|
||||||
|
id="-acmeDomains"
|
||||||
|
value="example.com www.example.com"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button class="js-create-csr" hidden>Create CSR</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>4. ACME Certificate Order</h2>
|
||||||
|
<form class="js-acme-order">
|
||||||
|
Challenge type:
|
||||||
|
<label for="-http01"
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
id="-http01"
|
||||||
|
name="acme-challenge-type"
|
||||||
|
value="http-01"
|
||||||
|
checked
|
||||||
|
/>http-01</label
|
||||||
|
>
|
||||||
|
<label for="-dns01"
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
id="-dns01"
|
||||||
|
name="acme-challenge-type"
|
||||||
|
value="dns-01"
|
||||||
|
/>dns-01</label
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<label for="-skipDryrun"
|
||||||
|
><input
|
||||||
|
class="js-skip-dryrun"
|
||||||
|
name="skip-dryrun"
|
||||||
|
type="checkbox"
|
||||||
|
id="-skipDryrun"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
Skip dry-run challenge</label
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<button class="js-create-order" hidden>Create Order</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="js-loading" hidden>Loading</div>
|
||||||
|
|
||||||
|
<details class="js-toc-jwk" hidden>
|
||||||
|
<summary>JWK Keypair</summary>
|
||||||
|
<pre><code class="js-jwk"> </code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-der-private" hidden>
|
||||||
|
<summary>DER Private Binary</summary>
|
||||||
|
<pre><code class="js-der-private"> </code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-der-public" hidden>
|
||||||
|
<summary>DER Public Binary</summary>
|
||||||
|
<pre><code class="js-der-public"> </code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-pem js-toc-pem-pkcs1-private" hidden>
|
||||||
|
<summary>PEM Private (base64-encoded PKCS1 DER)</summary>
|
||||||
|
<pre><code class="js-input-pem-pkcs1-private" ></code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-pem js-toc-pem-sec1-private" hidden>
|
||||||
|
<summary>PEM Private (base64-encoded SEC1 DER)</summary>
|
||||||
|
<pre><code class="js-input-pem-sec1-private" ></code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-pem js-toc-pem-pkcs8-private" hidden>
|
||||||
|
<summary>PEM Private (base64-encoded PKCS8 DER)</summary>
|
||||||
|
<pre><code class="js-input-pem-pkcs8-private" ></code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-pem js-toc-pem-pkcs1-public" hidden>
|
||||||
|
<summary>PEM Public (base64-encoded PKCS1 DER)</summary>
|
||||||
|
<pre><code class="js-input-pem-pkcs1-public" ></code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-pem js-toc-pem-spki-public" hidden>
|
||||||
|
<summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary>
|
||||||
|
<pre><code class="js-input-pem-spki-public" ></code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-acme-account-response" hidden>
|
||||||
|
<summary>ACME Account Request</summary>
|
||||||
|
<pre><code class="js-acme-account-response"> </code></pre>
|
||||||
|
</details>
|
||||||
|
<details class="js-toc-acme-order-response" hidden>
|
||||||
|
<summary>ACME Order Response</summary>
|
||||||
|
<pre><code class="js-acme-order-response"> </code></pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
[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/acme.js">@root/acme</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- script src="../dist/acme.js"></script -->
|
||||||
|
<!-- script src="../dist/app.js"></script -->
|
||||||
|
<script src="./app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,174 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var crypto = require('crypto');
|
||||||
|
//var dnsjs = require('dns-suite');
|
||||||
|
var dig = require('dig.js/dns-request');
|
||||||
|
var request = require('util').promisify(require('@root/request'));
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
var nameservers = require('dns').getServers();
|
||||||
|
var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length;
|
||||||
|
var nameserver = nameservers[index];
|
||||||
|
|
||||||
|
app.use('/', express.static(__dirname));
|
||||||
|
app.use('/api', express.json());
|
||||||
|
app.get('/api/dns/:domain', function (req, res, next) {
|
||||||
|
var domain = req.params.domain;
|
||||||
|
var casedDomain = domain
|
||||||
|
.toLowerCase()
|
||||||
|
.split('')
|
||||||
|
.map(function (ch) {
|
||||||
|
// dns0x20 takes advantage of the fact that the binary operation for toUpperCase is
|
||||||
|
// ch = ch | 0x20;
|
||||||
|
return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase();
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
var typ = req.query.type;
|
||||||
|
var query = {
|
||||||
|
header: {
|
||||||
|
id: crypto.randomBytes(2).readUInt16BE(0),
|
||||||
|
qr: 0,
|
||||||
|
opcode: 0,
|
||||||
|
aa: 0, // Authoritative-Only
|
||||||
|
tc: 0, // NA
|
||||||
|
rd: 1, // Recurse
|
||||||
|
ra: 0, // NA
|
||||||
|
rcode: 0 // NA
|
||||||
|
},
|
||||||
|
question: [
|
||||||
|
{
|
||||||
|
name: casedDomain,
|
||||||
|
//, type: typ || 'A'
|
||||||
|
typeName: typ || 'A',
|
||||||
|
className: 'IN'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var opts = {
|
||||||
|
onError: function (err) {
|
||||||
|
next(err);
|
||||||
|
},
|
||||||
|
onMessage: function (packet) {
|
||||||
|
var fail0x20;
|
||||||
|
|
||||||
|
if (packet.id !== query.id) {
|
||||||
|
console.error(
|
||||||
|
"[SECURITY] ignoring packet for '" +
|
||||||
|
packet.question[0].name +
|
||||||
|
"' due to mismatched id"
|
||||||
|
);
|
||||||
|
console.error(packet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.question.forEach(function (q) {
|
||||||
|
// if (-1 === q.name.lastIndexOf(cli.casedQuery))
|
||||||
|
if (q.name !== casedDomain) {
|
||||||
|
fail0x20 = q.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
['question', 'answer', 'authority', 'additional'].forEach(function (
|
||||||
|
group
|
||||||
|
) {
|
||||||
|
(packet[group] || []).forEach(function (a) {
|
||||||
|
var an = a.name;
|
||||||
|
var i = domain
|
||||||
|
.toLowerCase()
|
||||||
|
.lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM
|
||||||
|
var j = a.name
|
||||||
|
.toLowerCase()
|
||||||
|
.lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM
|
||||||
|
|
||||||
|
// it's important to note that these should only relpace changes in casing that we expected
|
||||||
|
// any abnormalities should be left intact to go "huh?" about
|
||||||
|
// TODO detect abnormalities?
|
||||||
|
if (-1 !== i) {
|
||||||
|
// "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4))
|
||||||
|
a.name = a.name.replace(
|
||||||
|
casedDomain.substr(i),
|
||||||
|
domain.substr(i)
|
||||||
|
);
|
||||||
|
} else if (-1 !== j) {
|
||||||
|
// "www.example.com".replace("EXamPLE.cOm", "example.com")
|
||||||
|
a.name =
|
||||||
|
a.name.substr(0, j) +
|
||||||
|
a.name.substr(j).replace(casedDomain, domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: right now this assumes that anything matching the query matches all the way to the end
|
||||||
|
// it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly
|
||||||
|
// (but I don't think it should need to)
|
||||||
|
if (a.name.length !== an.length) {
|
||||||
|
console.error(
|
||||||
|
"[ERROR] question / answer mismatch: '" +
|
||||||
|
an +
|
||||||
|
"' != '" +
|
||||||
|
a.length +
|
||||||
|
"'"
|
||||||
|
);
|
||||||
|
console.error(a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fail0x20) {
|
||||||
|
console.warn(
|
||||||
|
";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" +
|
||||||
|
casedDomain +
|
||||||
|
"' but got response for '" +
|
||||||
|
fail0x20 +
|
||||||
|
"'."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
header: packet.header,
|
||||||
|
question: packet.question,
|
||||||
|
answer: packet.answer,
|
||||||
|
authority: packet.authority,
|
||||||
|
additional: packet.additional,
|
||||||
|
edns_options: packet.edns_options
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onListening: function () {},
|
||||||
|
onSent: function (/*res*/) {},
|
||||||
|
onTimeout: function (res) {
|
||||||
|
console.error('dns timeout:', res);
|
||||||
|
next(new Error('DNS timeout - no response'));
|
||||||
|
},
|
||||||
|
onClose: function () {},
|
||||||
|
//, mdns: cli.mdns
|
||||||
|
nameserver: nameserver,
|
||||||
|
port: 53,
|
||||||
|
timeout: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
dig.resolveJson(query, opts);
|
||||||
|
});
|
||||||
|
app.get('/api/http', function (req, res) {
|
||||||
|
var url = req.query.url;
|
||||||
|
return request({ method: 'GET', url: url }).then(function (resp) {
|
||||||
|
res.send(resp.body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.get('/api/_acme_api_', function (req, res) {
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
|
if (require.main === module) {
|
||||||
|
// curl -L http://localhost:3000/api/dns/example.com?type=A
|
||||||
|
console.info('Listening on localhost:3000');
|
||||||
|
app.listen(3000);
|
||||||
|
console.info('Try this:');
|
||||||
|
console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'");
|
||||||
|
console.info(
|
||||||
|
"\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"
|
||||||
|
);
|
||||||
|
console.info(
|
||||||
|
"\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'"
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"private": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"d": "HB1OvdHfLnIy2mYYO9cLU4BqP36CeyS8OsDf3OnYP-M",
|
||||||
|
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
|
||||||
|
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
|
||||||
|
"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs"
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
|
||||||
|
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
|
||||||
|
"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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"
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "9JZE7ZMAAQ-26oP-_pzd9gy2CbuEvgvrB42R1rP2Pb0",
|
||||||
|
"y": "8yvSYK5sAx30upYpqVknnPPQlK1T3zGTLbJRC-DH_qw"
|
||||||
|
},
|
||||||
|
"contact": ["mailto:letsencrypt+staging@therootcompany.com"],
|
||||||
|
"initialIp": "66.219.236.169",
|
||||||
|
"createdAt": "2019-10-24T23:19:57.480171297Z",
|
||||||
|
"status": "valid"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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-----
|
|
@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"type": "dns-01",
|
||||||
|
"status": "pending",
|
||||||
|
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/16603341/yCthUw",
|
||||||
|
"token": "DiO9DFHuFTpNsJxIbOxfVCSPVkpe4lJUjozeSyzkMjI"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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" }
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
// there is no nonce response body, see the headers
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"private": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw",
|
||||||
|
"e": "AQAB",
|
||||||
|
"d": "YCzN9yVr4Jw5D_UK7WEMuzGUcMAZZs-TQFgY4UK7Ovbj18_QQrhKElb6Zfhepcf1HUYkO6PVjpuZ1tEl9hWgVcFa781AROyvSj04beiaVMDeSCCwjgW3MM3w6olnxTOUDaBMl9NNiqq0v9riDImkQbAQbe3To-KAH2ig4AMNlSZJAhmI2zAMiJhQE_pAcCxc-bQ5oNO-WSU0GRHWdMJSXp9mFgoBhVPDYGW-dmnoFzuNWssxlSqGXY-8a2YOuiunK6XM5_80c1eQqmy-k1InUIViR_wljskc8UiH6xa8BCznZYacgSz4PnvKsiKWKQQ1eliIucV3MC6BzMD3N8EWqQ",
|
||||||
|
"p": "8NUtOIglu0dvDGmEB7QC5eC02Y2jZKnoxHSPKMAEPxQ0131_2aL49IzADWoTvae3NBPzU7ol3RwJo_GvS967OysfOr6Od699p1FSLwLfK89aql7_uVPJh4Q43H-W_NtRHKUkv0OmkDiwa4WqBQTVfREdPQ3NJT7vIY-cqH_AMRc",
|
||||||
|
"q": "xZNIl9NRl3b0_V8Y-7_6_foIu9Sx5ILv2XV7WONDx2jp4vuT7byLm1UWdYPBbxLyd5TAvWqtyvaRtVNyplrD0PyyPK3NxqVJde0uzScAU-bf25DeK30V22Xo7IEZiPZoizrjtzGnS6VVNJmZ-Ictz3xmWIudw5d5XDH12fFRlmU",
|
||||||
|
"dp": "F1Ld9UqiNNf_NjmF0uUpHrA7c5JXD6mw5E3Ri4XFI4LGd1QtLJuu9qgm9WWfkc-LW5zPBP3TKu3LNThz3KougdV0SdEopQi255xllC34BRso0bUvmPg3XUt94kTtD4ICAf8wZuGbYP5Mf61LQP8t2dXtefs7Me89Y4ewCVWN_HM",
|
||||||
|
"dq": "oPuT35lgVtCnZ7dPrPjNMpnC-gCg_fcuJPqTiWaLuHQkdjzUWJYTDnqy9Qdo2e8PPx4mOXAtsT1clekrdp5oBOWQ-N4I172fcIXUZ3ZKzxJD_iw4yih-YajUs7exLabQoflWx9KeZIWPOm-ZRCYoznGnFqiT4GWQje1rS6xT9P0",
|
||||||
|
"qi": "aXkK-w4Npw0BpUEzQ1PURVGm5y5cKIdd-CfEYwub19rronI9EEvuQHoqR7ODtZ_mlIIffHmHaM3ug50fJDB9QDOG4Ioc5S4YxVURT58Ps8at-dQAAP1UgSlV3vhXh4WZRaDECUI_728U3fxQqH78bJsy81mU8MtGU8LR_eTMXx8",
|
||||||
|
"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk"
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw",
|
||||||
|
"e": "AQAB",
|
||||||
|
"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk"
|
||||||
|
}
|
||||||
|
}
|
153
index.html
153
index.html
|
@ -1,153 +0,0 @@
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Bluecrypt ACME - A Root Project</title>
|
|
||||||
<style>
|
|
||||||
textarea {
|
|
||||||
width: 42em;
|
|
||||||
height: 10em;
|
|
||||||
}
|
|
||||||
/* need to word wrap the binary no space der */
|
|
||||||
.js-der-public, .js-der-private{
|
|
||||||
white-space: pre-wrap; /* CSS3 */
|
|
||||||
white-space: -moz-pre-wrap; /* Firefox */
|
|
||||||
white-space: -pre-wrap; /* Opera <7 */
|
|
||||||
white-space: -o-pre-wrap; /* Opera 7 */
|
|
||||||
word-wrap: break-word; /* IE */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>@bluecrypt/acme: Let's Encrypt for the Browser</h1>
|
|
||||||
|
|
||||||
<p>This is intended to be explored with your JavaScript console open.</p>
|
|
||||||
<pre><code><script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.js">https://rootprojects.org/acme/bluecrypt-acme.js</a>"></script></code></pre>
|
|
||||||
<pre><code><script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.min.js">https://rootprojects.org/acme/bluecrypt-acme.min.js</a>"></script></code></pre>
|
|
||||||
<a href="https://git.rootprojects.org/root/bluecrypt-acme.js">Documentation</a>
|
|
||||||
|
|
||||||
<h2>1. Keypair Generation</h2>
|
|
||||||
<form class="js-keygen">
|
|
||||||
<p>Key Type:</p>
|
|
||||||
<div>
|
|
||||||
<input type="radio" id="-ktyEC"
|
|
||||||
name="kty" value="EC" checked>
|
|
||||||
<label for="-ktyEC">ECDSA</label>
|
|
||||||
<input type="radio" id="-ktyRSA"
|
|
||||||
name="kty" value="RSA">
|
|
||||||
<label for="-ktyRSA">RSA</label>
|
|
||||||
</div>
|
|
||||||
<div class="js-ec-opts">
|
|
||||||
<p>EC Options:</p>
|
|
||||||
<label for="-crv2"><input type="radio" id="-crv2"
|
|
||||||
name="ec-crv" value="P-256" checked>P-256</label>
|
|
||||||
<label for="-crv3"><input type="radio" id="-crv3"
|
|
||||||
name="ec-crv" value="P-384">P-384</label>
|
|
||||||
<!-- label for="-crv5"><input type="radio" id="-crv5"
|
|
||||||
name="ec-crv" value="P-521">P-521</label -->
|
|
||||||
</div>
|
|
||||||
<div class="js-rsa-opts" hidden>
|
|
||||||
<p>RSA Options:</p>
|
|
||||||
<label for="-modlen2"><input type="radio" id="-modlen2"
|
|
||||||
name="rsa-len" value="2048" checked>2048</label>
|
|
||||||
<label for="-modlen3"><input type="radio" id="-modlen3"
|
|
||||||
name="rsa-len" value="3072">3072</label>
|
|
||||||
<label for="-modlen5"><input type="radio" id="-modlen5"
|
|
||||||
name="rsa-len" value="4096">4096</label>
|
|
||||||
</div>
|
|
||||||
<button class="js-generate" hidden>Generate</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>2. ACME Account</h2>
|
|
||||||
<form class="js-acme-account">
|
|
||||||
<label for="-acmeEmail">Email:</label>
|
|
||||||
<input class="js-email" type="email" id="-acmeEmail" value="john.doe@gmail.com">
|
|
||||||
<br>
|
|
||||||
<label for="-acmeTos"><input class="js-tos" name="tos" type="checkbox" id="-acmeTos" checked>
|
|
||||||
Agree to Let's Encrypt Terms of Service</label>
|
|
||||||
<br>
|
|
||||||
<button class="js-create-account" hidden>Create Account</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>3. (optional) Certificate Signing Request</h2>
|
|
||||||
<form class="js-csr">
|
|
||||||
<label for="-acmeDomains">Domains:</label>
|
|
||||||
<input class="js-domains" type="text" id="-acmeDomains" value="example.com www.example.com">
|
|
||||||
<br>
|
|
||||||
<button class="js-create-csr" hidden>Create CSR</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>4. ACME Certificate Order</h2>
|
|
||||||
<form class="js-acme-order">
|
|
||||||
Challenge type:
|
|
||||||
<label for="-http01"><input type="radio" id="-http01"
|
|
||||||
name="acme-challenge-type" value="http-01" checked>http-01</label>
|
|
||||||
<label for="-dns01"><input type="radio" id="-dns01"
|
|
||||||
name="acme-challenge-type" value="dns-01">dns-01</label>
|
|
||||||
<br>
|
|
||||||
<label for="-skipDryrun"><input class="js-skip-dryrun" name="skip-dryrun"
|
|
||||||
type="checkbox" id="-skipDryrun" checked> Skip dry-run challenge</label>
|
|
||||||
<br>
|
|
||||||
<button class="js-create-order" hidden>Create Order</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="js-loading" hidden>Loading</div>
|
|
||||||
|
|
||||||
<details class="js-toc-jwk" hidden>
|
|
||||||
<summary>JWK Keypair</summary>
|
|
||||||
<pre><code class="js-jwk"> </code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-der-private" hidden>
|
|
||||||
<summary>DER Private Binary</summary>
|
|
||||||
<pre><code class="js-der-private"> </code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-der-public" hidden>
|
|
||||||
<summary>DER Public Binary</summary>
|
|
||||||
<pre><code class="js-der-public"> </code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-pem js-toc-pem-pkcs1-private" hidden>
|
|
||||||
<summary>PEM Private (base64-encoded PKCS1 DER)</summary>
|
|
||||||
<pre><code class="js-input-pem-pkcs1-private" ></code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-pem js-toc-pem-sec1-private" hidden>
|
|
||||||
<summary>PEM Private (base64-encoded SEC1 DER)</summary>
|
|
||||||
<pre><code class="js-input-pem-sec1-private" ></code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-pem js-toc-pem-pkcs8-private" hidden>
|
|
||||||
<summary>PEM Private (base64-encoded PKCS8 DER)</summary>
|
|
||||||
<pre><code class="js-input-pem-pkcs8-private" ></code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-pem js-toc-pem-pkcs1-public" hidden>
|
|
||||||
<summary>PEM Public (base64-encoded PKCS1 DER)</summary>
|
|
||||||
<pre><code class="js-input-pem-pkcs1-public" ></code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-pem js-toc-pem-spki-public" hidden>
|
|
||||||
<summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary>
|
|
||||||
<pre><code class="js-input-pem-spki-public" ></code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-acme-account-response" hidden>
|
|
||||||
<summary>ACME Account Request</summary>
|
|
||||||
<pre><code class="js-acme-account-response"> </code></pre>
|
|
||||||
</details>
|
|
||||||
<details class="js-toc-acme-order-response" hidden>
|
|
||||||
<summary>ACME Order Response</summary>
|
|
||||||
<pre><code class="js-acme-order-response"> </code></pre>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<p>Bluecrypt™ is a collection of lightweight, zero-dependency, libraries written in VanillaJS.
|
|
||||||
They are fast, tiny, and secure, using the native features of modern browsers where possible.
|
|
||||||
This means it's easy-to-use crypto in kilobytes, not megabytes.</p>
|
|
||||||
<br>
|
|
||||||
<footer>View (git) source <a href="https://git.rootprojects.org/root/bluecrypt-acme.js">@bluecrypt/acme</a></footer>
|
|
||||||
|
|
||||||
<script src="./lib/encoding.js"></script>
|
|
||||||
<script src="./lib/asn1-packer.js"></script>
|
|
||||||
<script src="./lib/x509.js"></script>
|
|
||||||
<script src="./lib/ecdsa.js"></script>
|
|
||||||
<script src="./lib/rsa.js"></script>
|
|
||||||
<script src="./lib/keypairs.js"></script>
|
|
||||||
<script src="./lib/asn1-parser.js"></script>
|
|
||||||
<script src="./lib/csr.js"></script>
|
|
||||||
<script src="./lib/acme.js"></script>
|
|
||||||
<script src="./app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
1517
lib/acme.js
1517
lib/acme.js
File diff suppressed because it is too large
Load Diff
|
@ -1,147 +0,0 @@
|
||||||
(function(exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (!exports.ASN1) {
|
|
||||||
exports.ASN1 = {};
|
|
||||||
}
|
|
||||||
if (!exports.Enc) {
|
|
||||||
exports.Enc = {};
|
|
||||||
}
|
|
||||||
if (!exports.PEM) {
|
|
||||||
exports.PEM = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
var ASN1 = exports.ASN1;
|
|
||||||
var Enc = exports.Enc;
|
|
||||||
var PEM = exports.PEM;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Packer
|
|
||||||
//
|
|
||||||
|
|
||||||
// Almost every ASN.1 type that's important for CSR
|
|
||||||
// can be represented generically with only a few rules.
|
|
||||||
exports.ASN1 = function ASN1(/*type, hexstrings...*/) {
|
|
||||||
var args = Array.prototype.slice.call(arguments);
|
|
||||||
var typ = args.shift();
|
|
||||||
var str = args
|
|
||||||
.join('')
|
|
||||||
.replace(/\s+/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
var len = str.length / 2;
|
|
||||||
var lenlen = 0;
|
|
||||||
var hex = typ;
|
|
||||||
|
|
||||||
// We can't have an odd number of hex chars
|
|
||||||
if (len !== Math.round(len)) {
|
|
||||||
throw new Error('invalid hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
|
|
||||||
// The second byte is either the size of the value, or the size of its size
|
|
||||||
|
|
||||||
// 1. If the second byte is < 0x80 (128) it is considered the size
|
|
||||||
// 2. If it is > 0x80 then it describes the number of bytes of the size
|
|
||||||
// ex: 0x82 means the next 2 bytes describe the size of the value
|
|
||||||
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
|
|
||||||
|
|
||||||
if (len > 127) {
|
|
||||||
lenlen += 1;
|
|
||||||
while (len > 255) {
|
|
||||||
lenlen += 1;
|
|
||||||
len = len >> 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lenlen) {
|
|
||||||
hex += Enc.numToHex(0x80 + lenlen);
|
|
||||||
}
|
|
||||||
return hex + Enc.numToHex(str.length / 2) + str;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Integer type has some special rules
|
|
||||||
ASN1.UInt = function UINT() {
|
|
||||||
var str = Array.prototype.slice.call(arguments).join('');
|
|
||||||
var first = parseInt(str.slice(0, 2), 16);
|
|
||||||
|
|
||||||
// If the first byte is 0x80 or greater, the number is considered negative
|
|
||||||
// Therefore we add a '00' prefix if the 0x80 bit is set
|
|
||||||
if (0x80 & first) {
|
|
||||||
str = '00' + str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ASN1('02', str);
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Bit String type also has a special rule
|
|
||||||
ASN1.BitStr = function BITSTR() {
|
|
||||||
var str = Array.prototype.slice.call(arguments).join('');
|
|
||||||
// '00' is a mask of how many bits of the next byte to ignore
|
|
||||||
return ASN1('03', '00' + str);
|
|
||||||
};
|
|
||||||
|
|
||||||
ASN1.pack = function(arr) {
|
|
||||||
var typ = Enc.numToHex(arr[0]);
|
|
||||||
var str = '';
|
|
||||||
if (Array.isArray(arr[1])) {
|
|
||||||
arr[1].forEach(function(a) {
|
|
||||||
str += ASN1.pack(a);
|
|
||||||
});
|
|
||||||
} else if ('string' === typeof arr[1]) {
|
|
||||||
str = arr[1];
|
|
||||||
} else {
|
|
||||||
throw new Error('unexpected array');
|
|
||||||
}
|
|
||||||
if ('03' === typ) {
|
|
||||||
return ASN1.BitStr(str);
|
|
||||||
} else if ('02' === typ) {
|
|
||||||
return ASN1.UInt(str);
|
|
||||||
} else {
|
|
||||||
return ASN1(typ, str);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Object.keys(ASN1).forEach(function(k) {
|
|
||||||
exports.ASN1[k] = ASN1[k];
|
|
||||||
});
|
|
||||||
ASN1 = exports.ASN1;
|
|
||||||
|
|
||||||
PEM.packBlock = function(opts) {
|
|
||||||
// TODO allow for headers?
|
|
||||||
return (
|
|
||||||
'-----BEGIN ' +
|
|
||||||
opts.type +
|
|
||||||
'-----\n' +
|
|
||||||
Enc.bufToBase64(opts.bytes)
|
|
||||||
.match(/.{1,64}/g)
|
|
||||||
.join('\n') +
|
|
||||||
'\n' +
|
|
||||||
'-----END ' +
|
|
||||||
opts.type +
|
|
||||||
'-----'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToBase64 = function(u8) {
|
|
||||||
var bin = '';
|
|
||||||
u8.forEach(function(i) {
|
|
||||||
bin += String.fromCharCode(i);
|
|
||||||
});
|
|
||||||
return btoa(bin);
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.hexToBuf = function(hex) {
|
|
||||||
var arr = [];
|
|
||||||
hex.match(/.{2}/g).forEach(function(h) {
|
|
||||||
arr.push(parseInt(h, 16));
|
|
||||||
});
|
|
||||||
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.numToHex = function(d) {
|
|
||||||
d = d.toString(16);
|
|
||||||
if (d.length % 2) {
|
|
||||||
return '0' + d;
|
|
||||||
}
|
|
||||||
return d;
|
|
||||||
};
|
|
||||||
})('undefined' !== typeof window ? window : module.exports);
|
|
|
@ -1,222 +0,0 @@
|
||||||
// Copyright 2018 AJ ONeal. All rights reserved
|
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
(function(exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (!exports.ASN1) {
|
|
||||||
exports.ASN1 = {};
|
|
||||||
}
|
|
||||||
if (!exports.Enc) {
|
|
||||||
exports.Enc = {};
|
|
||||||
}
|
|
||||||
if (!exports.PEM) {
|
|
||||||
exports.PEM = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
var ASN1 = exports.ASN1;
|
|
||||||
var Enc = exports.Enc;
|
|
||||||
var PEM = exports.PEM;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Parser
|
|
||||||
//
|
|
||||||
|
|
||||||
// Although I've only seen 9 max in https certificates themselves,
|
|
||||||
// but each domain list could have up to 100
|
|
||||||
ASN1.ELOOPN = 102;
|
|
||||||
ASN1.ELOOP =
|
|
||||||
'uASN1.js Error: iterated over ' +
|
|
||||||
ASN1.ELOOPN +
|
|
||||||
'+ elements (probably a malformed file)';
|
|
||||||
// I've seen https certificates go 29 deep
|
|
||||||
ASN1.EDEEPN = 60;
|
|
||||||
ASN1.EDEEP =
|
|
||||||
'uASN1.js Error: element nested ' +
|
|
||||||
ASN1.EDEEPN +
|
|
||||||
'+ layers deep (probably a malformed file)';
|
|
||||||
// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
|
|
||||||
// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
|
|
||||||
// Bit String (0x03) and Octet String (0x04) may be values or containers
|
|
||||||
// Sometimes Bit String is used as a container (RSA Pub Spki)
|
|
||||||
ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
|
|
||||||
ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
|
|
||||||
ASN1.parse = function parseAsn1Helper(buf) {
|
|
||||||
//var ws = ' ';
|
|
||||||
function parseAsn1(buf, depth, eager) {
|
|
||||||
if (depth.length >= ASN1.EDEEPN) {
|
|
||||||
throw new Error(ASN1.EDEEP);
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
|
|
||||||
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
|
|
||||||
var child;
|
|
||||||
var iters = 0;
|
|
||||||
var adjust = 0;
|
|
||||||
var adjustedLen;
|
|
||||||
|
|
||||||
// Determine how many bytes the length uses, and what it is
|
|
||||||
if (0x80 & asn1.length) {
|
|
||||||
asn1.lengthSize = 0x7f & asn1.length;
|
|
||||||
// I think that buf->hex->int solves the problem of Endianness... not sure
|
|
||||||
asn1.length = parseInt(
|
|
||||||
Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
|
|
||||||
16
|
|
||||||
);
|
|
||||||
index += asn1.lengthSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-order bit Integers have a leading 0x00 to signify that they are positive.
|
|
||||||
// Bit Streams use the first byte to signify padding, which x.509 doesn't use.
|
|
||||||
if (
|
|
||||||
0x00 === buf[index] &&
|
|
||||||
(0x02 === asn1.type || 0x03 === asn1.type)
|
|
||||||
) {
|
|
||||||
// However, 0x00 on its own is a valid number
|
|
||||||
if (asn1.length > 1) {
|
|
||||||
index += 1;
|
|
||||||
adjust = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adjustedLen = asn1.length + adjust;
|
|
||||||
|
|
||||||
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
|
||||||
|
|
||||||
function parseChildren(eager) {
|
|
||||||
asn1.children = [];
|
|
||||||
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
|
|
||||||
while (
|
|
||||||
iters < ASN1.ELOOPN &&
|
|
||||||
index < 2 + asn1.length + asn1.lengthSize
|
|
||||||
) {
|
|
||||||
iters += 1;
|
|
||||||
depth.length += 1;
|
|
||||||
child = parseAsn1(
|
|
||||||
buf.slice(index, index + adjustedLen),
|
|
||||||
depth,
|
|
||||||
eager
|
|
||||||
);
|
|
||||||
depth.length -= 1;
|
|
||||||
// The numbers don't match up exactly and I don't remember why...
|
|
||||||
// probably something with adjustedLen or some such, but the tests pass
|
|
||||||
index += 2 + child.lengthSize + child.length;
|
|
||||||
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
|
|
||||||
if (index > 2 + asn1.lengthSize + asn1.length) {
|
|
||||||
if (!eager) {
|
|
||||||
console.error(
|
|
||||||
JSON.stringify(asn1, ASN1._replacer, 2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
'Parse error: child value length (' +
|
|
||||||
child.length +
|
|
||||||
') is greater than remaining parent length (' +
|
|
||||||
(asn1.length - index) +
|
|
||||||
' = ' +
|
|
||||||
asn1.length +
|
|
||||||
' - ' +
|
|
||||||
index +
|
|
||||||
')'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
asn1.children.push(child);
|
|
||||||
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
|
||||||
}
|
|
||||||
if (index !== 2 + asn1.lengthSize + asn1.length) {
|
|
||||||
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
|
|
||||||
throw new Error('premature end-of-file');
|
|
||||||
}
|
|
||||||
if (iters >= ASN1.ELOOPN) {
|
|
||||||
throw new Error(ASN1.ELOOP);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete asn1.value;
|
|
||||||
return asn1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse into types that are _always_ containers
|
|
||||||
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
|
|
||||||
return parseChildren(eager);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return types that are _always_ values
|
|
||||||
asn1.value = buf.slice(index, index + adjustedLen);
|
|
||||||
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
|
|
||||||
return asn1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For ambigious / unknown types, recurse and return on failure
|
|
||||||
// (and return child array size to zero)
|
|
||||||
try {
|
|
||||||
return parseChildren(true);
|
|
||||||
} catch (e) {
|
|
||||||
asn1.children.length = 0;
|
|
||||||
return asn1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var asn1 = parseAsn1(buf, []);
|
|
||||||
var len = buf.byteLength || buf.length;
|
|
||||||
if (len !== 2 + asn1.lengthSize + asn1.length) {
|
|
||||||
throw new Error(
|
|
||||||
'Length of buffer does not match length of ASN.1 sequence.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return asn1;
|
|
||||||
};
|
|
||||||
ASN1._replacer = function(k, v) {
|
|
||||||
if ('type' === k) {
|
|
||||||
return '0x' + Enc.numToHex(v);
|
|
||||||
}
|
|
||||||
if (v && 'value' === k) {
|
|
||||||
return '0x' + Enc.bufToHex(v.data || v);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
};
|
|
||||||
|
|
||||||
// don't replace the full parseBlock, if it exists
|
|
||||||
PEM.parseBlock =
|
|
||||||
PEM.parseBlock ||
|
|
||||||
function(str) {
|
|
||||||
var der = str
|
|
||||||
.split(/\n/)
|
|
||||||
.filter(function(line) {
|
|
||||||
return !/-----/.test(line);
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
return { bytes: Enc.base64ToBuf(der) };
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.base64ToBuf = function(b64) {
|
|
||||||
return Enc.binToBuf(atob(b64));
|
|
||||||
};
|
|
||||||
Enc.binToBuf = function(bin) {
|
|
||||||
var arr = bin.split('').map(function(ch) {
|
|
||||||
return ch.charCodeAt(0);
|
|
||||||
});
|
|
||||||
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
|
||||||
};
|
|
||||||
Enc.bufToHex = function(u8) {
|
|
||||||
var hex = [];
|
|
||||||
var i, h;
|
|
||||||
var len = u8.byteLength || u8.length;
|
|
||||||
|
|
||||||
for (i = 0; i < len; i += 1) {
|
|
||||||
h = u8[i].toString(16);
|
|
||||||
if (h.length % 2) {
|
|
||||||
h = '0' + h;
|
|
||||||
}
|
|
||||||
hex.push(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.join('').toLowerCase();
|
|
||||||
};
|
|
||||||
Enc.numToHex = function(d) {
|
|
||||||
d = d.toString(16);
|
|
||||||
if (d.length % 2) {
|
|
||||||
return '0' + d;
|
|
||||||
}
|
|
||||||
return d;
|
|
||||||
};
|
|
||||||
})('undefined' !== typeof window ? window : module.exports);
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var UserAgent = module.exports;
|
||||||
|
UserAgent.get = function () {
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var http = module.exports;
|
||||||
|
|
||||||
|
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 () {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Array.from(resp.headers.entries()).forEach(function (h) {
|
||||||
|
headers[h[0]] = h[1];
|
||||||
|
});
|
||||||
|
if (!headers['content-type']) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (/json/.test(headers['content-type'])) {
|
||||||
|
return resp.json().then(function (json) {
|
||||||
|
result.body = json;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resp.text().then(function (txt) {
|
||||||
|
result.body = txt;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var sha2 = module.exports;
|
||||||
|
|
||||||
|
var encoder = new TextEncoder();
|
||||||
|
sha2.sum = function (alg, str) {
|
||||||
|
var data = str;
|
||||||
|
if ('string' === typeof data) {
|
||||||
|
data = encoder.encode(str);
|
||||||
|
}
|
||||||
|
var sha = 'SHA-' + String(alg).replace(/^sha-?/i, '');
|
||||||
|
return window.crypto.subtle.digest(sha, data);
|
||||||
|
};
|
402
lib/csr.js
402
lib/csr.js
|
@ -1,402 +0,0 @@
|
||||||
// Copyright 2018-present 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/. */
|
|
||||||
(function(exports) {
|
|
||||||
'use strict';
|
|
||||||
/*global Promise*/
|
|
||||||
|
|
||||||
var ASN1 = exports.ASN1;
|
|
||||||
var Enc = exports.Enc;
|
|
||||||
var PEM = exports.PEM;
|
|
||||||
var X509 = exports.x509;
|
|
||||||
var Keypairs = exports.Keypairs;
|
|
||||||
|
|
||||||
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
|
|
||||||
var CSR = (exports.CSR = function(opts) {
|
|
||||||
// We're using a Promise here to be compatible with the browser version
|
|
||||||
// which will probably use the webcrypto API for some of the conversions
|
|
||||||
return CSR._prepare(opts).then(function(opts) {
|
|
||||||
return CSR.create(opts).then(function(bytes) {
|
|
||||||
return CSR._encode(opts, bytes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
CSR._prepare = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
var Keypairs;
|
|
||||||
opts = JSON.parse(JSON.stringify(opts));
|
|
||||||
|
|
||||||
// We do a bit of extra error checking for user convenience
|
|
||||||
if (!opts) {
|
|
||||||
throw new Error(
|
|
||||||
'You must pass options with key and domains to rsacsr'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!Array.isArray(opts.domains) || 0 === opts.domains.length) {
|
|
||||||
new Error('You must pass options.domains as a non-empty array');
|
|
||||||
}
|
|
||||||
|
|
||||||
// I need to check that 例.中国 is a valid domain name
|
|
||||||
if (
|
|
||||||
!opts.domains.every(function(d) {
|
|
||||||
// allow punycode? xn--
|
|
||||||
if (
|
|
||||||
'string' ===
|
|
||||||
typeof d /*&& /\./.test(d) && !/--/.test(d)*/
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
throw new Error('You must pass options.domains as strings');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.jwk) {
|
|
||||||
return opts;
|
|
||||||
}
|
|
||||||
if (opts.key && opts.key.kty) {
|
|
||||||
opts.jwk = opts.key;
|
|
||||||
return opts;
|
|
||||||
}
|
|
||||||
if (!opts.pem && !opts.key) {
|
|
||||||
throw new Error('You must pass options.key as a JSON web key');
|
|
||||||
}
|
|
||||||
|
|
||||||
Keypairs = exports.Keypairs;
|
|
||||||
if (!exports.Keypairs) {
|
|
||||||
throw new Error(
|
|
||||||
'Keypairs.js is an optional dependency for PEM-to-JWK.\n' +
|
|
||||||
"Install it if you'd like to use it:\n" +
|
|
||||||
'\tnpm install --save rasha\n' +
|
|
||||||
'Otherwise supply a jwk as the private key.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
|
|
||||||
pair
|
|
||||||
) {
|
|
||||||
opts.jwk = pair.private;
|
|
||||||
return opts;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
CSR._encode = function(opts, bytes) {
|
|
||||||
if ('der' === (opts.encoding || '').toLowerCase()) {
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'CERTIFICATE REQUEST',
|
|
||||||
bytes: bytes /* { jwk: jwk, domains: opts.domains } */
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
CSR.create = function createCsr(opts) {
|
|
||||||
var hex = CSR.request(opts.jwk, opts.domains);
|
|
||||||
return CSR._sign(opts.jwk, hex).then(function(csr) {
|
|
||||||
return Enc.hexToBuf(csr);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// EC / RSA
|
|
||||||
//
|
|
||||||
CSR.request = function createCsrBodyEc(jwk, domains) {
|
|
||||||
var asn1pub;
|
|
||||||
if (/^EC/i.test(jwk.kty)) {
|
|
||||||
asn1pub = X509.packCsrEcPublicKey(jwk);
|
|
||||||
} else {
|
|
||||||
asn1pub = X509.packCsrRsaPublicKey(jwk);
|
|
||||||
}
|
|
||||||
return X509.packCsr(asn1pub, domains);
|
|
||||||
};
|
|
||||||
|
|
||||||
CSR._sign = function csrEcSig(jwk, request) {
|
|
||||||
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
|
||||||
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
|
|
||||||
// TODO have a consistent non-private way to sign
|
|
||||||
return Keypairs._sign(
|
|
||||||
{ jwk: jwk, format: 'x509' },
|
|
||||||
Enc.hexToBuf(request)
|
|
||||||
).then(function(sig) {
|
|
||||||
return CSR._toDer({
|
|
||||||
request: request,
|
|
||||||
signature: sig,
|
|
||||||
kty: jwk.kty
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
CSR._toDer = function encode(opts) {
|
|
||||||
var sty;
|
|
||||||
if (/^EC/i.test(opts.kty)) {
|
|
||||||
// 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
|
|
||||||
sty = ASN1('30', ASN1('06', '2a8648ce3d040302'));
|
|
||||||
} else {
|
|
||||||
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
|
|
||||||
sty = ASN1('30', ASN1('06', '2a864886f70d01010b'), ASN1('05'));
|
|
||||||
}
|
|
||||||
return ASN1(
|
|
||||||
'30',
|
|
||||||
// The Full CSR Request Body
|
|
||||||
opts.request,
|
|
||||||
// The Signature Type
|
|
||||||
sty,
|
|
||||||
// The Signature
|
|
||||||
ASN1.BitStr(Enc.bufToHex(opts.signature))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
X509.packCsr = function(asn1pubkey, domains) {
|
|
||||||
return ASN1(
|
|
||||||
'30',
|
|
||||||
// Version (0)
|
|
||||||
ASN1.UInt('00'),
|
|
||||||
|
|
||||||
// 2.5.4.3 commonName (X.520 DN component)
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1(
|
|
||||||
'31',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1('06', '550403'),
|
|
||||||
ASN1('0c', Enc.utf8ToHex(domains[0]))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
// Public Key (RSA or EC)
|
|
||||||
asn1pubkey,
|
|
||||||
|
|
||||||
// Request Body
|
|
||||||
ASN1(
|
|
||||||
'a0',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
|
|
||||||
ASN1('06', '2a864886f70d01090e'),
|
|
||||||
ASN1(
|
|
||||||
'31',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
// 2.5.29.17 subjectAltName (X.509 extension)
|
|
||||||
ASN1('06', '551d11'),
|
|
||||||
ASN1(
|
|
||||||
'04',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
domains
|
|
||||||
.map(function(d) {
|
|
||||||
return ASN1(
|
|
||||||
'82',
|
|
||||||
Enc.utf8ToHex(d)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO finish this later
|
|
||||||
// we want to parse the domains, the public key, and verify the signature
|
|
||||||
CSR._info = function(der) {
|
|
||||||
// standard base64 PEM
|
|
||||||
if ('string' === typeof der && '-' === der[0]) {
|
|
||||||
der = PEM.parseBlock(der).bytes;
|
|
||||||
}
|
|
||||||
// jose urlBase64 not-PEM
|
|
||||||
if ('string' === typeof der) {
|
|
||||||
der = Enc.base64ToBuf(der);
|
|
||||||
}
|
|
||||||
// not supporting binary-encoded bas64
|
|
||||||
var c = ASN1.parse(der);
|
|
||||||
var kty;
|
|
||||||
// A cert has 3 parts: cert, signature meta, signature
|
|
||||||
if (c.children.length !== 3) {
|
|
||||||
throw new Error(
|
|
||||||
"doesn't look like a certificate request: expected 3 parts of header"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var sig = c.children[2];
|
|
||||||
if (sig.children.length) {
|
|
||||||
// ASN1/X509 EC
|
|
||||||
sig = sig.children[0];
|
|
||||||
sig = ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt(Enc.bufToHex(sig.children[0].value)),
|
|
||||||
ASN1.UInt(Enc.bufToHex(sig.children[1].value))
|
|
||||||
);
|
|
||||||
sig = Enc.hexToBuf(sig);
|
|
||||||
kty = 'EC';
|
|
||||||
} else {
|
|
||||||
// Raw RSA Sig
|
|
||||||
sig = sig.value;
|
|
||||||
kty = 'RSA';
|
|
||||||
}
|
|
||||||
//c.children[1]; // signature type
|
|
||||||
var req = c.children[0];
|
|
||||||
// TODO utf8
|
|
||||||
if (4 !== req.children.length) {
|
|
||||||
throw new Error(
|
|
||||||
"doesn't look like a certificate request: expected 4 parts to request"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 0 null
|
|
||||||
// 1 commonName / subject
|
|
||||||
var sub = Enc.bufToBin(
|
|
||||||
req.children[1].children[0].children[0].children[1].value
|
|
||||||
);
|
|
||||||
// 3 public key (type, key)
|
|
||||||
//console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value));
|
|
||||||
var pub;
|
|
||||||
// TODO reuse ASN1 parser for these?
|
|
||||||
if ('EC' === kty) {
|
|
||||||
// throw away compression byte
|
|
||||||
pub = req.children[2].children[1].value.slice(1);
|
|
||||||
pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) };
|
|
||||||
while (0 === pub.x[0]) {
|
|
||||||
pub.x = pub.x.slice(1);
|
|
||||||
}
|
|
||||||
while (0 === pub.y[0]) {
|
|
||||||
pub.y = pub.y.slice(1);
|
|
||||||
}
|
|
||||||
if ((pub.x.length || pub.x.byteLength) > 48) {
|
|
||||||
pub.crv = 'P-521';
|
|
||||||
} else if ((pub.x.length || pub.x.byteLength) > 32) {
|
|
||||||
pub.crv = 'P-384';
|
|
||||||
} else {
|
|
||||||
pub.crv = 'P-256';
|
|
||||||
}
|
|
||||||
pub.x = Enc.bufToUrlBase64(pub.x);
|
|
||||||
pub.y = Enc.bufToUrlBase64(pub.y);
|
|
||||||
} else {
|
|
||||||
pub = req.children[2].children[1].children[0];
|
|
||||||
pub = {
|
|
||||||
kty: kty,
|
|
||||||
n: pub.children[0].value,
|
|
||||||
e: pub.children[1].value
|
|
||||||
};
|
|
||||||
while (0 === pub.n[0]) {
|
|
||||||
pub.n = pub.n.slice(1);
|
|
||||||
}
|
|
||||||
while (0 === pub.e[0]) {
|
|
||||||
pub.e = pub.e.slice(1);
|
|
||||||
}
|
|
||||||
pub.n = Enc.bufToUrlBase64(pub.n);
|
|
||||||
pub.e = Enc.bufToUrlBase64(pub.e);
|
|
||||||
}
|
|
||||||
// 4 extensions
|
|
||||||
var domains = req.children[3].children
|
|
||||||
.filter(function(seq) {
|
|
||||||
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
|
|
||||||
if (
|
|
||||||
'2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(function(seq) {
|
|
||||||
return seq.children[1].children[0].children
|
|
||||||
.filter(function(seq2) {
|
|
||||||
// subjectAltName (X.509 extension)
|
|
||||||
if ('551d11' === Enc.bufToHex(seq2.children[0].value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(function(seq2) {
|
|
||||||
return seq2.children[1].children[0].children.map(
|
|
||||||
function(name) {
|
|
||||||
// TODO utf8
|
|
||||||
return Enc.bufToBin(name.value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})[0];
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
return {
|
|
||||||
subject: sub,
|
|
||||||
altnames: domains,
|
|
||||||
jwk: pub,
|
|
||||||
signature: sig
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
X509.packCsrRsaPublicKey = function(jwk) {
|
|
||||||
// Sequence the key
|
|
||||||
var n = ASN1.UInt(Enc.base64ToHex(jwk.n));
|
|
||||||
var e = ASN1.UInt(Enc.base64ToHex(jwk.e));
|
|
||||||
var asn1pub = ASN1('30', n, e);
|
|
||||||
|
|
||||||
// Add the CSR pub key header
|
|
||||||
return ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')),
|
|
||||||
ASN1.BitStr(asn1pub)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
X509.packCsrEcPublicKey = function(jwk) {
|
|
||||||
var ecOid = X509._oids[jwk.crv];
|
|
||||||
if (!ecOid) {
|
|
||||||
throw new Error(
|
|
||||||
"Unsupported namedCurve '" +
|
|
||||||
jwk.crv +
|
|
||||||
"'. Supported types are " +
|
|
||||||
Object.keys(X509._oids)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var cmp = '04'; // 04 == x+y, 02 == x-only
|
|
||||||
var hxy = '';
|
|
||||||
// Placeholder. I'm not even sure if compression should be supported.
|
|
||||||
if (!jwk.y) {
|
|
||||||
cmp = '02';
|
|
||||||
}
|
|
||||||
hxy += Enc.base64ToHex(jwk.x);
|
|
||||||
if (jwk.y) {
|
|
||||||
hxy += Enc.base64ToHex(jwk.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.2.840.10045.2.1 ecPublicKey
|
|
||||||
return ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1('30', ASN1('06', '2a8648ce3d0201'), ASN1('06', ecOid)),
|
|
||||||
ASN1.BitStr(cmp + hxy)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
X509._oids = {
|
|
||||||
// 1.2.840.10045.3.1.7 prime256v1
|
|
||||||
// (ANSI X9.62 named elliptic curve) (06 08 - 2A 86 48 CE 3D 03 01 07)
|
|
||||||
'P-256': '2a8648ce3d030107',
|
|
||||||
// 1.3.132.0.34 P-384 (06 05 - 2B 81 04 00 22)
|
|
||||||
// (SEC 2 recommended EC domain secp256r1)
|
|
||||||
'P-384': '2b81040022'
|
|
||||||
// requires more logic and isn't a recommended standard
|
|
||||||
// 1.3.132.0.35 P-521 (06 05 - 2B 81 04 00 23)
|
|
||||||
// (SEC 2 alternate P-521)
|
|
||||||
//, 'P-521': '2B 81 04 00 23'
|
|
||||||
};
|
|
||||||
|
|
||||||
// don't replace the full parseBlock, if it exists
|
|
||||||
PEM.parseBlock =
|
|
||||||
PEM.parseBlock ||
|
|
||||||
function(str) {
|
|
||||||
var der = str
|
|
||||||
.split(/\n/)
|
|
||||||
.filter(function(line) {
|
|
||||||
return !/-----/.test(line);
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
return { bytes: Enc.base64ToBuf(der) };
|
|
||||||
};
|
|
||||||
})('undefined' === typeof window ? module.exports : window);
|
|
227
lib/ecdsa.js
227
lib/ecdsa.js
|
@ -1,227 +0,0 @@
|
||||||
/*global Promise*/
|
|
||||||
(function(exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var EC = (exports.Eckles = {});
|
|
||||||
var x509 = exports.x509;
|
|
||||||
if ('undefined' !== typeof module) {
|
|
||||||
module.exports = EC;
|
|
||||||
}
|
|
||||||
var PEM = exports.PEM;
|
|
||||||
var SSH = exports.SSH;
|
|
||||||
var Enc = {};
|
|
||||||
var textEncoder = new TextEncoder();
|
|
||||||
|
|
||||||
EC._stance =
|
|
||||||
"We take the stance that if you're knowledgeable enough to" +
|
|
||||||
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
|
|
||||||
EC._universal =
|
|
||||||
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
|
|
||||||
EC.generate = function(opts) {
|
|
||||||
var wcOpts = {};
|
|
||||||
if (!opts) {
|
|
||||||
opts = {};
|
|
||||||
}
|
|
||||||
if (!opts.kty) {
|
|
||||||
opts.kty = 'EC';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ECDSA has only the P curves and an associated bitlength
|
|
||||||
wcOpts.name = 'ECDSA';
|
|
||||||
if (!opts.namedCurve) {
|
|
||||||
opts.namedCurve = 'P-256';
|
|
||||||
}
|
|
||||||
wcOpts.namedCurve = opts.namedCurve; // true for supported curves
|
|
||||||
if (/256/.test(wcOpts.namedCurve)) {
|
|
||||||
wcOpts.namedCurve = 'P-256';
|
|
||||||
wcOpts.hash = { name: 'SHA-256' };
|
|
||||||
} else if (/384/.test(wcOpts.namedCurve)) {
|
|
||||||
wcOpts.namedCurve = 'P-384';
|
|
||||||
wcOpts.hash = { name: 'SHA-384' };
|
|
||||||
} else {
|
|
||||||
return Promise.Reject(
|
|
||||||
new Error(
|
|
||||||
"'" +
|
|
||||||
wcOpts.namedCurve +
|
|
||||||
"' is not an NIST approved ECDSA namedCurve. " +
|
|
||||||
" Please choose either 'P-256' or 'P-384'. " +
|
|
||||||
EC._stance
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var extractable = true;
|
|
||||||
return window.crypto.subtle
|
|
||||||
.generateKey(wcOpts, extractable, ['sign', 'verify'])
|
|
||||||
.then(function(result) {
|
|
||||||
return window.crypto.subtle
|
|
||||||
.exportKey('jwk', result.privateKey)
|
|
||||||
.then(function(privJwk) {
|
|
||||||
privJwk.key_ops = undefined;
|
|
||||||
privJwk.ext = undefined;
|
|
||||||
return {
|
|
||||||
private: privJwk,
|
|
||||||
public: EC.neuter({ jwk: privJwk })
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
EC.export = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
|
|
||||||
throw new Error('must pass { jwk: jwk } as a JSON object');
|
|
||||||
}
|
|
||||||
var jwk = JSON.parse(JSON.stringify(opts.jwk));
|
|
||||||
var format = opts.format;
|
|
||||||
if (
|
|
||||||
opts.public ||
|
|
||||||
-1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)
|
|
||||||
) {
|
|
||||||
jwk.d = null;
|
|
||||||
}
|
|
||||||
if ('EC' !== jwk.kty) {
|
|
||||||
throw new Error("options.jwk.kty must be 'EC' for EC keys");
|
|
||||||
}
|
|
||||||
if (!jwk.d) {
|
|
||||||
if (!format || -1 !== ['spki', 'pkix'].indexOf(format)) {
|
|
||||||
format = 'spki';
|
|
||||||
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
|
|
||||||
format = 'ssh';
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"options.format must be 'spki' or 'ssh' for public EC keys, not (" +
|
|
||||||
typeof format +
|
|
||||||
') ' +
|
|
||||||
format
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!format || 'sec1' === format) {
|
|
||||||
format = 'sec1';
|
|
||||||
} else if ('pkcs8' !== format) {
|
|
||||||
throw new Error(
|
|
||||||
"options.format must be 'sec1' or 'pkcs8' for private EC keys, not '" +
|
|
||||||
format +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-1 === ['P-256', 'P-384'].indexOf(jwk.crv)) {
|
|
||||||
throw new Error(
|
|
||||||
"options.jwk.crv must be either P-256 or P-384 for EC keys, not '" +
|
|
||||||
jwk.crv +
|
|
||||||
"'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!jwk.y) {
|
|
||||||
throw new Error(
|
|
||||||
'options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('sec1' === format) {
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'EC PRIVATE KEY',
|
|
||||||
bytes: x509.packSec1(jwk)
|
|
||||||
});
|
|
||||||
} else if ('pkcs8' === format) {
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'PRIVATE KEY',
|
|
||||||
bytes: x509.packPkcs8(jwk)
|
|
||||||
});
|
|
||||||
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'PUBLIC KEY',
|
|
||||||
bytes: x509.packSpki(jwk)
|
|
||||||
});
|
|
||||||
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
|
|
||||||
return SSH.packSsh(jwk);
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'Sanity Error: reached unreachable code block with format: ' +
|
|
||||||
format
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
EC.pack = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
return EC.exportSync(opts);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chopping off the private parts is now part of the public API.
|
|
||||||
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
|
|
||||||
EC.neuter = function(opts) {
|
|
||||||
// trying to find the best balance of an immutable copy with custom attributes
|
|
||||||
var jwk = {};
|
|
||||||
Object.keys(opts.jwk).forEach(function(k) {
|
|
||||||
if ('undefined' === typeof opts.jwk[k]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ignore EC private parts
|
|
||||||
if ('d' === k) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
|
|
||||||
});
|
|
||||||
return jwk;
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
|
||||||
EC.__thumbprint = function(jwk) {
|
|
||||||
// Use the same entropy for SHA as for key
|
|
||||||
var alg = 'SHA-256';
|
|
||||||
if (/384/.test(jwk.crv)) {
|
|
||||||
alg = 'SHA-384';
|
|
||||||
}
|
|
||||||
return window.crypto.subtle
|
|
||||||
.digest(
|
|
||||||
{ name: alg },
|
|
||||||
textEncoder.encode(
|
|
||||||
'{"crv":"' +
|
|
||||||
jwk.crv +
|
|
||||||
'","kty":"EC","x":"' +
|
|
||||||
jwk.x +
|
|
||||||
'","y":"' +
|
|
||||||
jwk.y +
|
|
||||||
'"}'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(function(hash) {
|
|
||||||
return Enc.bufToUrlBase64(new Uint8Array(hash));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
EC.thumbprint = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
var jwk;
|
|
||||||
if ('EC' === opts.kty) {
|
|
||||||
jwk = opts;
|
|
||||||
} else if (opts.jwk) {
|
|
||||||
jwk = opts.jwk;
|
|
||||||
} else {
|
|
||||||
return EC.import(opts).then(function(jwk) {
|
|
||||||
return EC.__thumbprint(jwk);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return EC.__thumbprint(jwk);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToUrlBase64 = function(u8) {
|
|
||||||
return Enc.bufToBase64(u8)
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToBase64 = function(u8) {
|
|
||||||
var bin = '';
|
|
||||||
u8.forEach(function(i) {
|
|
||||||
bin += String.fromCharCode(i);
|
|
||||||
});
|
|
||||||
return btoa(bin);
|
|
||||||
};
|
|
||||||
})('undefined' !== typeof module ? module.exports : window);
|
|
151
lib/encoding.js
151
lib/encoding.js
|
@ -1,151 +0,0 @@
|
||||||
(function(exports) {
|
|
||||||
var Enc = (exports.Enc = {});
|
|
||||||
|
|
||||||
Enc.bufToBin = function(buf) {
|
|
||||||
var bin = '';
|
|
||||||
// cannot use .map() because Uint8Array would return only 0s
|
|
||||||
buf.forEach(function(ch) {
|
|
||||||
bin += String.fromCharCode(ch);
|
|
||||||
});
|
|
||||||
return bin;
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToHex = function toHex(u8) {
|
|
||||||
var hex = [];
|
|
||||||
var i, h;
|
|
||||||
var len = u8.byteLength || u8.length;
|
|
||||||
|
|
||||||
for (i = 0; i < len; i += 1) {
|
|
||||||
h = u8[i].toString(16);
|
|
||||||
if (h.length % 2) {
|
|
||||||
h = '0' + h;
|
|
||||||
}
|
|
||||||
hex.push(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.join('').toLowerCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) {
|
|
||||||
var r = str % 4;
|
|
||||||
if (2 === r) {
|
|
||||||
str += '==';
|
|
||||||
} else if (3 === r) {
|
|
||||||
str += '=';
|
|
||||||
}
|
|
||||||
return str.replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.base64ToBuf = function(b64) {
|
|
||||||
return Enc.binToBuf(atob(b64));
|
|
||||||
};
|
|
||||||
Enc.binToBuf = function(bin) {
|
|
||||||
var arr = bin.split('').map(function(ch) {
|
|
||||||
return ch.charCodeAt(0);
|
|
||||||
});
|
|
||||||
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
|
||||||
};
|
|
||||||
Enc.bufToHex = function(u8) {
|
|
||||||
var hex = [];
|
|
||||||
var i, h;
|
|
||||||
var len = u8.byteLength || u8.length;
|
|
||||||
|
|
||||||
for (i = 0; i < len; i += 1) {
|
|
||||||
h = u8[i].toString(16);
|
|
||||||
if (h.length % 2) {
|
|
||||||
h = '0' + h;
|
|
||||||
}
|
|
||||||
hex.push(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.join('').toLowerCase();
|
|
||||||
};
|
|
||||||
Enc.numToHex = function(d) {
|
|
||||||
d = d.toString(16);
|
|
||||||
if (d.length % 2) {
|
|
||||||
return '0' + d;
|
|
||||||
}
|
|
||||||
return d;
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToUrlBase64 = function(u8) {
|
|
||||||
return Enc.base64ToUrlBase64(Enc.bufToBase64(u8));
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.base64ToUrlBase64 = function(str) {
|
|
||||||
return str
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToBase64 = function(u8) {
|
|
||||||
var bin = '';
|
|
||||||
u8.forEach(function(i) {
|
|
||||||
bin += String.fromCharCode(i);
|
|
||||||
});
|
|
||||||
return btoa(bin);
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.hexToBuf = function(hex) {
|
|
||||||
var arr = [];
|
|
||||||
hex.match(/.{2}/g).forEach(function(h) {
|
|
||||||
arr.push(parseInt(h, 16));
|
|
||||||
});
|
|
||||||
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.numToHex = function(d) {
|
|
||||||
d = d.toString(16);
|
|
||||||
if (d.length % 2) {
|
|
||||||
return '0' + d;
|
|
||||||
}
|
|
||||||
return d;
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// JWK to SSH (tested working)
|
|
||||||
//
|
|
||||||
Enc.base64ToHex = function(b64) {
|
|
||||||
var bin = atob(Enc.urlBase64ToBase64(b64));
|
|
||||||
return Enc.binToHex(bin);
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.binToHex = function(bin) {
|
|
||||||
return bin
|
|
||||||
.split('')
|
|
||||||
.map(function(ch) {
|
|
||||||
var h = ch.charCodeAt(0).toString(16);
|
|
||||||
if (h.length % 2) {
|
|
||||||
h = '0' + h;
|
|
||||||
}
|
|
||||||
return h;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
};
|
|
||||||
// TODO are there any nuance differences here?
|
|
||||||
Enc.utf8ToHex = Enc.binToHex;
|
|
||||||
|
|
||||||
Enc.hexToBase64 = function(hex) {
|
|
||||||
return btoa(Enc.hexToBin(hex));
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.hexToBin = function(hex) {
|
|
||||||
return hex
|
|
||||||
.match(/.{2}/g)
|
|
||||||
.map(function(h) {
|
|
||||||
return String.fromCharCode(parseInt(h, 16));
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) {
|
|
||||||
var r = str % 4;
|
|
||||||
if (2 === r) {
|
|
||||||
str += '==';
|
|
||||||
} else if (3 === r) {
|
|
||||||
str += '=';
|
|
||||||
}
|
|
||||||
return str.replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
};
|
|
||||||
})('undefined' !== typeof exports ? module.exports : window);
|
|
432
lib/keypairs.js
432
lib/keypairs.js
|
@ -1,432 +0,0 @@
|
||||||
/*global Promise*/
|
|
||||||
(function(exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var Keypairs = (exports.Keypairs = {});
|
|
||||||
var Rasha = exports.Rasha;
|
|
||||||
var Eckles = exports.Eckles;
|
|
||||||
var Enc = exports.Enc || {};
|
|
||||||
|
|
||||||
Keypairs._stance =
|
|
||||||
"We take the stance that if you're knowledgeable enough to" +
|
|
||||||
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
|
|
||||||
Keypairs._universal =
|
|
||||||
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
|
|
||||||
Keypairs.generate = function(opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
var p;
|
|
||||||
if (!opts.kty) {
|
|
||||||
opts.kty = opts.type;
|
|
||||||
}
|
|
||||||
if (!opts.kty) {
|
|
||||||
opts.kty = 'EC';
|
|
||||||
}
|
|
||||||
if (/^EC/i.test(opts.kty)) {
|
|
||||||
p = Eckles.generate(opts);
|
|
||||||
} else if (/^RSA$/i.test(opts.kty)) {
|
|
||||||
p = Rasha.generate(opts);
|
|
||||||
} else {
|
|
||||||
return Promise.Reject(
|
|
||||||
new Error(
|
|
||||||
"'" +
|
|
||||||
opts.kty +
|
|
||||||
"' is not a well-supported key type." +
|
|
||||||
Keypairs._universal +
|
|
||||||
" Please choose 'EC', or 'RSA' if you have good reason to."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return p.then(function(pair) {
|
|
||||||
return Keypairs.thumbprint({ jwk: pair.public }).then(function(
|
|
||||||
thumb
|
|
||||||
) {
|
|
||||||
pair.private.kid = thumb; // maybe not the same id on the private key?
|
|
||||||
pair.public.kid = thumb;
|
|
||||||
return pair;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Keypairs.export = function(opts) {
|
|
||||||
return Eckles.export(opts).catch(function(err) {
|
|
||||||
return Rasha.export(opts).catch(function() {
|
|
||||||
return Promise.reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chopping off the private parts is now part of the public API.
|
|
||||||
* I thought it sounded a little too crude at first, but it really is the best name in every possible way.
|
|
||||||
*/
|
|
||||||
Keypairs.neuter = function(opts) {
|
|
||||||
/** trying to find the best balance of an immutable copy with custom attributes */
|
|
||||||
var jwk = {};
|
|
||||||
Object.keys(opts.jwk).forEach(function(k) {
|
|
||||||
if ('undefined' === typeof opts.jwk[k]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ignore RSA and EC private parts
|
|
||||||
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
|
|
||||||
});
|
|
||||||
return jwk;
|
|
||||||
};
|
|
||||||
|
|
||||||
Keypairs.thumbprint = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
if (/EC/i.test(opts.jwk.kty)) {
|
|
||||||
return Eckles.thumbprint(opts);
|
|
||||||
} else {
|
|
||||||
return Rasha.thumbprint(opts);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Keypairs.publish = function(opts) {
|
|
||||||
if ('object' !== typeof opts.jwk || !opts.jwk.kty) {
|
|
||||||
throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** returns a copy */
|
|
||||||
var jwk = Keypairs.neuter(opts);
|
|
||||||
|
|
||||||
if (jwk.exp) {
|
|
||||||
jwk.exp = setTime(jwk.exp);
|
|
||||||
} else {
|
|
||||||
if (opts.exp) {
|
|
||||||
jwk.exp = setTime(opts.exp);
|
|
||||||
} else if (opts.expiresIn) {
|
|
||||||
jwk.exp = Math.round(Date.now() / 1000) + opts.expiresIn;
|
|
||||||
} else if (opts.expiresAt) {
|
|
||||||
jwk.exp = opts.expiresAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!jwk.use && false !== jwk.use) {
|
|
||||||
jwk.use = 'sig';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jwk.kid) {
|
|
||||||
return Promise.resolve(jwk);
|
|
||||||
}
|
|
||||||
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
|
|
||||||
jwk.kid = thumb;
|
|
||||||
return jwk;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// JWT a.k.a. JWS with Claims using Compact Serialization
|
|
||||||
Keypairs.signJwt = function(opts) {
|
|
||||||
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
|
|
||||||
var header = opts.header || {};
|
|
||||||
var claims = JSON.parse(JSON.stringify(opts.claims || {}));
|
|
||||||
header.typ = 'JWT';
|
|
||||||
|
|
||||||
if (!header.kid) {
|
|
||||||
header.kid = thumb;
|
|
||||||
}
|
|
||||||
if (!header.alg && opts.alg) {
|
|
||||||
header.alg = opts.alg;
|
|
||||||
}
|
|
||||||
if (!claims.iat && (false === claims.iat || false === opts.iat)) {
|
|
||||||
claims.iat = undefined;
|
|
||||||
} else if (!claims.iat) {
|
|
||||||
claims.iat = Math.round(Date.now() / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.exp) {
|
|
||||||
claims.exp = setTime(opts.exp);
|
|
||||||
} else if (
|
|
||||||
!claims.exp &&
|
|
||||||
(false === claims.exp || false === opts.exp)
|
|
||||||
) {
|
|
||||||
claims.exp = undefined;
|
|
||||||
} else if (!claims.exp) {
|
|
||||||
throw new Error(
|
|
||||||
"opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.iss) {
|
|
||||||
claims.iss = opts.iss;
|
|
||||||
}
|
|
||||||
if (!claims.iss && (false === claims.iss || false === opts.iss)) {
|
|
||||||
claims.iss = undefined;
|
|
||||||
} else if (!claims.iss) {
|
|
||||||
throw new Error(
|
|
||||||
'opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Keypairs.signJws({
|
|
||||||
jwk: opts.jwk,
|
|
||||||
pem: opts.pem,
|
|
||||||
protected: header,
|
|
||||||
header: undefined,
|
|
||||||
payload: claims
|
|
||||||
}).then(function(jws) {
|
|
||||||
return [jws.protected, jws.payload, jws.signature].join('.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Keypairs.signJws = function(opts) {
|
|
||||||
return Keypairs.thumbprint(opts).then(function(thumb) {
|
|
||||||
function alg() {
|
|
||||||
if (!opts.jwk) {
|
|
||||||
throw new Error(
|
|
||||||
"opts.jwk must exist and must declare 'typ'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (opts.jwk.alg) {
|
|
||||||
return opts.jwk.alg;
|
|
||||||
}
|
|
||||||
var typ = 'RSA' === opts.jwk.kty ? 'RS' : 'ES';
|
|
||||||
return typ + Keypairs._getBits(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sign() {
|
|
||||||
var protect = opts.protected;
|
|
||||||
var payload = opts.payload;
|
|
||||||
|
|
||||||
// Compute JWS signature
|
|
||||||
var protectedHeader = '';
|
|
||||||
// Because unprotected headers are allowed, regrettably...
|
|
||||||
// https://stackoverflow.com/a/46288694
|
|
||||||
if (false !== protect) {
|
|
||||||
if (!protect) {
|
|
||||||
protect = {};
|
|
||||||
}
|
|
||||||
if (!protect.alg) {
|
|
||||||
protect.alg = alg();
|
|
||||||
}
|
|
||||||
// There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
|
|
||||||
if (false === protect.kid) {
|
|
||||||
protect.kid = undefined;
|
|
||||||
} else if (!protect.kid) {
|
|
||||||
protect.kid = thumb;
|
|
||||||
}
|
|
||||||
protectedHeader = JSON.stringify(protect);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not sure how to handle the empty case since ACME POST-as-GET must be empty
|
|
||||||
//if (!payload) {
|
|
||||||
// throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)");
|
|
||||||
//}
|
|
||||||
// Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
|
|
||||||
if (
|
|
||||||
payload &&
|
|
||||||
'string' !== typeof payload &&
|
|
||||||
'undefined' === typeof payload.byteLength &&
|
|
||||||
'undefined' === typeof payload.buffer
|
|
||||||
) {
|
|
||||||
payload = JSON.stringify(payload);
|
|
||||||
}
|
|
||||||
// Converting to a buffer, even if it was just converted to a string
|
|
||||||
if ('string' === typeof payload) {
|
|
||||||
payload = Enc.binToBuf(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
|
|
||||||
var protected64 = Enc.strToUrlBase64(protectedHeader);
|
|
||||||
var payload64 = Enc.bufToUrlBase64(payload);
|
|
||||||
var msg = protected64 + '.' + payload64;
|
|
||||||
|
|
||||||
return Keypairs._sign(opts, msg).then(function(buf) {
|
|
||||||
var signedMsg = {
|
|
||||||
protected: protected64,
|
|
||||||
payload: payload64,
|
|
||||||
signature: Enc.bufToUrlBase64(buf)
|
|
||||||
};
|
|
||||||
|
|
||||||
return signedMsg;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.jwk) {
|
|
||||||
return sign();
|
|
||||||
} else {
|
|
||||||
return Keypairs.import({ pem: opts.pem }).then(function(pair) {
|
|
||||||
opts.jwk = pair.private;
|
|
||||||
return sign();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Keypairs._sign = function(opts, payload) {
|
|
||||||
return Keypairs._import(opts).then(function(privkey) {
|
|
||||||
if ('string' === typeof payload) {
|
|
||||||
payload = new TextEncoder().encode(payload);
|
|
||||||
}
|
|
||||||
return window.crypto.subtle
|
|
||||||
.sign(
|
|
||||||
{
|
|
||||||
name: Keypairs._getName(opts),
|
|
||||||
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
|
|
||||||
},
|
|
||||||
privkey,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
.then(function(signature) {
|
|
||||||
signature = new Uint8Array(signature); // ArrayBuffer -> u8
|
|
||||||
// This will come back into play for CSRs, but not for JOSE
|
|
||||||
if (
|
|
||||||
'EC' === opts.jwk.kty &&
|
|
||||||
/x509|asn1/i.test(opts.format)
|
|
||||||
) {
|
|
||||||
return Keypairs._ecdsaJoseSigToAsn1Sig(signature);
|
|
||||||
} else {
|
|
||||||
// jose/jws/jwt
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Keypairs._getBits = function(opts) {
|
|
||||||
if (opts.alg) {
|
|
||||||
return opts.alg.replace(/[a-z\-]/gi, '');
|
|
||||||
}
|
|
||||||
// base64 len to byte len
|
|
||||||
var len = Math.floor((opts.jwk.n || '').length * 0.75);
|
|
||||||
|
|
||||||
// TODO this may be a bug
|
|
||||||
// need to confirm that the padding is no more or less than 1 byte
|
|
||||||
if (/521/.test(opts.jwk.crv) || len >= 511) {
|
|
||||||
return '512';
|
|
||||||
} else if (/384/.test(opts.jwk.crv) || len >= 383) {
|
|
||||||
return '384';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '256';
|
|
||||||
};
|
|
||||||
Keypairs._getName = function(opts) {
|
|
||||||
if (/EC/i.test(opts.jwk.kty)) {
|
|
||||||
return 'ECDSA';
|
|
||||||
} else {
|
|
||||||
return 'RSASSA-PKCS1-v1_5';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Keypairs._import = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
var ops;
|
|
||||||
// all private keys just happen to have a 'd'
|
|
||||||
if (opts.jwk.d) {
|
|
||||||
ops = ['sign'];
|
|
||||||
} else {
|
|
||||||
ops = ['verify'];
|
|
||||||
}
|
|
||||||
// gotta mark it as extractable, as if it matters
|
|
||||||
opts.jwk.ext = true;
|
|
||||||
opts.jwk.key_ops = ops;
|
|
||||||
|
|
||||||
return window.crypto.subtle
|
|
||||||
.importKey(
|
|
||||||
'jwk',
|
|
||||||
opts.jwk,
|
|
||||||
{
|
|
||||||
name: Keypairs._getName(opts),
|
|
||||||
namedCurve: opts.jwk.crv,
|
|
||||||
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
ops
|
|
||||||
)
|
|
||||||
.then(function(privkey) {
|
|
||||||
delete opts.jwk.ext;
|
|
||||||
return privkey;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures
|
|
||||||
// https://tools.ietf.org/html/rfc7518#section-3.4
|
|
||||||
Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) {
|
|
||||||
// it's easier to do the manipulation in the browser with an array
|
|
||||||
bufsig = Array.from(bufsig);
|
|
||||||
var hlen = bufsig.length / 2; // should be even
|
|
||||||
var r = bufsig.slice(0, hlen);
|
|
||||||
var s = bufsig.slice(hlen);
|
|
||||||
// unpad positive ints less than 32 bytes wide
|
|
||||||
while (!r[0]) {
|
|
||||||
r = r.slice(1);
|
|
||||||
}
|
|
||||||
while (!s[0]) {
|
|
||||||
s = s.slice(1);
|
|
||||||
}
|
|
||||||
// pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide
|
|
||||||
if (0x80 & r[0]) {
|
|
||||||
r.unshift(0);
|
|
||||||
}
|
|
||||||
if (0x80 & s[0]) {
|
|
||||||
s.unshift(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var len = 2 + r.length + 2 + s.length;
|
|
||||||
var head = [0x30];
|
|
||||||
// hard code 0x80 + 1 because it won't be longer than
|
|
||||||
// two SHA512 plus two pad bytes (130 bytes <= 256)
|
|
||||||
if (len >= 0x80) {
|
|
||||||
head.push(0x81);
|
|
||||||
}
|
|
||||||
head.push(len);
|
|
||||||
|
|
||||||
return Uint8Array.from(
|
|
||||||
head.concat([0x02, r.length], r, [0x02, s.length], s)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function setTime(time) {
|
|
||||||
if ('number' === typeof time) {
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
var t = time.match(/^(\-?\d+)([dhms])$/i);
|
|
||||||
if (!t || !t[0]) {
|
|
||||||
throw new Error(
|
|
||||||
"'" +
|
|
||||||
time +
|
|
||||||
"' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = Math.round(Date.now() / 1000);
|
|
||||||
var num = parseInt(t[1], 10);
|
|
||||||
var unit = t[2];
|
|
||||||
var mult = 1;
|
|
||||||
switch (unit) {
|
|
||||||
// fancy fallthrough, what fun!
|
|
||||||
case 'd':
|
|
||||||
mult *= 24;
|
|
||||||
/*falls through*/
|
|
||||||
case 'h':
|
|
||||||
mult *= 60;
|
|
||||||
/*falls through*/
|
|
||||||
case 'm':
|
|
||||||
mult *= 60;
|
|
||||||
/*falls through*/
|
|
||||||
case 's':
|
|
||||||
mult *= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return now + mult * num;
|
|
||||||
}
|
|
||||||
|
|
||||||
Enc.hexToBuf = function(hex) {
|
|
||||||
var arr = [];
|
|
||||||
hex.match(/.{2}/g).forEach(function(h) {
|
|
||||||
arr.push(parseInt(h, 16));
|
|
||||||
});
|
|
||||||
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
|
||||||
};
|
|
||||||
Enc.strToUrlBase64 = function(str) {
|
|
||||||
return Enc.bufToUrlBase64(Enc.binToBuf(str));
|
|
||||||
};
|
|
||||||
Enc.binToBuf = function(bin) {
|
|
||||||
var arr = bin.split('').map(function(ch) {
|
|
||||||
return ch.charCodeAt(0);
|
|
||||||
});
|
|
||||||
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
|
||||||
};
|
|
||||||
})('undefined' !== typeof module ? module.exports : window);
|
|
|
@ -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';
|
||||||
|
});
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var http = module.exports;
|
||||||
|
var promisify = require('util').promisify;
|
||||||
|
var request = promisify(require('@root/request'));
|
||||||
|
|
||||||
|
http.request = function (opts) {
|
||||||
|
return request(opts);
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* global Promise */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var sha2 = module.exports;
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
sha2.sum = function (alg, str) {
|
||||||
|
return Promise.resolve().then(function () {
|
||||||
|
var sha = 'sha' + String(alg).replace(/^sha-?/i, '');
|
||||||
|
// utf8 is the default for strings
|
||||||
|
var buf = Buffer.from(str);
|
||||||
|
return crypto.createHash(sha).update(buf).digest();
|
||||||
|
});
|
||||||
|
};
|
235
lib/rsa.js
235
lib/rsa.js
|
@ -1,235 +0,0 @@
|
||||||
/*global Promise*/
|
|
||||||
(function(exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var RSA = (exports.Rasha = {});
|
|
||||||
var x509 = exports.x509;
|
|
||||||
if ('undefined' !== typeof module) {
|
|
||||||
module.exports = RSA;
|
|
||||||
}
|
|
||||||
var PEM = exports.PEM;
|
|
||||||
var SSH = exports.SSH;
|
|
||||||
var Enc = {};
|
|
||||||
var textEncoder = new TextEncoder();
|
|
||||||
|
|
||||||
RSA._stance =
|
|
||||||
"We take the stance that if you're knowledgeable enough to" +
|
|
||||||
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
|
|
||||||
RSA._universal =
|
|
||||||
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
|
|
||||||
RSA.generate = function(opts) {
|
|
||||||
var wcOpts = {};
|
|
||||||
if (!opts) {
|
|
||||||
opts = {};
|
|
||||||
}
|
|
||||||
if (!opts.kty) {
|
|
||||||
opts.kty = 'RSA';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support PSS? I don't think it's used for Let's Encrypt
|
|
||||||
wcOpts.name = 'RSASSA-PKCS1-v1_5';
|
|
||||||
if (!opts.modulusLength) {
|
|
||||||
opts.modulusLength = 2048;
|
|
||||||
}
|
|
||||||
wcOpts.modulusLength = opts.modulusLength;
|
|
||||||
if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) {
|
|
||||||
// erring on the small side... for no good reason
|
|
||||||
wcOpts.hash = { name: 'SHA-256' };
|
|
||||||
} else if (
|
|
||||||
wcOpts.modulusLength >= 3072 &&
|
|
||||||
wcOpts.modulusLength < 4096
|
|
||||||
) {
|
|
||||||
wcOpts.hash = { name: 'SHA-384' };
|
|
||||||
} else if (wcOpts.modulusLength < 4097) {
|
|
||||||
wcOpts.hash = { name: 'SHA-512' };
|
|
||||||
} else {
|
|
||||||
// Public key thumbprints should be paired with a hash of similar length,
|
|
||||||
// so anything above SHA-512's keyspace would be left under-represented anyway.
|
|
||||||
return Promise.Reject(
|
|
||||||
new Error(
|
|
||||||
"'" +
|
|
||||||
wcOpts.modulusLength +
|
|
||||||
"' is not within the safe and universally" +
|
|
||||||
' acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values' +
|
|
||||||
' divisible by 8 are allowed. ' +
|
|
||||||
RSA._stance
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// TODO maybe allow this to be set to any of the standard values?
|
|
||||||
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
|
|
||||||
|
|
||||||
var extractable = true;
|
|
||||||
return window.crypto.subtle
|
|
||||||
.generateKey(wcOpts, extractable, ['sign', 'verify'])
|
|
||||||
.then(function(result) {
|
|
||||||
return window.crypto.subtle
|
|
||||||
.exportKey('jwk', result.privateKey)
|
|
||||||
.then(function(privJwk) {
|
|
||||||
return {
|
|
||||||
private: privJwk,
|
|
||||||
public: RSA.neuter({ jwk: privJwk })
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chopping off the private parts is now part of the public API.
|
|
||||||
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
|
|
||||||
RSA.neuter = function(opts) {
|
|
||||||
// trying to find the best balance of an immutable copy with custom attributes
|
|
||||||
var jwk = {};
|
|
||||||
Object.keys(opts.jwk).forEach(function(k) {
|
|
||||||
if ('undefined' === typeof opts.jwk[k]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ignore RSA private parts
|
|
||||||
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
|
|
||||||
});
|
|
||||||
return jwk;
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
|
||||||
RSA.__thumbprint = function(jwk) {
|
|
||||||
// Use the same entropy for SHA as for key
|
|
||||||
var len = Math.floor(jwk.n.length * 0.75);
|
|
||||||
var alg = 'SHA-256';
|
|
||||||
// TODO this may be a bug
|
|
||||||
// need to confirm that the padding is no more or less than 1 byte
|
|
||||||
if (len >= 511) {
|
|
||||||
alg = 'SHA-512';
|
|
||||||
} else if (len >= 383) {
|
|
||||||
alg = 'SHA-384';
|
|
||||||
}
|
|
||||||
return window.crypto.subtle
|
|
||||||
.digest(
|
|
||||||
{ name: alg },
|
|
||||||
textEncoder.encode(
|
|
||||||
'{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(function(hash) {
|
|
||||||
return Enc.bufToUrlBase64(new Uint8Array(hash));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
RSA.thumbprint = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
var jwk;
|
|
||||||
if ('EC' === opts.kty) {
|
|
||||||
jwk = opts;
|
|
||||||
} else if (opts.jwk) {
|
|
||||||
jwk = opts.jwk;
|
|
||||||
} else {
|
|
||||||
return RSA.import(opts).then(function(jwk) {
|
|
||||||
return RSA.__thumbprint(jwk);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return RSA.__thumbprint(jwk);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
RSA.export = function(opts) {
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
|
|
||||||
throw new Error('must pass { jwk: jwk }');
|
|
||||||
}
|
|
||||||
var jwk = JSON.parse(JSON.stringify(opts.jwk));
|
|
||||||
var format = opts.format;
|
|
||||||
var pub = opts.public;
|
|
||||||
if (
|
|
||||||
pub ||
|
|
||||||
-1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)
|
|
||||||
) {
|
|
||||||
jwk = RSA.neuter({ jwk: jwk });
|
|
||||||
}
|
|
||||||
if ('RSA' !== jwk.kty) {
|
|
||||||
throw new Error("options.jwk.kty must be 'RSA' for RSA keys");
|
|
||||||
}
|
|
||||||
if (!jwk.p) {
|
|
||||||
// TODO test for n and e
|
|
||||||
pub = true;
|
|
||||||
if (!format || 'pkcs1' === format) {
|
|
||||||
format = 'pkcs1';
|
|
||||||
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
|
|
||||||
format = 'spki';
|
|
||||||
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
|
|
||||||
format = 'ssh';
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"options.format must be 'spki', 'pkcs1', or 'ssh' for public RSA keys, not (" +
|
|
||||||
typeof format +
|
|
||||||
') ' +
|
|
||||||
format
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO test for all necessary keys (d, p, q ...)
|
|
||||||
if (!format || 'pkcs1' === format) {
|
|
||||||
format = 'pkcs1';
|
|
||||||
} else if ('pkcs8' !== format) {
|
|
||||||
throw new Error(
|
|
||||||
"options.format must be 'pkcs1' or 'pkcs8' for private RSA keys"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('pkcs1' === format) {
|
|
||||||
if (jwk.d) {
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'RSA PRIVATE KEY',
|
|
||||||
bytes: x509.packPkcs1(jwk)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'RSA PUBLIC KEY',
|
|
||||||
bytes: x509.packPkcs1(jwk)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if ('pkcs8' === format) {
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'PRIVATE KEY',
|
|
||||||
bytes: x509.packPkcs8(jwk)
|
|
||||||
});
|
|
||||||
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
|
|
||||||
return PEM.packBlock({
|
|
||||||
type: 'PUBLIC KEY',
|
|
||||||
bytes: x509.packSpki(jwk)
|
|
||||||
});
|
|
||||||
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
|
|
||||||
return SSH.pack({ jwk: jwk, comment: opts.comment });
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'Sanity Error: reached unreachable code block with format: ' +
|
|
||||||
format
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
RSA.pack = function(opts) {
|
|
||||||
// wrapped in a promise for API compatibility
|
|
||||||
// with the forthcoming browser version
|
|
||||||
// (and potential future native node capability)
|
|
||||||
return Promise.resolve().then(function() {
|
|
||||||
return RSA.export(opts);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToUrlBase64 = function(u8) {
|
|
||||||
return Enc.bufToBase64(u8)
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
Enc.bufToBase64 = function(u8) {
|
|
||||||
var bin = '';
|
|
||||||
u8.forEach(function(i) {
|
|
||||||
bin += String.fromCharCode(i);
|
|
||||||
});
|
|
||||||
return btoa(bin);
|
|
||||||
};
|
|
||||||
})('undefined' !== typeof module ? module.exports : window);
|
|
301
lib/x509.js
301
lib/x509.js
|
@ -1,301 +0,0 @@
|
||||||
(function(exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var x509 = (exports.x509 = {});
|
|
||||||
var ASN1 = exports.ASN1;
|
|
||||||
var Enc = exports.Enc;
|
|
||||||
|
|
||||||
// 1.2.840.10045.3.1.7
|
|
||||||
// prime256v1 (ANSI X9.62 named elliptic curve)
|
|
||||||
var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase();
|
|
||||||
// 1.3.132.0.34
|
|
||||||
// secp384r1 (SECG (Certicom) named elliptic curve)
|
|
||||||
var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase();
|
|
||||||
// 1.2.840.10045.2.1
|
|
||||||
// ecPublicKey (ANSI X9.62 public key type)
|
|
||||||
var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'
|
|
||||||
.replace(/\s+/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
x509.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) {
|
|
||||||
var index = 7;
|
|
||||||
var len = 32;
|
|
||||||
var olen = OBJ_ID_EC.length / 2;
|
|
||||||
|
|
||||||
if ('P-384' === jwk.crv) {
|
|
||||||
olen = OBJ_ID_EC_384.length / 2;
|
|
||||||
index = 8;
|
|
||||||
len = 48;
|
|
||||||
}
|
|
||||||
if (len !== u8[index - 1]) {
|
|
||||||
throw new Error('Unexpected bitlength ' + len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// private part is d
|
|
||||||
var d = u8.slice(index, index + len);
|
|
||||||
// compression bit index
|
|
||||||
var ci = index + len + 2 + olen + 2 + 3;
|
|
||||||
var c = u8[ci];
|
|
||||||
var x, y;
|
|
||||||
|
|
||||||
if (0x04 === c) {
|
|
||||||
y = u8.slice(ci + 1 + len, ci + 1 + len + len);
|
|
||||||
} else if (0x02 !== c) {
|
|
||||||
throw new Error('not a supported EC private key');
|
|
||||||
}
|
|
||||||
x = u8.slice(ci + 1, ci + 1 + len);
|
|
||||||
|
|
||||||
return {
|
|
||||||
kty: jwk.kty,
|
|
||||||
crv: jwk.crv,
|
|
||||||
d: Enc.bufToUrlBase64(d),
|
|
||||||
//, dh: Enc.bufToHex(d)
|
|
||||||
x: Enc.bufToUrlBase64(x),
|
|
||||||
//, xh: Enc.bufToHex(x)
|
|
||||||
y: Enc.bufToUrlBase64(y)
|
|
||||||
//, yh: Enc.bufToHex(y)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
x509.packPkcs1 = function(jwk) {
|
|
||||||
var n = ASN1.UInt(Enc.base64ToHex(jwk.n));
|
|
||||||
var e = ASN1.UInt(Enc.base64ToHex(jwk.e));
|
|
||||||
|
|
||||||
if (!jwk.d) {
|
|
||||||
return Enc.hexToBuf(ASN1('30', n, e));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('00'),
|
|
||||||
n,
|
|
||||||
e,
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.d)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.p)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.q)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.dp)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.dq)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.qi))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) {
|
|
||||||
var index = 24 + OBJ_ID_EC.length / 2;
|
|
||||||
var len = 32;
|
|
||||||
if ('P-384' === jwk.crv) {
|
|
||||||
index = 24 + OBJ_ID_EC_384.length / 2 + 2;
|
|
||||||
len = 48;
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log(index, u8.slice(index));
|
|
||||||
if (0x04 !== u8[index]) {
|
|
||||||
//console.log(jwk);
|
|
||||||
throw new Error('privkey not found');
|
|
||||||
}
|
|
||||||
var d = u8.slice(index + 2, index + 2 + len);
|
|
||||||
var ci = index + 2 + len + 5;
|
|
||||||
var xi = ci + 1;
|
|
||||||
var x = u8.slice(xi, xi + len);
|
|
||||||
var yi = xi + len;
|
|
||||||
var y;
|
|
||||||
if (0x04 === u8[ci]) {
|
|
||||||
y = u8.slice(yi, yi + len);
|
|
||||||
} else if (0x02 !== u8[ci]) {
|
|
||||||
throw new Error('invalid compression bit (expected 0x04 or 0x02)');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
kty: jwk.kty,
|
|
||||||
crv: jwk.crv,
|
|
||||||
d: Enc.bufToUrlBase64(d),
|
|
||||||
//, dh: Enc.bufToHex(d)
|
|
||||||
x: Enc.bufToUrlBase64(x),
|
|
||||||
//, xh: Enc.bufToHex(x)
|
|
||||||
y: Enc.bufToUrlBase64(y)
|
|
||||||
//, yh: Enc.bufToHex(y)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
x509.parseSpki = function parsePem(u8, jwk) {
|
|
||||||
var ci = 16 + OBJ_ID_EC.length / 2;
|
|
||||||
var len = 32;
|
|
||||||
|
|
||||||
if ('P-384' === jwk.crv) {
|
|
||||||
ci = 16 + OBJ_ID_EC_384.length / 2;
|
|
||||||
len = 48;
|
|
||||||
}
|
|
||||||
|
|
||||||
var c = u8[ci];
|
|
||||||
var xi = ci + 1;
|
|
||||||
var x = u8.slice(xi, xi + len);
|
|
||||||
var yi = xi + len;
|
|
||||||
var y;
|
|
||||||
if (0x04 === c) {
|
|
||||||
y = u8.slice(yi, yi + len);
|
|
||||||
} else if (0x02 !== c) {
|
|
||||||
throw new Error('not a supported EC private key');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
kty: jwk.kty,
|
|
||||||
crv: jwk.crv,
|
|
||||||
x: Enc.bufToUrlBase64(x),
|
|
||||||
//, xh: Enc.bufToHex(x)
|
|
||||||
y: Enc.bufToUrlBase64(y)
|
|
||||||
//, yh: Enc.bufToHex(y)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
x509.parsePkix = x509.parseSpki;
|
|
||||||
|
|
||||||
x509.packSec1 = function(jwk) {
|
|
||||||
var d = Enc.base64ToHex(jwk.d);
|
|
||||||
var x = Enc.base64ToHex(jwk.x);
|
|
||||||
var y = Enc.base64ToHex(jwk.y);
|
|
||||||
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('01'),
|
|
||||||
ASN1('04', d),
|
|
||||||
ASN1('A0', objId),
|
|
||||||
ASN1('A1', ASN1.BitStr('04' + x + y))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* take a private jwk and creates a der from it
|
|
||||||
* @param {*} jwk
|
|
||||||
*/
|
|
||||||
x509.packPkcs8 = function(jwk) {
|
|
||||||
if ('RSA' === jwk.kty) {
|
|
||||||
if (!jwk.d) {
|
|
||||||
// Public RSA
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1('06', '2a864886f70d010101'),
|
|
||||||
ASN1('05')
|
|
||||||
),
|
|
||||||
ASN1.BitStr(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.n)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.e))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private RSA
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('00'),
|
|
||||||
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')),
|
|
||||||
ASN1(
|
|
||||||
'04',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('00'),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.n)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.e)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.d)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.p)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.q)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.dp)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.dq)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.qi))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var d = Enc.base64ToHex(jwk.d);
|
|
||||||
var x = Enc.base64ToHex(jwk.x);
|
|
||||||
var y = Enc.base64ToHex(jwk.y);
|
|
||||||
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('00'),
|
|
||||||
ASN1('30', OBJ_ID_EC_PUB, objId),
|
|
||||||
ASN1(
|
|
||||||
'04',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('01'),
|
|
||||||
ASN1('04', d),
|
|
||||||
ASN1('A1', ASN1.BitStr('04' + x + y))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
x509.packSpki = function(jwk) {
|
|
||||||
if (/EC/i.test(jwk.kty)) {
|
|
||||||
return x509.packSpkiEc(jwk);
|
|
||||||
}
|
|
||||||
return x509.packSpkiRsa(jwk);
|
|
||||||
};
|
|
||||||
x509.packSpkiRsa = function(jwk) {
|
|
||||||
if (!jwk.d) {
|
|
||||||
// Public RSA
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')),
|
|
||||||
ASN1.BitStr(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.n)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.e))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private RSA
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('00'),
|
|
||||||
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')),
|
|
||||||
ASN1(
|
|
||||||
'04',
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1.UInt('00'),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.n)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.e)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.d)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.p)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.q)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.dp)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.dq)),
|
|
||||||
ASN1.UInt(Enc.base64ToHex(jwk.qi))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
x509.packSpkiEc = function(jwk) {
|
|
||||||
var x = Enc.base64ToHex(jwk.x);
|
|
||||||
var y = Enc.base64ToHex(jwk.y);
|
|
||||||
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
|
|
||||||
return Enc.hexToBuf(
|
|
||||||
ASN1(
|
|
||||||
'30',
|
|
||||||
ASN1('30', OBJ_ID_EC_PUB, objId),
|
|
||||||
ASN1.BitStr('04' + x + y)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
x509.packPkix = x509.packSpki;
|
|
||||||
})('undefined' !== typeof module ? module.exports : window);
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -1,30 +1,60 @@
|
||||||
{
|
{
|
||||||
"name": "acme",
|
"name": "@root/acme",
|
||||||
"version": "2.0.0-wip.0",
|
"version": "3.1.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@root/request": {
|
"@root/asn1": {
|
||||||
"version": "1.3.10",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
|
||||||
"integrity": "sha512-GSn8dfsGp0juJyXS9k7B/DjYm7Axe85wiCHfPs30eQ+/V6p2aqey45e1czb3ZwP+iPmzWCKXahhWnZhSDIil6w==",
|
"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"accepts": {
|
|
||||||
"version": "1.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.6.tgz",
|
|
||||||
"integrity": "sha512-QsaoUD2dpVpjENy8JFpQnXP9vyzoZPmAoKrE3S6HtSB7qzSebkJNnmdY4p004FQUSSiHXPueENpoeuUW/7a8Ig==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"mime-types": "~2.1.24",
|
"@root/encoding": "^1.0.1"
|
||||||
"negotiator": "0.6.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"array-flatten": {
|
"@root/csr": {
|
||||||
"version": "1.1.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
|
||||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
|
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
|
||||||
"dev": true
|
"requires": {
|
||||||
|
"@root/asn1": "^1.0.0",
|
||||||
|
"@root/pem": "^1.0.4",
|
||||||
|
"@root/x509": "^0.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@root/encoding": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
|
||||||
|
},
|
||||||
|
"@root/keypairs": {
|
||||||
|
"version": "0.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",
|
||||||
|
"@root/x509": "^0.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@root/pem": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
|
||||||
|
},
|
||||||
|
"@root/request": {
|
||||||
|
"version": "1.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",
|
||||||
|
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==",
|
||||||
|
"requires": {
|
||||||
|
"@root/asn1": "^1.0.0",
|
||||||
|
"@root/encoding": "^1.0.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
@ -33,29 +63,11 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"bluebird": {
|
"bluebird": {
|
||||||
"version": "3.5.4",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz",
|
||||||
"integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==",
|
"integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"body-parser": {
|
|
||||||
"version": "1.18.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
|
|
||||||
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"bytes": "3.0.0",
|
|
||||||
"content-type": "~1.0.4",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "~1.1.2",
|
|
||||||
"http-errors": "~1.6.3",
|
|
||||||
"iconv-lite": "0.4.23",
|
|
||||||
"on-finished": "~2.3.0",
|
|
||||||
"qs": "6.5.2",
|
|
||||||
"raw-body": "2.3.3",
|
|
||||||
"type-is": "~1.6.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
@ -66,12 +78,6 @@
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bytes": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"cli": {
|
"cli": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz",
|
||||||
|
@ -82,63 +88,12 @@
|
||||||
"glob": "^7.1.1"
|
"glob": "^7.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commander": {
|
|
||||||
"version": "2.20.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz",
|
|
||||||
"integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"content-disposition": {
|
|
||||||
"version": "0.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
|
|
||||||
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"content-type": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"cookie": {
|
|
||||||
"version": "0.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
|
||||||
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"cookie-signature": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
|
||||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"version": "2.6.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"ms": "2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"depd": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
|
||||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"destroy": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
|
||||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"dig.js": {
|
"dig.js": {
|
||||||
"version": "1.3.9",
|
"version": "1.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/dig.js/-/dig.js-1.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/dig.js/-/dig.js-1.3.9.tgz",
|
||||||
|
@ -162,37 +117,26 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dns-suite": {
|
"dns-suite": {
|
||||||
"version": "1.2.12",
|
"version": "1.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/dns-suite/-/dns-suite-1.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/dns-suite/-/dns-suite-1.2.13.tgz",
|
||||||
"integrity": "sha512-K4LWqmJT/T2QLaGCJ+qRvrT9DicKs5XxXYXw6uIZ1apdwyfToQk7K9AZbpFd0FLRdZG809v2vAcsquPbQh+Ipg==",
|
"integrity": "sha512-veYKPHUc2RfRCe7c4G/iKxhRv0S4InJ3JsW8tEhW6Yb7dn3ac34iozC6cNX0uzHYZUw0BG5V9Fu65L1bx1GeBg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bluebird": "^3.5.0",
|
"@root/hexdump": "^1.1.1"
|
||||||
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@root/hexdump": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@root/hexdump/-/hexdump-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-AmrmLOutlzctR599ittO06lINOco1TIqb0c1wu83fP2Eoi5iSvx7kVWC4mDufze8rxPewC+aQOx4e6Pw7izV4A==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ee-first": {
|
"dotenv": {
|
||||||
"version": "1.1.1",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
|
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"encodeurl": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"escape-html": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
|
||||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"etag": {
|
|
||||||
"version": "1.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
|
||||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
|
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"exit": {
|
"exit": {
|
||||||
|
@ -201,71 +145,6 @@
|
||||||
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
|
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"express": {
|
|
||||||
"version": "4.16.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
|
|
||||||
"integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"accepts": "~1.3.5",
|
|
||||||
"array-flatten": "1.1.1",
|
|
||||||
"body-parser": "1.18.3",
|
|
||||||
"content-disposition": "0.5.2",
|
|
||||||
"content-type": "~1.0.4",
|
|
||||||
"cookie": "0.3.1",
|
|
||||||
"cookie-signature": "1.0.6",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "~1.1.2",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"etag": "~1.8.1",
|
|
||||||
"finalhandler": "1.1.1",
|
|
||||||
"fresh": "0.5.2",
|
|
||||||
"merge-descriptors": "1.0.1",
|
|
||||||
"methods": "~1.1.2",
|
|
||||||
"on-finished": "~2.3.0",
|
|
||||||
"parseurl": "~1.3.2",
|
|
||||||
"path-to-regexp": "0.1.7",
|
|
||||||
"proxy-addr": "~2.0.4",
|
|
||||||
"qs": "6.5.2",
|
|
||||||
"range-parser": "~1.2.0",
|
|
||||||
"safe-buffer": "5.1.2",
|
|
||||||
"send": "0.16.2",
|
|
||||||
"serve-static": "1.13.2",
|
|
||||||
"setprototypeof": "1.1.0",
|
|
||||||
"statuses": "~1.4.0",
|
|
||||||
"type-is": "~1.6.16",
|
|
||||||
"utils-merge": "1.0.1",
|
|
||||||
"vary": "~1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"finalhandler": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"on-finished": "~2.3.0",
|
|
||||||
"parseurl": "~1.3.2",
|
|
||||||
"statuses": "~1.4.0",
|
|
||||||
"unpipe": "~1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forwarded": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
|
||||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"fresh": {
|
|
||||||
"version": "0.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
|
||||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
@ -273,9 +152,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "^1.0.0",
|
"fs.realpath": "^1.0.0",
|
||||||
|
@ -291,27 +170,6 @@
|
||||||
"from": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4",
|
"from": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"http-errors": {
|
|
||||||
"version": "1.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
|
|
||||||
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"depd": "~1.1.2",
|
|
||||||
"inherits": "2.0.3",
|
|
||||||
"setprototypeof": "1.1.0",
|
|
||||||
"statuses": ">= 1.4.0 < 2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"iconv-lite": {
|
|
||||||
"version": "0.4.23",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
|
|
||||||
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inflight": {
|
"inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
@ -323,56 +181,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ipaddr.js": {
|
|
||||||
"version": "1.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
|
|
||||||
"integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"media-typer": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
|
||||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"merge-descriptors": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"methods": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
|
||||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"mime": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"mime-db": {
|
|
||||||
"version": "1.40.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
|
||||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"mime-types": {
|
|
||||||
"version": "2.1.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
|
||||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"mime-db": "1.40.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
@ -382,27 +195,6 @@
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"negotiator": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
|
||||||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"on-finished": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
|
||||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"ee-first": "1.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"once": {
|
"once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
@ -412,157 +204,16 @@
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"parseurl": {
|
|
||||||
"version": "1.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
|
||||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"path-is-absolute": {
|
"path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-to-regexp": {
|
"punycode": {
|
||||||
"version": "0.1.7",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
|
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"proxy-addr": {
|
|
||||||
"version": "2.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
|
|
||||||
"integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"forwarded": "~0.1.2",
|
|
||||||
"ipaddr.js": "1.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"qs": {
|
|
||||||
"version": "6.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
|
||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"range-parser": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
|
||||||
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"raw-body": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"bytes": "3.0.0",
|
|
||||||
"http-errors": "1.6.3",
|
|
||||||
"iconv-lite": "0.4.23",
|
|
||||||
"unpipe": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"send": {
|
|
||||||
"version": "0.16.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
|
|
||||||
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "~1.1.2",
|
|
||||||
"destroy": "~1.0.4",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"etag": "~1.8.1",
|
|
||||||
"fresh": "0.5.2",
|
|
||||||
"http-errors": "~1.6.2",
|
|
||||||
"mime": "1.4.1",
|
|
||||||
"ms": "2.0.0",
|
|
||||||
"on-finished": "~2.3.0",
|
|
||||||
"range-parser": "~1.2.0",
|
|
||||||
"statuses": "~1.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"serve-static": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"parseurl": "~1.3.2",
|
|
||||||
"send": "0.16.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"setprototypeof": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"source-map": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"statuses": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"type-is": {
|
|
||||||
"version": "1.6.18",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
|
||||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"media-typer": "0.3.0",
|
|
||||||
"mime-types": "~2.1.24"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uglify-js": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"commander": "~2.20.0",
|
|
||||||
"source-map": "~0.6.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"unpipe": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"utils-merge": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"vary": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
|
||||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
|
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
|
|
45
package.json
45
package.json
|
@ -1,44 +1,63 @@
|
||||||
{
|
{
|
||||||
"name": "acme",
|
"name": "@root/acme",
|
||||||
"version": "2.0.0-wip.0",
|
"version": "3.1.0",
|
||||||
"description": "Free SSL certificates through Let's Encrypt, right in your browser",
|
"description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt",
|
||||||
"main": "bluecrypt-acme.js",
|
|
||||||
"homepage": "https://rootprojects.org/acme/",
|
"homepage": "https://rootprojects.org/acme/",
|
||||||
"directories": {
|
"main": "acme.js",
|
||||||
"lib": "lib"
|
"browser": {
|
||||||
|
"./lib/native.js": "./lib/browser.js",
|
||||||
|
"./lib/node/sha2.js": "./lib/browser/sha2.js",
|
||||||
|
"./lib/node/http.js": "./lib/browser/http.js",
|
||||||
|
"./lib/node/client-user-agent.js": "./lib/browser/client-user-agent.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
"*.js",
|
||||||
"lib",
|
"lib",
|
||||||
|
"bin",
|
||||||
|
"scripts",
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node bin/bundle.js",
|
"build": "node_xxx bin/bundle.js",
|
||||||
"lint": "jshint lib bin",
|
"lint": "jshint lib bin",
|
||||||
|
"postinstall": "node scripts/postinstall",
|
||||||
"test": "node server.js",
|
"test": "node server.js",
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.coolaj86.com/coolaj86/bluecrypt-acme.js.git"
|
"url": "https://git.rootprojects.org/root/acme.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ACME",
|
"ACME",
|
||||||
"Let's Encrypt",
|
"Let's Encrypt",
|
||||||
"browser",
|
|
||||||
"EC",
|
"EC",
|
||||||
"RSA",
|
"RSA",
|
||||||
"CSR",
|
"CSR",
|
||||||
|
"browser",
|
||||||
"greenlock",
|
"greenlock",
|
||||||
"VanillaJS",
|
"VanillaJS",
|
||||||
"ZeroSSL"
|
"ZeroSSL"
|
||||||
],
|
],
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@root/csr": "^0.8.1",
|
||||||
|
"@root/encoding": "^1.0.1",
|
||||||
|
"@root/keypairs": "^0.10.0",
|
||||||
|
"@root/pem": "^1.0.4",
|
||||||
|
"@root/request": "^1.6.1",
|
||||||
|
"@root/x509": "^0.7.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@root/request": "^1.3.10",
|
|
||||||
"dig.js": "^1.3.9",
|
"dig.js": "^1.3.9",
|
||||||
"dns-suite": "^1.2.12",
|
"dns-suite": "^1.2.13",
|
||||||
"express": "^4.16.4",
|
"dotenv": "^8.1.0",
|
||||||
"uglify-js": "^3.6.0"
|
"punycode": "^1.4.1"
|
||||||
|
},
|
||||||
|
"trulyOptionalDependencies": {
|
||||||
|
"eslint": "^6.5.1",
|
||||||
|
"webpack": "^4.41.0",
|
||||||
|
"webpack-cli": "^3.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// TODO put postinstall message
|
139
server.js
139
server.js
|
@ -1,139 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var crypto = require('crypto');
|
|
||||||
//var dnsjs = require('dns-suite');
|
|
||||||
var dig = require('dig.js/dns-request');
|
|
||||||
var request = require('util').promisify(require('@root/request'));
|
|
||||||
var express = require('express');
|
|
||||||
var app = express();
|
|
||||||
|
|
||||||
var nameservers = require('dns').getServers();
|
|
||||||
var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length;
|
|
||||||
var nameserver = nameservers[index];
|
|
||||||
|
|
||||||
app.use('/', express.static(__dirname));
|
|
||||||
app.use('/api', express.json());
|
|
||||||
app.get('/api/dns/:domain', function (req, res, next) {
|
|
||||||
var domain = req.params.domain;
|
|
||||||
var casedDomain = domain.toLowerCase().split('').map(function (ch) {
|
|
||||||
// dns0x20 takes advantage of the fact that the binary operation for toUpperCase is
|
|
||||||
// ch = ch | 0x20;
|
|
||||||
return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase();
|
|
||||||
}).join('');
|
|
||||||
var typ = req.query.type;
|
|
||||||
var query = {
|
|
||||||
header: {
|
|
||||||
id: crypto.randomBytes(2).readUInt16BE(0)
|
|
||||||
, qr: 0
|
|
||||||
, opcode: 0
|
|
||||||
, aa: 0 // Authoritative-Only
|
|
||||||
, tc: 0 // NA
|
|
||||||
, rd: 1 // Recurse
|
|
||||||
, ra: 0 // NA
|
|
||||||
, rcode: 0 // NA
|
|
||||||
}
|
|
||||||
, question: [
|
|
||||||
{ name: casedDomain
|
|
||||||
//, type: typ || 'A'
|
|
||||||
, typeName: typ || 'A'
|
|
||||||
, className: 'IN'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
var opts = {
|
|
||||||
onError: function (err) {
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
, onMessage: function (packet) {
|
|
||||||
var fail0x20;
|
|
||||||
|
|
||||||
if (packet.id !== query.id) {
|
|
||||||
console.error('[SECURITY] ignoring packet for \'' + packet.question[0].name + '\' due to mismatched id');
|
|
||||||
console.error(packet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
packet.question.forEach(function (q) {
|
|
||||||
// if (-1 === q.name.lastIndexOf(cli.casedQuery))
|
|
||||||
if (q.name !== casedDomain) {
|
|
||||||
fail0x20 = q.name;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
[ 'question', 'answer', 'authority', 'additional' ].forEach(function (group) {
|
|
||||||
(packet[group]||[]).forEach(function (a) {
|
|
||||||
var an = a.name;
|
|
||||||
var i = domain.toLowerCase().lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM
|
|
||||||
var j = a.name.toLowerCase().lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM
|
|
||||||
|
|
||||||
// it's important to note that these should only relpace changes in casing that we expected
|
|
||||||
// any abnormalities should be left intact to go "huh?" about
|
|
||||||
// TODO detect abnormalities?
|
|
||||||
if (-1 !== i) {
|
|
||||||
// "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4))
|
|
||||||
a.name = a.name.replace(casedDomain.substr(i), domain.substr(i));
|
|
||||||
} else if (-1 !== j) {
|
|
||||||
// "www.example.com".replace("EXamPLE.cOm", "example.com")
|
|
||||||
a.name = a.name.substr(0, j) + a.name.substr(j).replace(casedDomain, domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: right now this assumes that anything matching the query matches all the way to the end
|
|
||||||
// it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly
|
|
||||||
// (but I don't think it should need to)
|
|
||||||
if (a.name.length !== an.length) {
|
|
||||||
console.error("[ERROR] question / answer mismatch: '" + an + "' != '" + a.length + "'");
|
|
||||||
console.error(a);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fail0x20) {
|
|
||||||
console.warn(";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '"
|
|
||||||
+ casedDomain + "' but got response for '" + fail0x20 + "'.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
header: packet.header
|
|
||||||
, question: packet.question
|
|
||||||
, answer: packet.answer
|
|
||||||
, authority: packet.authority
|
|
||||||
, additional: packet.additional
|
|
||||||
, edns_options: packet.edns_options
|
|
||||||
});
|
|
||||||
}
|
|
||||||
, onListening: function () {}
|
|
||||||
, onSent: function (/*res*/) { }
|
|
||||||
, onTimeout: function (res) {
|
|
||||||
console.error('dns timeout:', res);
|
|
||||||
next(new Error("DNS timeout - no response"));
|
|
||||||
}
|
|
||||||
, onClose: function () { }
|
|
||||||
//, mdns: cli.mdns
|
|
||||||
, nameserver: nameserver
|
|
||||||
, port: 53
|
|
||||||
, timeout: 2000
|
|
||||||
};
|
|
||||||
|
|
||||||
dig.resolveJson(query, opts);
|
|
||||||
});
|
|
||||||
app.get('/api/http', function (req, res) {
|
|
||||||
var url = req.query.url;
|
|
||||||
return request({ method: 'GET', url: url }).then(function (resp) {
|
|
||||||
res.send(resp.body);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
app.get('/api/_acme_api_', function (req, res) {
|
|
||||||
res.send({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = app;
|
|
||||||
if (require.main === module) {
|
|
||||||
// curl -L http://localhost:3000/api/dns/example.com?type=A
|
|
||||||
console.info("Listening on localhost:3000");
|
|
||||||
app.listen(3000);
|
|
||||||
console.info("Try this:");
|
|
||||||
console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'");
|
|
||||||
console.info("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'");
|
|
||||||
console.info("\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'");
|
|
||||||
}
|
|
|
@ -0,0 +1,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);
|
||||||
|
});
|
||||||
|
};
|
|
@ -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();
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
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);
|
||||||
|
var pem = await Keypairs.export({
|
||||||
|
jwk: certKeypair.private,
|
||||||
|
encoding: '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');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
module.exports();
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
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')();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
|
@ -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) + '例'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './examples/app.js',
|
||||||
|
//entry: './acme.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'app.js'
|
||||||
|
//filename: 'acme.js',
|
||||||
|
//library: '@root/acme',
|
||||||
|
//libraryTarget: 'umd'
|
||||||
|
//globalObject: "typeof self !== 'undefined' ? self : this"
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
aliasFields: ['webpack', 'browser'],
|
||||||
|
mainFields: ['browser', 'main']
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue