Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fd9678ab5 | |||
|
|
7a6c2ae573 | ||
|
|
4758dc2bd2 | ||
| d28d82130c | |||
|
|
3a41c3006c | ||
| bfe1737b9b | |||
|
|
9172d4c98e | ||
|
|
530b25f691 | ||
|
|
d85f4070f3 | ||
|
|
51bcc1f20a | ||
|
|
f79c62032c | ||
|
|
3ed2d45d3d | ||
|
|
d0e20a44cd | ||
|
|
10978ab99a | ||
|
|
72fb7b7c07 | ||
|
|
72646ced80 | ||
|
|
9f01021948 | ||
|
|
f63070ce54 | ||
|
|
6126222e8f | ||
|
|
7288d14fac | ||
|
|
681c0edc71 | ||
|
|
f350ae44c1 | ||
|
|
6b1b168e5a | ||
|
|
a97c5933d6 | ||
|
|
a8b9817415 | ||
|
|
fe635a965c | ||
|
|
8436b615cb | ||
|
|
fbaa77cb4c | ||
|
|
528cec03a8 | ||
|
|
e3d4add0b9 | ||
|
|
218497ab0e | ||
|
|
764c614940 | ||
|
|
80613b98e2 | ||
|
|
4050bd2a82 | ||
|
|
17df564f69 | ||
|
|
60e4ed8f7b | ||
|
|
420351da62 | ||
|
|
903ebf0491 | ||
|
|
4c7c21a751 | ||
|
|
01f283b7fd | ||
|
|
c1513fe120 | ||
|
|
4e5d373055 | ||
|
|
572621086e | ||
|
|
b980fde859 | ||
|
|
4ded1088c8 | ||
|
|
5d970344db | ||
|
|
82a456f7c8 | ||
|
|
e7a36123ca | ||
|
|
b5b516a131 | ||
|
|
761202ae8e | ||
|
|
2f607314b9 | ||
|
|
e8acedd6d7 | ||
|
|
36acd623ca | ||
|
|
8e6d31436c | ||
|
|
e52f043cfe | ||
|
|
3bab5857dd | ||
|
|
ee48f4a477 | ||
|
|
2c7b7e1bcf | ||
|
|
7504268047 | ||
|
|
55707a417d | ||
|
|
e0909ad0ca | ||
|
|
526e19b23e | ||
|
|
764096a0b2 | ||
|
|
44fb789543 | ||
|
|
4c2e638aed | ||
|
|
ad71d8bb97 | ||
|
|
acb400ef7f | ||
|
|
3babc27847 | ||
|
|
8ec66c2a1f | ||
|
|
6a8592994e | ||
|
|
e0202a559e | ||
|
|
7eace7e37e | ||
|
|
1b443f9274 | ||
|
|
21bf389c73 | ||
|
|
d4d57b96f7 | ||
|
|
a875f551e2 | ||
|
|
aa6119378a | ||
|
|
89c688c955 | ||
|
|
4a11207dcd | ||
|
|
f322085452 | ||
|
|
c92a9f1864 | ||
|
|
78c9daa059 | ||
|
|
57eab66a4d | ||
|
|
d9b5aa5a28 | ||
|
|
abf436b2bb | ||
|
|
1d3ecd8fcf | ||
|
|
77286c8c26 | ||
|
|
b07f079360 | ||
|
|
4f74fd90c9 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -29,3 +29,5 @@ build/Release
|
|||||||
# Dependency directory
|
# Dependency directory
|
||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
node_modules
|
node_modules
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
|||||||
2
AUTHORS
2
AUTHORS
@ -1,3 +1,3 @@
|
|||||||
ISRG
|
ISRG
|
||||||
Anatol Sommer <anatol@anatol.at>
|
Anatol Sommer <anatol@anatol.at>
|
||||||
AJ ONeal <aj@daplie.com> (https://daplie.com/)
|
AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)
|
||||||
|
|||||||
268
README.md
268
README.md
@ -1,4 +1,8 @@
|
|||||||
# letiny-core
|
# le-acme-core
|
||||||
|
|
||||||
|
Looking for **letiny-core**? Check the [v1.x branch](https://git.coolaj86.com/coolaj86/le-acme-core.js/tree/v1.x).
|
||||||
|
|
||||||
|
<!-- rename to le-acme-core -->
|
||||||
|
|
||||||
A framework for building letsencrypt clients, forked from `letiny`.
|
A framework for building letsencrypt clients, forked from `letiny`.
|
||||||
|
|
||||||
@ -9,19 +13,44 @@ Supports all of:
|
|||||||
* browser WebCrypto (not implemented, but... Let's Encrypt over WebRTC anyone?)
|
* browser WebCrypto (not implemented, but... Let's Encrypt over WebRTC anyone?)
|
||||||
* any javascript implementation
|
* any javascript implementation
|
||||||
|
|
||||||
|
# NEW: Let's Encrypt v2 Support
|
||||||
|
Let's Encrypt v2 (aka ACME v2 or ACME draft 11) is available in [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js)
|
||||||
|
|
||||||
### These aren't the droids you're looking for
|
### These aren't the droids you're looking for
|
||||||
|
|
||||||
This is a library / framework for building letsencrypt clients.
|
This is a library / framework for building letsencrypt clients.
|
||||||
You probably want one of these pre-built clients instead:
|
You probably want one of these pre-built clients instead:
|
||||||
|
|
||||||
* `letsencrypt` (100% compatible with the official client)
|
* [`letsencrypt`](https://git.coolaj86.com/coolaj86/greenlock.js) (compatible with the official client)
|
||||||
* `letiny` (lightweight client)
|
* `letiny` (lightweight client cli)
|
||||||
* `letsencrypt-express` (automatic https for express)
|
* [`letsencrypt-express`](https://git.coolaj86.com/coolaj86/greenlock-express.js) (automatic https for express)
|
||||||
|
|
||||||
## Usage:
|
## Install & Usage:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --save letiny-core
|
npm install --save le-acme-core
|
||||||
|
```
|
||||||
|
|
||||||
|
To use the default dependencies:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ACME = require('le-acme-core').ACME.create();
|
||||||
|
```
|
||||||
|
|
||||||
|
For **testing** and **development**, you can also inject the dependencies you want to use:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ACME = require('le-acme-core').ACME.create({
|
||||||
|
, RSA: require('rsa-compat').RSA
|
||||||
|
});
|
||||||
|
|
||||||
|
ACME.getAcmeUrls(discoveryUrl, function (err, urls) {
|
||||||
|
console.log(urls);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
You will follow these steps to obtain certificates:
|
You will follow these steps to obtain certificates:
|
||||||
@ -34,39 +63,133 @@ You will follow these steps to obtain certificates:
|
|||||||
* implement a method to get the challenge token as `getChallenge`
|
* implement a method to get the challenge token as `getChallenge`
|
||||||
* implement a method to remove the challenge token as `removeChallenge`
|
* implement a method to remove the challenge token as `removeChallenge`
|
||||||
|
|
||||||
|
### Demo
|
||||||
|
|
||||||
|
You can see this working for yourself, but you'll need to be on an internet connected computer with a domain.
|
||||||
|
|
||||||
|
Get a temporary domain for testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g ddns-cli
|
||||||
|
ddns --random --email user@example.com --agree
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: use **YOUR EMAIL** and accept the terms of service (run `ddns --help` to see them).
|
||||||
|
|
||||||
|
<!-- TODO tutorial on ddns -->
|
||||||
|
|
||||||
|
Install le-acme-core and its dependencies. **Note**: it's okay if you're on windows
|
||||||
|
and `ursa` fails to compile. It'll still work.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.coolaj86.com/coolaj86/le-acme-core.js.git ~/le-acme-core
|
||||||
|
pushd ~/le-acme-core
|
||||||
|
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the demo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node examples/letsencrypt.js user@example.com example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: use **YOUR TEMPORARY DOMAIN** and **YOUR EMAIL**.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The Goodies
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Accounts
|
||||||
|
ACME.registerNewAccount(options, cb) // returns "regr" registration data
|
||||||
|
|
||||||
|
{ newRegUrl: '<url>' // no defaults, specify acmeUrls.newAuthz
|
||||||
|
, email: '<email>' // valid email (server checks MX records)
|
||||||
|
, accountKeypair: { // privateKeyPem or privateKeyJwt
|
||||||
|
privateKeyPem: '<ASCII PEM>'
|
||||||
|
}
|
||||||
|
, agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration
|
||||||
|
ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key), cert, chain (ca) })
|
||||||
|
|
||||||
|
{ newAuthzUrl: '<url>' // specify acmeUrls.newAuthz
|
||||||
|
, newCertUrl: '<url>' // specify acmeUrls.newCert
|
||||||
|
|
||||||
|
, domainKeypair: {
|
||||||
|
privateKeyPem: '<ASCII PEM>'
|
||||||
|
}
|
||||||
|
, accountKeypair: {
|
||||||
|
privateKeyPem: '<ASCII PEM>'
|
||||||
|
}
|
||||||
|
, domains: ['example.com']
|
||||||
|
|
||||||
|
, setChallenge: fn (hostname, key, val, cb)
|
||||||
|
, removeChallenge: fn (hostname, key, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovery URLs
|
||||||
|
ACME.getAcmeUrls(acmeDiscoveryUrl, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert})
|
||||||
|
```
|
||||||
|
|
||||||
|
Helpers & Stuff
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Constants
|
||||||
|
ACME.productionServerUrl // https://acme-v01.api.letsencrypt.org/directory
|
||||||
|
ACME.stagingServerUrl // https://acme-staging.api.letsencrypt.org/directory
|
||||||
|
ACME.acmeChallengePrefix // /.well-known/acme-challenge/
|
||||||
|
ACME.knownEndpoints // new-authz, new-cert, new-reg, revoke-cert
|
||||||
|
|
||||||
|
|
||||||
|
// HTTP Client Helpers
|
||||||
|
ACME.Acme // Signs requests with JWK
|
||||||
|
acme = new Acme(keypair) // 'keypair' is an object with `privateKeyPem` and/or `privateKeyJwk`
|
||||||
|
acme.post(url, body, cb) // POST with signature
|
||||||
|
acme.parseLinks(link) // (internal) parses 'link' header
|
||||||
|
acme.getNonce(url, cb) // (internal) HEAD request to get 'replay-nonce' strings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Below you'll find a stripped-down example. You can see the full example in the example folder.
|
||||||
|
|
||||||
|
* [example/](https://git.coolaj86.com/coolaj86/le-acme-core.js/blob/master/example/)
|
||||||
|
|
||||||
#### Register Account & Domain
|
#### Register Account & Domain
|
||||||
|
|
||||||
**But wait**, there's more!
|
This is how you **register an ACME account** and **get an HTTPS certificate**
|
||||||
See [example/letsencrypt.js](https://github.com/Daplie/letiny-core/blob/master/example/letsencrypt.js)
|
|
||||||
|
|
||||||
See
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var LeCore = require('letiny-core');
|
var ACME = require('le-acme-core').ACME.create();
|
||||||
|
var RSA = require('rsa-compat').RSA;
|
||||||
|
|
||||||
var email = 'user@example.com'; // CHANGE TO YOUR EMAIL
|
var email = 'user@example.com'; // CHANGE TO YOUR EMAIL
|
||||||
var domains = 'example.com'; // CHANGE TO YOUR DOMAIN
|
var domains = 'example.com'; // CHANGE TO YOUR DOMAIN
|
||||||
var acmeDiscoveryUrl = LeCore.stagingServerUrl; // CHANGE to production, when ready
|
var acmeDiscoveryUrl = ACME.stagingServerUrl; // CHANGE to production, when ready
|
||||||
|
|
||||||
var accountPrivateKeyPem = null;
|
var accountKeypair = null; // { privateKeyPem: null, privateKeyJwk: null };
|
||||||
var domainPrivateKeyPem = null;
|
var domainKeypair = null; // same as above
|
||||||
var acmeUrls = null;
|
var acmeUrls = null;
|
||||||
|
|
||||||
LeCore.leCrypto.generateRsaKeypair(2048, 65537, function (err, pems) {
|
RSA.generateKeypair(2048, 65537, function (err, keypair) {
|
||||||
|
accountKeypair = keypair;
|
||||||
// ...
|
// ...
|
||||||
LeCore.getAcmeUrls(acmeDiscoveryUrl, function (err, urls) {
|
ACME.getAcmeUrls(acmeDiscoveryUrl, function (err, urls) {
|
||||||
// ...
|
// ...
|
||||||
runDemo();
|
runDemo();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function runDemo() {
|
function runDemo() {
|
||||||
LeCore.registerNewAccount(
|
ACME.registerNewAccount(
|
||||||
{ newRegUrl: acmeUrls.newReg
|
{ newRegUrl: acmeUrls.newReg
|
||||||
, email: email
|
, email: email
|
||||||
, accountPrivateKeyPem: accountPrivateKeyPem
|
, accountKeypair: accountKeypair
|
||||||
, agreeToTerms: function (tosUrl, done) {
|
, agreeToTerms: function (tosUrl, done) {
|
||||||
|
|
||||||
// agree to the exact version of these terms
|
// agree to the exact version of these terms
|
||||||
@ -75,13 +198,12 @@ function runDemo() {
|
|||||||
}
|
}
|
||||||
, function (err, regr) {
|
, function (err, regr) {
|
||||||
|
|
||||||
console.log('Registering New Certificate');
|
ACME.getCertificate(
|
||||||
LeCore.getCertificate(
|
|
||||||
{ newAuthzUrl: acmeUrls.newAuthz
|
{ newAuthzUrl: acmeUrls.newAuthz
|
||||||
, newCertUrl: acmeUrls.newCert
|
, newCertUrl: acmeUrls.newCert
|
||||||
|
|
||||||
, domainPrivateKeyPem: domainPrivateKeyPem
|
, domainKeypair: domainKeypair
|
||||||
, accountPrivateKeyPem: accountPrivateKeyPem
|
, accountKeypair: accountKeypair
|
||||||
, domains: domains
|
, domains: domains
|
||||||
|
|
||||||
, setChallenge: challengeStore.set
|
, setChallenge: challengeStore.set
|
||||||
@ -103,21 +225,21 @@ function runDemo() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**But wait**, there's more!
|
||||||
|
See [example/letsencrypt.js](https://git.coolaj86.com/coolaj86/le-acme-core.js/blob/master/example/letsencrypt.js)
|
||||||
|
|
||||||
#### Run a Server on 80, 443, and 5001 (https/tls)
|
#### Run a Server on 80, 443, and 5001 (https/tls)
|
||||||
|
|
||||||
That will fail unless you have a webserver running on 80 and 443 (or 5001)
|
That will fail unless you have a webserver running on 80 and 443 (or 5001)
|
||||||
to respond to `/.well-known/acme-challenge/xxxxxxxx` with the proper token
|
to respond to `/.well-known/acme-challenge/xxxxxxxx` with the proper token
|
||||||
|
|
||||||
**But wait**, there's more!
|
|
||||||
See [example/serve.js](https://github.com/Daplie/letiny-core/blob/master/example/serve.js)
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var https = require('https');
|
var https = require('https');
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
|
|
||||||
|
|
||||||
var LeCore = deps.LeCore;
|
var LeCore = deps.LeCore;
|
||||||
var httpsOptions = deps.httpsOptions;
|
var tlsOptions = deps.tlsOptions;
|
||||||
var challengeStore = deps.challengeStore;
|
var challengeStore = deps.challengeStore;
|
||||||
var certStore = deps.certStore;
|
var certStore = deps.certStore;
|
||||||
|
|
||||||
@ -142,7 +264,7 @@ function acmeResponder(req, res) {
|
|||||||
//
|
//
|
||||||
// Server
|
// Server
|
||||||
//
|
//
|
||||||
https.createServer(httpsOptions, acmeResponder).listen(5001, function () {
|
https.createServer(tlsOptions, acmeResponder).listen(5001, function () {
|
||||||
console.log('Listening https on', this.address());
|
console.log('Listening https on', this.address());
|
||||||
});
|
});
|
||||||
http.createServer(acmeResponder).listen(80, function () {
|
http.createServer(acmeResponder).listen(80, function () {
|
||||||
@ -150,16 +272,13 @@ http.createServer(acmeResponder).listen(80, function () {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**But wait**, there's more!
|
||||||
|
See [example/serve.js](https://git.coolaj86.com/coolaj86/le-acme-core.js/blob/master/example/serve.js)
|
||||||
|
|
||||||
#### Put some storage in place
|
#### Put some storage in place
|
||||||
|
|
||||||
Finally, you need an implementation of `challengeStore`:
|
Finally, you need an implementation of `challengeStore`:
|
||||||
|
|
||||||
**But wait**, there's more!
|
|
||||||
See
|
|
||||||
|
|
||||||
* [example/challenge-store.js](https://github.com/Daplie/letiny-core/blob/master/challenge-store.js)
|
|
||||||
* [example/cert-store.js](https://github.com/Daplie/letiny-core/blob/master/cert-store.js)
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var challengeCache = {};
|
var challengeCache = {};
|
||||||
var challengeStore = {
|
var challengeStore = {
|
||||||
@ -192,88 +311,17 @@ var certStore = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
**But wait**, there's more!
|
||||||
|
See
|
||||||
|
|
||||||
The Goodies
|
* [example/challenge-store.js](https://git.coolaj86.com/coolaj86/le-acme-core.js/blob/master/challenge-store.js)
|
||||||
|
* [example/cert-store.js](https://git.coolaj86.com/coolaj86/le-acme-core.js/blob/master/cert-store.js)
|
||||||
```javascript
|
|
||||||
// Accounts
|
|
||||||
LeCore.registerNewAccount(options, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert})
|
|
||||||
|
|
||||||
{ newRegUrl: '<url>' // no defaults, specify acmeUrls.newAuthz
|
|
||||||
, email: '<email>' // valid email (server checks MX records)
|
|
||||||
, accountPrivateKeyPem: '<ASCII PEM>' // callback to allow user interaction for tosUrl
|
|
||||||
, agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registration
|
|
||||||
LeCore.getCertificate(options, cb) // returns (err, pems={ key, cert, ca })
|
|
||||||
|
|
||||||
{ newAuthzUrl: '<url>' // specify acmeUrls.newAuthz
|
|
||||||
, newCertUrl: '<url>' // specify acmeUrls.newCert
|
|
||||||
|
|
||||||
, domainPrivateKeyPem: '<ASCII PEM>'
|
|
||||||
, accountPrivateKeyPem: '<ASCII PEM>'
|
|
||||||
, domains: ['example.com']
|
|
||||||
|
|
||||||
, setChallenge: fn (hostname, key, val, cb)
|
|
||||||
, removeChallenge: fn (hostname, key, cb)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Helpers & Stuff
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Constants
|
|
||||||
LeCore.productionServerUrl // https://acme-v01.api.letsencrypt.org/directory
|
|
||||||
LeCore.stagingServerUrl // https://acme-staging.api.letsencrypt.org/directory
|
|
||||||
LeCore.acmeChallengePrefix // /.well-known/acme-challenge/
|
|
||||||
LeCore.configDir // /etc/letsencrypt/
|
|
||||||
LeCore.logsDir // /var/log/letsencrypt/
|
|
||||||
LeCore.workDir // /var/lib/letsencrypt/
|
|
||||||
LeCore.knownEndpoints // new-authz, new-cert, new-reg, revoke-cert
|
|
||||||
|
|
||||||
|
|
||||||
// HTTP Client Helpers
|
|
||||||
LeCore.Acme // Signs requests with JWK
|
|
||||||
acme = new Acme(lePrivateKey) // privateKey format is abstract
|
|
||||||
acme.post(url, body, cb) // POST with signature
|
|
||||||
acme.parseLinks(link) // (internal) parses 'link' header
|
|
||||||
acme.getNonce(url, cb) // (internal) HEAD request to get 'replay-nonce' strings
|
|
||||||
|
|
||||||
// Note: some of these are not async,
|
|
||||||
// but they will be soon. Don't rely
|
|
||||||
// on their API yet.
|
|
||||||
|
|
||||||
// Crypto Helpers
|
|
||||||
LeCore.leCrypto
|
|
||||||
generateRsaKeypair(bitLen, exponent, cb); // returns { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 }
|
|
||||||
thumbprint(lePubKey) // generates public key thumbprint
|
|
||||||
generateSignature(lePrivKey, bodyBuf, nonce) // generates a signature
|
|
||||||
privateJwkToPems(jwk) // { n: '...', e: '...', iq: '...', ... } to PEMs
|
|
||||||
privatePemToJwk // PEM to JWK (see line above)
|
|
||||||
importPemPrivateKey(privateKeyPem) // (internal) returns abstract private key
|
|
||||||
```
|
|
||||||
|
|
||||||
For testing and development, you can also inject the dependencies you want to use:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
LeCore = LeCore.create({
|
|
||||||
request: require('request')
|
|
||||||
, leCrypto: rquire('./lib/letsencrypt-forge')
|
|
||||||
});
|
|
||||||
|
|
||||||
// now uses node `request` (could also use jQuery or Angular in the browser)
|
|
||||||
LeCore.getAcmeUrls(discoveryUrl, function (err, urls) {
|
|
||||||
console.log(urls);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
* ISRG
|
* ISRG
|
||||||
* Anatol Sommer (https://github.com/anatolsommer)
|
* Anatol Sommer (https://github.com/anatolsommer)
|
||||||
* AJ ONeal <aj@daplie.com> (https://daplie.com)
|
* AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com)
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
@ -282,4 +330,4 @@ MPL 2.0
|
|||||||
All of the code is available under the MPL-2.0.
|
All of the code is available under the MPL-2.0.
|
||||||
|
|
||||||
Some of the files are original work not modified from `letiny`
|
Some of the files are original work not modified from `letiny`
|
||||||
and are made available under MIT as well (check file headers).
|
and are made available under MIT and Apache-2.0 as well (check file headers).
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*!
|
/*!
|
||||||
* letiny-core
|
* letiny-core
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*!
|
/*!
|
||||||
* letiny-core
|
* letiny-core
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
/*!
|
/*!
|
||||||
* letiny-core
|
* letiny-core
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
//var LeCore = require('letiny-core');
|
//var LeCore = require('letiny-core');
|
||||||
var LeCore = require('../');
|
var LeCore = require('../').ACME.create();
|
||||||
|
|
||||||
var email = process.argv[2] || 'user@example.com'; // CHANGE TO YOUR EMAIL
|
var email = process.argv[2] || 'user@example.com'; // CHANGE TO YOUR EMAIL
|
||||||
var domains = [process.argv[3] || 'example.com']; // CHANGE TO YOUR DOMAIN
|
var domains = [process.argv[3] || 'example.com']; // CHANGE TO YOUR DOMAIN
|
||||||
@ -17,8 +17,8 @@ var certStore = require('./cert-store');
|
|||||||
var serve = require('./serve');
|
var serve = require('./serve');
|
||||||
var closer;
|
var closer;
|
||||||
|
|
||||||
var accountPrivateKeyPem = null;
|
var accountKeypair = null;
|
||||||
var domainPrivateKeyPem = null;
|
var domainKeypair = null;
|
||||||
var acmeUrls = null;
|
var acmeUrls = null;
|
||||||
|
|
||||||
|
|
||||||
@ -44,14 +44,14 @@ function init() {
|
|||||||
|
|
||||||
function getPrivateKeys(cb) {
|
function getPrivateKeys(cb) {
|
||||||
console.log('Generating Account Keypair');
|
console.log('Generating Account Keypair');
|
||||||
console.log("(Note: if you're using forge and not ursa, this will take a long time");
|
const RSA = require('rsa-compat').RSA;
|
||||||
LeCore.leCrypto.generateRsaKeypair(2048, 65537, function (err, pems) {
|
RSA.generateKeypair(2048, 65537, {}, function (err, pems) {
|
||||||
|
|
||||||
accountPrivateKeyPem = pems.privateKeyPem;
|
accountKeypair = pems;
|
||||||
console.log('Generating Domain Keypair');
|
console.log('Generating Domain Keypair');
|
||||||
LeCore.leCrypto.generateRsaKeypair(2048, 65537, function (err, pems) {
|
RSA.generateKeypair(2048, 65537, {}, function (err, pems2) {
|
||||||
|
|
||||||
domainPrivateKeyPem = pems.privateKeyPem;
|
domainKeypair = pems2;
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -62,7 +62,7 @@ function runDemo() {
|
|||||||
LeCore.registerNewAccount(
|
LeCore.registerNewAccount(
|
||||||
{ newRegUrl: acmeUrls.newReg
|
{ newRegUrl: acmeUrls.newReg
|
||||||
, email: email
|
, email: email
|
||||||
, accountPrivateKeyPem: accountPrivateKeyPem
|
, accountKeypair: accountKeypair
|
||||||
, agreeToTerms: function (tosUrl, done) {
|
, agreeToTerms: function (tosUrl, done) {
|
||||||
|
|
||||||
// agree to the exact version of these terms
|
// agree to the exact version of these terms
|
||||||
@ -82,8 +82,8 @@ function runDemo() {
|
|||||||
{ newAuthzUrl: acmeUrls.newAuthz
|
{ newAuthzUrl: acmeUrls.newAuthz
|
||||||
, newCertUrl: acmeUrls.newCert
|
, newCertUrl: acmeUrls.newCert
|
||||||
|
|
||||||
, domainPrivateKeyPem: domainPrivateKeyPem
|
, domainKeypair: domainKeypair
|
||||||
, accountPrivateKeyPem: accountPrivateKeyPem
|
, accountKeypair: accountKeypair
|
||||||
, domains: domains
|
, domains: domains
|
||||||
|
|
||||||
, setChallenge: challengeStore.set
|
, setChallenge: challengeStore.set
|
||||||
@ -99,7 +99,7 @@ function runDemo() {
|
|||||||
closer();
|
closer();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -111,8 +111,7 @@ function runDemo() {
|
|||||||
//
|
//
|
||||||
closer = serve.init({
|
closer = serve.init({
|
||||||
LeCore: LeCore
|
LeCore: LeCore
|
||||||
// needs a default key and cert chain, anything will do
|
, tlsOptions: {}
|
||||||
, httpsOptions: require('localhost.daplie.com-certificates')
|
|
||||||
, challengeStore: challengeStore
|
, challengeStore: challengeStore
|
||||||
, certStore: certStore
|
, certStore: certStore
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*!
|
/*!
|
||||||
* letiny-core
|
* letiny-core
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
@ -15,7 +15,7 @@ module.exports.init = function (deps) {
|
|||||||
|
|
||||||
|
|
||||||
var LeCore = deps.LeCore;
|
var LeCore = deps.LeCore;
|
||||||
var httpsOptions = deps.httpsOptions;
|
var tlsOptions = deps.tlsOptions || deps.httpsOptions;
|
||||||
var challengeStore = deps.challengeStore;
|
var challengeStore = deps.challengeStore;
|
||||||
var certStore = deps.certStore;
|
var certStore = deps.certStore;
|
||||||
|
|
||||||
@ -63,11 +63,11 @@ module.exports.init = function (deps) {
|
|||||||
//
|
//
|
||||||
// Server
|
// Server
|
||||||
//
|
//
|
||||||
httpsOptions.SNICallback = certGetter;
|
tlsOptions.SNICallback = certGetter;
|
||||||
https.createServer(httpsOptions, acmeResponder).listen(443, function () {
|
https.createServer(tlsOptions, acmeResponder).listen(443, function () {
|
||||||
console.log('Listening https on', this.address());
|
console.log('Listening https on', this.address());
|
||||||
});
|
});
|
||||||
https.createServer(httpsOptions, acmeResponder).listen(5001, function () {
|
https.createServer(tlsOptions, acmeResponder).listen(5001, function () {
|
||||||
console.log('Listening https on', this.address());
|
console.log('Listening https on', this.address());
|
||||||
});
|
});
|
||||||
http.createServer(acmeResponder).listen(80, function () {
|
http.createServer(acmeResponder).listen(80, function () {
|
||||||
@ -77,6 +77,6 @@ module.exports.init = function (deps) {
|
|||||||
return function () {
|
return function () {
|
||||||
// Note: we should just keep a handle on
|
// Note: we should just keep a handle on
|
||||||
// the servers and close them each with server.close()
|
// the servers and close them each with server.close()
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,20 +8,29 @@
|
|||||||
|
|
||||||
module.exports.create = function (deps) {
|
module.exports.create = function (deps) {
|
||||||
|
|
||||||
var NOOP=function () {};
|
var NOOP = function () {
|
||||||
var log=NOOP;
|
};
|
||||||
var request=require('request');
|
var log = NOOP;
|
||||||
var generateSignature=deps.leCrypto.generateSignature;
|
var acmeRequest = deps.acmeRequest;
|
||||||
|
var RSA = deps.RSA;
|
||||||
|
var generateSignature = RSA.signJws;
|
||||||
|
|
||||||
function Acme(privateKey) {
|
function Acme(keypair) {
|
||||||
this.privateKey=privateKey;
|
if (!keypair) {
|
||||||
|
throw new Error("no keypair given. that's bad");
|
||||||
|
}
|
||||||
|
if ('string' === typeof keypair) {
|
||||||
|
// backwards compat
|
||||||
|
keypair = RSA.import({ privateKeyPem: keypair });
|
||||||
|
}
|
||||||
|
this.keypair = keypair;
|
||||||
this.nonces=[];
|
this.nonces=[];
|
||||||
}
|
}
|
||||||
|
|
||||||
Acme.prototype.getNonce=function(url, cb) {
|
Acme.prototype.getNonce=function(url, cb) {
|
||||||
var self=this;
|
var self=this;
|
||||||
|
|
||||||
request.head({
|
acmeRequest.create().head({
|
||||||
url:url,
|
url:url,
|
||||||
}, function(err, res/*, body*/) {
|
}, function(err, res/*, body*/) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -54,42 +63,45 @@ module.exports.create = function (deps) {
|
|||||||
log('Using nonce: '+this.nonces[0]);
|
log('Using nonce: '+this.nonces[0]);
|
||||||
payload=JSON.stringify(body, null, 2);
|
payload=JSON.stringify(body, null, 2);
|
||||||
jws=generateSignature(
|
jws=generateSignature(
|
||||||
this.privateKey, new Buffer(payload), this.nonces.shift()
|
self.keypair, new Buffer(payload), this.nonces.shift()
|
||||||
);
|
);
|
||||||
signed=JSON.stringify(jws, null, 2);
|
signed=JSON.stringify(jws, null, 2);
|
||||||
|
|
||||||
log('Posting to '+url);
|
log('Posting to '+url);
|
||||||
log(signed.green);
|
log(signed);
|
||||||
log('Payload:'+payload.blue);
|
log('Payload:'+payload);
|
||||||
|
|
||||||
return request.post({
|
//process.exit(1);
|
||||||
url:url,
|
//return;
|
||||||
body:signed,
|
return acmeRequest.create().post({
|
||||||
encoding:null
|
url: url
|
||||||
|
, body: signed
|
||||||
|
, encoding: null
|
||||||
}, function(err, res, body) {
|
}, function(err, res, body) {
|
||||||
var parsed;
|
var parsed;
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
|
console.error('[letiny-core/lib/acme-client.js] post');
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
if (res) {
|
if (res) {
|
||||||
log(('HTTP/1.1 '+res.statusCode).yellow);
|
log(('HTTP/1.1 '+res.statusCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(res.headers).forEach(function(key) {
|
Object.keys(res.headers).forEach(function(key) {
|
||||||
var value, upcased;
|
var value, upcased;
|
||||||
value=res.headers[key];
|
value=res.headers[key];
|
||||||
upcased=key.charAt(0).toUpperCase()+key.slice(1);
|
upcased=key.charAt(0).toUpperCase()+key.slice(1);
|
||||||
log((upcased+': '+value).yellow);
|
log((upcased+': '+value));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
|
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
|
||||||
try {
|
try {
|
||||||
parsed=JSON.parse(body);
|
parsed=JSON.parse(body);
|
||||||
log(JSON.stringify(parsed, null, 2).cyan);
|
log(JSON.stringify(parsed, null, 2));
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
log(body.toString().cyan);
|
log(body.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
// Copyright 2014 ISRG. All rights reserved
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
fromStandardB64: function(x) {
|
|
||||||
return x.replace(/[+]/g, "-").replace(/\//g, "_").replace(/=/g,"");
|
|
||||||
},
|
|
||||||
|
|
||||||
toStandardB64: function(x) {
|
|
||||||
var b64 = x.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
|
|
||||||
|
|
||||||
switch (b64.length % 4) {
|
|
||||||
case 2: b64 += "=="; break;
|
|
||||||
case 3: b64 += "="; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return b64;
|
|
||||||
},
|
|
||||||
|
|
||||||
b64enc: function(buffer) {
|
|
||||||
return this.fromStandardB64(buffer.toString("base64"));
|
|
||||||
},
|
|
||||||
|
|
||||||
b64dec: function(str) {
|
|
||||||
return new Buffer(this.toStandardB64(str), "base64");
|
|
||||||
},
|
|
||||||
|
|
||||||
isB64String: function(x) {
|
|
||||||
return ("string" === typeof x) && !x.match(/[^a-zA-Z0-9_-]/);
|
|
||||||
},
|
|
||||||
|
|
||||||
fieldsPresent: function(fields, object) {
|
|
||||||
for (var i in fields) {
|
|
||||||
if (!(fields[i] in object)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
validSignature: function(sig) {
|
|
||||||
return (("object" === typeof sig) &&
|
|
||||||
("alg" in sig) && ("string" === typeof sig.alg) &&
|
|
||||||
("nonce" in sig) && this.isB64String(sig.nonce) &&
|
|
||||||
("sig" in sig) && this.isB64String(sig.sig) &&
|
|
||||||
("jwk" in sig) && this.validJWK(sig.jwk));
|
|
||||||
},
|
|
||||||
|
|
||||||
validJWK: function(jwk) {
|
|
||||||
return (("object" === typeof jwk) && ("kty" in jwk) && (
|
|
||||||
((jwk.kty === "RSA")
|
|
||||||
&& ("n" in jwk) && this.isB64String(jwk.n)
|
|
||||||
&& ("e" in jwk) && this.isB64String(jwk.e)) ||
|
|
||||||
((jwk.kty === "EC")
|
|
||||||
&& ("crv" in jwk)
|
|
||||||
&& ("x" in jwk) && this.isB64String(jwk.x)
|
|
||||||
&& ("y" in jwk) && this.isB64String(jwk.y))
|
|
||||||
) && !("d" in jwk));
|
|
||||||
},
|
|
||||||
|
|
||||||
// A simple, non-standard fingerprint for a JWK,
|
|
||||||
// just so that we don't have to store objects
|
|
||||||
keyFingerprint: function(jwk) {
|
|
||||||
switch (jwk.kty) {
|
|
||||||
case "RSA": return jwk.n;
|
|
||||||
case "EC": return jwk.crv + jwk.x + jwk.y;
|
|
||||||
}
|
|
||||||
throw "Unrecognized key type";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny
|
|
||||||
* Copyright(c) 2015 Anatol Sommer <anatol@anatol.at>
|
|
||||||
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
|
|
||||||
* MPL 2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
exports.Acme = require('./acme-client');
|
|
||||||
exports.registerNewAccount = require('./register-new-account');
|
|
||||||
exports.getCertificate = require('./get-certificate');
|
|
||||||
exports.getCert=function (options, cb) {
|
|
||||||
exports.registerNewAccount(options, function () {
|
|
||||||
exports.getCertificate(options, cb);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,19 +1,23 @@
|
|||||||
/*!
|
/*!
|
||||||
* letiny-core
|
* letiny-core
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports.create = function (deps) {
|
module.exports.create = function (deps) {
|
||||||
var request = deps.request;
|
var acmeRequest = deps.acmeRequest;
|
||||||
var knownUrls = deps.LeCore.knownEndpoints;
|
var knownUrls = deps.LeCore.knownEndpoints;
|
||||||
|
|
||||||
function getAcmeUrls(acmeDiscoveryUrl, cb) {
|
function getAcmeUrls(acmeDiscoveryUrl, cb) {
|
||||||
|
if ('string' !== typeof acmeDiscoveryUrl) {
|
||||||
|
cb(new Error("getAcmeUrls: acmeDiscoveryUrl must be a string"));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO check response header on request for cache time
|
// TODO check response header on request for cache time
|
||||||
return request({
|
return acmeRequest.create()({
|
||||||
url: acmeDiscoveryUrl
|
url: acmeDiscoveryUrl
|
||||||
|
, encoding: 'utf8'
|
||||||
}, function (err, resp) {
|
}, function (err, resp) {
|
||||||
if (err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
@ -26,16 +30,15 @@ module.exports.create = function (deps) {
|
|||||||
try {
|
try {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
e.raw = data;
|
||||||
|
e.url = acmeDiscoveryUrl;
|
||||||
|
e.stack += '\n\nresponse data:\n'
|
||||||
|
+ data + '\n\nacmeDiscoveryUrl:' + acmeDiscoveryUrl;
|
||||||
cb(e);
|
cb(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (4 !== Object.keys(data).length) {
|
|
||||||
console.warn("This Let's Encrypt / ACME server has been updated with urls that this client doesn't understand");
|
|
||||||
console.warn(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!knownUrls.every(function (url) {
|
if (!knownUrls.every(function (url) {
|
||||||
return data[url];
|
return data[url];
|
||||||
})) {
|
})) {
|
||||||
@ -48,6 +51,7 @@ module.exports.create = function (deps) {
|
|||||||
, newCert: data['new-cert']
|
, newCert: data['new-cert']
|
||||||
, newReg: data['new-reg']
|
, newReg: data['new-reg']
|
||||||
, revokeCert: data['revoke-cert']
|
, revokeCert: data['revoke-cert']
|
||||||
|
, keyChange: data['key-change']
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,16 +6,353 @@
|
|||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports.create = function (deps) {
|
function _toStandardBase64(str) {
|
||||||
var NOOP=function () {}, log=NOOP;
|
var b64 = str.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
|
||||||
var request=deps.request;
|
|
||||||
var toStandardB64 = deps.leUtils.toStandardB64;
|
|
||||||
var importPemPrivateKey = deps.leCrypto.importPemPrivateKey;
|
|
||||||
var thumbprinter = deps.leCrypto.thumbprint;
|
|
||||||
var generateCsr = deps.leCrypto.generateCsr || deps.leCrypto.generateCSR;
|
|
||||||
var Acme = deps.Acme;
|
|
||||||
|
|
||||||
|
switch (b64.length % 4) {
|
||||||
|
case 2: b64 += "=="; break;
|
||||||
|
case 3: b64 += "="; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
|
||||||
|
function certBufferToPem(cert) {
|
||||||
|
cert = _toStandardBase64(cert.toString('base64'));
|
||||||
|
cert = cert.match(/.{1,64}/g).join('\r\n');
|
||||||
|
return '-----BEGIN CERTIFICATE-----\r\n'+cert+'\r\n-----END CERTIFICATE-----\r\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.create = function (deps) {
|
||||||
|
var acmeRequest = deps.acmeRequest;
|
||||||
|
var Acme = deps.Acme;
|
||||||
|
var RSA = deps.RSA;
|
||||||
|
|
||||||
|
// getCertificate // returns "pems", meaning "certs"
|
||||||
function getCert(options, cb) {
|
function getCert(options, cb) {
|
||||||
|
|
||||||
|
function bodyToError(res, body) {
|
||||||
|
var err;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
err = new Error("[Error] letiny-core: no request body");
|
||||||
|
err.code = "E_NO_RESPONSE_BODY";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('{' === body[0] || '{' === String.fromCharCode(body[0])) {
|
||||||
|
try {
|
||||||
|
body = JSON.parse(body.toString('utf8'));
|
||||||
|
} catch(e) {
|
||||||
|
err = new Error("[Error] letiny-core: body could not be parsed");
|
||||||
|
err.code = "E_BODY_PARSE";
|
||||||
|
err.description = body;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.floor(res.statusCode / 100) !== 2) {
|
||||||
|
err = new Error("[Error] letiny-core: not 200 ok");
|
||||||
|
err.code = "E_STATUS_CODE";
|
||||||
|
err.type = body.type;
|
||||||
|
err.description = body;
|
||||||
|
err.detail = body.detail;
|
||||||
|
console.error("TODO: modules which depend on this module should expose this error properly but since some of them don't, I expose it here directly:");
|
||||||
|
console.error(err.stack);
|
||||||
|
console.error(body);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.type && body.detail) {
|
||||||
|
err = new Error("[Error] letiny-core: " + body.detail);
|
||||||
|
err.code = body.type;
|
||||||
|
err.type = body.type;
|
||||||
|
err.description = body.detail;
|
||||||
|
err.detail = body.detail;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextDomain() {
|
||||||
|
if (state.domains.length > 0) {
|
||||||
|
getChallenges(state.domains.shift());
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
getCertificate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChallenges(domain) {
|
||||||
|
state.domain = domain;
|
||||||
|
|
||||||
|
state.acme.post(state.newAuthzUrl, {
|
||||||
|
resource: 'new-authz',
|
||||||
|
identifier: {
|
||||||
|
type: 'dns',
|
||||||
|
value: state.domain,
|
||||||
|
}
|
||||||
|
}, function (err, res, body) {
|
||||||
|
if (!err && res.body) {
|
||||||
|
try {
|
||||||
|
body = bodyToError(res, body);
|
||||||
|
} catch(e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getReadyToValidate(err, res, body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReadyToValidate(err, res, body) {
|
||||||
|
var links;
|
||||||
|
var authz;
|
||||||
|
var httpChallenges;
|
||||||
|
var challenge;
|
||||||
|
var thumbprint;
|
||||||
|
var keyAuthorization;
|
||||||
|
|
||||||
|
function challengeDone(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('[letiny-core] setChallenge Error:');
|
||||||
|
console.error(err && err.stack || err);
|
||||||
|
ensureValidation(err, null, null, function () {
|
||||||
|
options.removeChallenge(state.domain, challenge.token, function () {
|
||||||
|
// ignore
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.acme.post(state.responseUrl, {
|
||||||
|
resource: 'challenge',
|
||||||
|
keyAuthorization: keyAuthorization
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (!err && res.body) {
|
||||||
|
try {
|
||||||
|
body = bodyToError(res, body);
|
||||||
|
} catch(e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureValidation(err, res, body, function unlink() {
|
||||||
|
options.removeChallenge(state.domain, challenge.token, function () {
|
||||||
|
// ignore
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return handleErr(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.floor(res.statusCode/100)!==2) {
|
||||||
|
return handleErr(null, 'Authorization request failed ('+res.statusCode+')');
|
||||||
|
}
|
||||||
|
|
||||||
|
links = Acme.parseLink(res.headers.link);
|
||||||
|
if (!links || !('next' in links)) {
|
||||||
|
return handleErr(err, 'Server didn\'t provide information to proceed (2)');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.authorizationUrl = res.headers.location;
|
||||||
|
state.newCertUrl = links.next;
|
||||||
|
|
||||||
|
authz = body;
|
||||||
|
|
||||||
|
httpChallenges = authz.challenges.filter(function(x) {
|
||||||
|
return x.type === options.challengeType;
|
||||||
|
});
|
||||||
|
if (httpChallenges.length === 0) {
|
||||||
|
return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
|
||||||
|
}
|
||||||
|
challenge = httpChallenges[0];
|
||||||
|
|
||||||
|
thumbprint = RSA.thumbprint(state.accountKeypair);
|
||||||
|
keyAuthorization = challenge.token + '.' + thumbprint;
|
||||||
|
|
||||||
|
state.responseUrl = challenge.uri;
|
||||||
|
|
||||||
|
options.setChallenge(state.domain, challenge.token, keyAuthorization, challengeDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidation(err, res, body, unlink) {
|
||||||
|
var authz, challengesState;
|
||||||
|
|
||||||
|
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||||
|
unlink();
|
||||||
|
return handleErr(err, 'Authorization status request failed ('
|
||||||
|
+ (res && res.statusCode || err.code || err.message || err) + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
authz=body;
|
||||||
|
|
||||||
|
if (authz.status==='pending') {
|
||||||
|
setTimeout(function() {
|
||||||
|
acmeRequest.create()({
|
||||||
|
method: 'GET'
|
||||||
|
, url: state.authorizationUrl
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (!err && res.body) {
|
||||||
|
try {
|
||||||
|
body = bodyToError(res, body);
|
||||||
|
} catch(e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureValidation(err, res, body, unlink);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
} else if (authz.status==='valid') {
|
||||||
|
log('Validating domain ... done');
|
||||||
|
state.validatedDomains.push(state.domain);
|
||||||
|
state.validAuthorizationUrls.push(state.authorizationUrl);
|
||||||
|
unlink();
|
||||||
|
nextDomain();
|
||||||
|
} else if (authz.status==='invalid') {
|
||||||
|
unlink();
|
||||||
|
challengesState = (authz.challenges || []).map(function (challenge) {
|
||||||
|
var result = ' - ' + challenge.uri + ' [' + challenge.status + ']';
|
||||||
|
if (challenge.error) {
|
||||||
|
result += '\n ' + challenge.error.detail;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}).join('\n');
|
||||||
|
return handleErr(null,
|
||||||
|
'The CA was unable to validate the file you provisioned. '
|
||||||
|
+ (authz.detail ? 'Details: ' + authz.detail : '')
|
||||||
|
+ (challengesState ? '\n' + challengesState : ''), body);
|
||||||
|
} else {
|
||||||
|
unlink();
|
||||||
|
return handleErr(null, 'CA returned an authorization in an unexpected state' + authz.detail, authz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCertificate() {
|
||||||
|
var csr=RSA.generateCsrWeb64(state.certKeypair, state.validatedDomains);
|
||||||
|
log('Requesting certificate...');
|
||||||
|
state.acme.post(state.newCertUrl, {
|
||||||
|
resource:'new-cert',
|
||||||
|
csr:csr,
|
||||||
|
authorizations:state.validAuthorizationUrls
|
||||||
|
}, function (err, res, body ) {
|
||||||
|
if (!err && res.body) {
|
||||||
|
try {
|
||||||
|
body = bodyToError(res, body);
|
||||||
|
} catch(e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadCertificate(err, res, body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCertificate(err, res, body) {
|
||||||
|
var links, certUrl;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
handleErr(err, 'Certificate request failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.floor(res.statusCode/100)!==2) {
|
||||||
|
err = new Error("invalid status code: " + res.statusCode);
|
||||||
|
err.code = "E_STATUS_CODE";
|
||||||
|
err.description = body;
|
||||||
|
handleErr(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
links=Acme.parseLink(res.headers.link);
|
||||||
|
if (!links || !('up' in links)) {
|
||||||
|
return handleErr(err, 'Failed to fetch issuer certificate');
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Requesting certificate: done');
|
||||||
|
|
||||||
|
state.certificate=body;
|
||||||
|
certUrl=res.headers.location;
|
||||||
|
acmeRequest.create()({
|
||||||
|
method: 'GET'
|
||||||
|
, url: certUrl
|
||||||
|
, encoding: null
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (!err) {
|
||||||
|
try {
|
||||||
|
body = bodyToError(res, body);
|
||||||
|
} catch(e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return handleErr(err, 'Failed to fetch cert from '+certUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.statusCode!==200) {
|
||||||
|
return handleErr(err, 'Failed to fetch cert from '+certUrl, res.body.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.toString()!==state.certificate.toString()) {
|
||||||
|
return handleErr(null, 'Cert at '+certUrl+' did not match returned cert');
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Successfully verified cert at '+certUrl);
|
||||||
|
downloadIssuerCert(links);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadIssuerCert(links) {
|
||||||
|
log('Requesting issuer certificate...');
|
||||||
|
acmeRequest.create()({
|
||||||
|
method: 'GET'
|
||||||
|
, url: links.up
|
||||||
|
, encoding: null
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (!err) {
|
||||||
|
try {
|
||||||
|
body = bodyToError(res, body);
|
||||||
|
} catch(e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err || res.statusCode!==200) {
|
||||||
|
return handleErr(err, 'Failed to fetch issuer certificate');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.chainPem = certBufferToPem(body);
|
||||||
|
log('Requesting issuer certificate: done');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
var privkeyPem = RSA.exportPrivatePem(state.certKeypair);
|
||||||
|
|
||||||
|
cb(null, {
|
||||||
|
cert: certBufferToPem(state.certificate)
|
||||||
|
, privkey: privkeyPem
|
||||||
|
, chain: state.chainPem
|
||||||
|
// TODO nix backwards compat
|
||||||
|
, key: privkeyPem
|
||||||
|
, ca: state.chainPem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleErr(err, text, info) {
|
||||||
|
log(text, err, info);
|
||||||
|
cb(err || new Error(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
var NOOP = function () {};
|
||||||
|
var log = options.debug ? console.log : NOOP;
|
||||||
var state={
|
var state={
|
||||||
validatedDomains:[]
|
validatedDomains:[]
|
||||||
, validAuthorizationUrls:[]
|
, validAuthorizationUrls:[]
|
||||||
@ -23,17 +360,31 @@ module.exports.create = function (deps) {
|
|||||||
, newCertUrl: options.newCertUrl
|
, newCertUrl: options.newCertUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!options.challengeType) {
|
||||||
|
options.challengeType = 'http-01';
|
||||||
|
}
|
||||||
|
if (-1 === [ 'http-01', 'tls-sni-01', 'dns-01' ].indexOf(options.challengeType)) {
|
||||||
|
return handleErr(new Error("options.challengeType '" + options.challengeType + "' is not yet supported"));
|
||||||
|
}
|
||||||
if (!options.newAuthzUrl) {
|
if (!options.newAuthzUrl) {
|
||||||
return handleErr(new Error("options.newAuthzUrl must be the authorization url"));
|
return handleErr(new Error("options.newAuthzUrl must be the authorization url"));
|
||||||
}
|
}
|
||||||
if (!options.newCertUrl) {
|
if (!options.newCertUrl) {
|
||||||
return handleErr(new Error("options.newCertUrl must be the new certificate url"));
|
return handleErr(new Error("options.newCertUrl must be the new certificate url"));
|
||||||
}
|
}
|
||||||
if (!options.accountPrivateKeyPem) {
|
if (!options.accountKeypair) {
|
||||||
return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem"));
|
if (!options.accountPrivateKeyPem) {
|
||||||
|
return handleErr(new Error("options.accountKeypair must be an object with `privateKeyPem` and/or `privateKeyJwk`"));
|
||||||
|
}
|
||||||
|
console.warn("'accountPrivateKeyPem' is deprecated. Use options.accountKeypair.privateKeyPem instead.");
|
||||||
|
options.accountKeypair = RSA.import({ privateKeyPem: options.accountPrivateKeyPem });
|
||||||
}
|
}
|
||||||
if (!options.domainPrivateKeyPem) {
|
if (!options.domainKeypair) {
|
||||||
return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem"));
|
if (!options.domainPrivateKeyPem) {
|
||||||
|
return handleErr(new Error("options.domainKeypair must be an object with `privateKeyPem` and/or `privateKeyJwk`"));
|
||||||
|
}
|
||||||
|
console.warn("'domainPrivateKeyPem' is deprecated. Use options.domainKeypair.privateKeyPem instead.");
|
||||||
|
options.domainKeypair = RSA.import({ privateKeyPem: options.domainPrivateKeyPem });
|
||||||
}
|
}
|
||||||
if (!options.setChallenge) {
|
if (!options.setChallenge) {
|
||||||
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
|
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
|
||||||
@ -47,207 +398,14 @@ module.exports.create = function (deps) {
|
|||||||
|
|
||||||
state.domains = options.domains.slice(0); // copy array
|
state.domains = options.domains.slice(0); // copy array
|
||||||
try {
|
try {
|
||||||
state.accountKeyPem=options.accountPrivateKeyPem;
|
state.accountKeypair = options.accountKeypair;
|
||||||
state.accountKeyPair=importPemPrivateKey(state.accountKeyPem);
|
state.certKeypair = options.domainKeypair;
|
||||||
state.acme=new Acme(state.accountKeyPair);
|
state.acme = new Acme(state.accountKeypair);
|
||||||
state.certPrivateKeyPem=options.domainPrivateKeyPem;
|
|
||||||
state.certPrivateKey=importPemPrivateKey(state.certPrivateKeyPem);
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
return handleErr(err, 'Failed to parse privateKey');
|
return handleErr(err, 'Failed to parse privateKey');
|
||||||
}
|
}
|
||||||
|
|
||||||
nextDomain();
|
nextDomain();
|
||||||
|
|
||||||
function nextDomain() {
|
|
||||||
if (state.domains.length > 0) {
|
|
||||||
getChallenges(state.domains.shift());
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
getCertificate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getChallenges(domain) {
|
|
||||||
state.domain=domain;
|
|
||||||
|
|
||||||
state.acme.post(state.newAuthzUrl, {
|
|
||||||
resource:'new-authz',
|
|
||||||
identifier:{
|
|
||||||
type:'dns',
|
|
||||||
value:state.domain,
|
|
||||||
}
|
|
||||||
}, getReadyToValidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReadyToValidate(err, res, body) {
|
|
||||||
var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath;
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return handleErr(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.floor(res.statusCode/100)!==2) {
|
|
||||||
return handleErr(null, 'Authorization request failed ('+res.statusCode+')');
|
|
||||||
}
|
|
||||||
|
|
||||||
links=Acme.parseLink(res.headers.link);
|
|
||||||
if (!links || !('next' in links)) {
|
|
||||||
return handleErr(err, 'Server didn\'t provide information to proceed (2)');
|
|
||||||
}
|
|
||||||
|
|
||||||
state.authorizationUrl=res.headers.location;
|
|
||||||
state.newCertUrl=links.next;
|
|
||||||
|
|
||||||
authz=JSON.parse(body);
|
|
||||||
|
|
||||||
httpChallenges=authz.challenges.filter(function(x) {
|
|
||||||
return x.type==='http-01';
|
|
||||||
});
|
|
||||||
if (httpChallenges.length===0) {
|
|
||||||
return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
|
|
||||||
}
|
|
||||||
challenge=httpChallenges[0];
|
|
||||||
|
|
||||||
thumbprint=thumbprinter(state.accountKeyPair.publicKey);
|
|
||||||
keyAuthorization=challenge.token+'.'+thumbprint;
|
|
||||||
state.responseUrl=challenge.uri;
|
|
||||||
|
|
||||||
options.setChallenge(state.domain, challenge.token, keyAuthorization, challengeDone);
|
|
||||||
|
|
||||||
function challengeDone() {
|
|
||||||
state.acme.post(state.responseUrl, {
|
|
||||||
resource:'challenge',
|
|
||||||
keyAuthorization:keyAuthorization
|
|
||||||
}, function(err, res, body) {
|
|
||||||
ensureValidation(err, res, body, function unlink() {
|
|
||||||
options.removeChallenge(state.domain, challenge.token, function () {
|
|
||||||
// ignore
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureValidation(err, res, body, unlink) {
|
|
||||||
var authz;
|
|
||||||
|
|
||||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
|
||||||
unlink();
|
|
||||||
return handleErr(err, 'Authorization status request failed ('+res.statusCode+')');
|
|
||||||
}
|
|
||||||
|
|
||||||
authz=JSON.parse(body);
|
|
||||||
|
|
||||||
if (authz.status==='pending') {
|
|
||||||
setTimeout(function() {
|
|
||||||
request.get(state.authorizationUrl, {}, function(err, res, body) {
|
|
||||||
ensureValidation(err, res, body, unlink);
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
} else if (authz.status==='valid') {
|
|
||||||
log('Validating domain ... done');
|
|
||||||
state.validatedDomains.push(state.domain);
|
|
||||||
state.validAuthorizationUrls.push(state.authorizationUrl);
|
|
||||||
unlink();
|
|
||||||
nextDomain();
|
|
||||||
} else if (authz.status==='invalid') {
|
|
||||||
unlink();
|
|
||||||
return handleErr(null, 'The CA was unable to validate the file you provisioned', body);
|
|
||||||
} else {
|
|
||||||
unlink();
|
|
||||||
return handleErr(null, 'CA returned an authorization in an unexpected state', authz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCertificate() {
|
|
||||||
var csr=generateCsr(state.certPrivateKey, state.validatedDomains);
|
|
||||||
log('Requesting certificate...');
|
|
||||||
state.acme.post(state.newCertUrl, {
|
|
||||||
resource:'new-cert',
|
|
||||||
csr:csr,
|
|
||||||
authorizations:state.validAuthorizationUrls
|
|
||||||
}, downloadCertificate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadCertificate(err, res, body) {
|
|
||||||
var links, certUrl;
|
|
||||||
|
|
||||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
|
||||||
log('Certificate request failed with error ', err);
|
|
||||||
if (body) {
|
|
||||||
log(body.toString());
|
|
||||||
}
|
|
||||||
return handleErr(err, 'Certificate request failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
links=Acme.parseLink(res.headers.link);
|
|
||||||
if (!links || !('up' in links)) {
|
|
||||||
return handleErr(err, 'Failed to fetch issuer certificate');
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Requesting certificate: done');
|
|
||||||
|
|
||||||
state.certificate=body;
|
|
||||||
certUrl=res.headers.location;
|
|
||||||
request.get({
|
|
||||||
url:certUrl,
|
|
||||||
encoding:null
|
|
||||||
}, function(err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
return handleErr(err, 'Failed to fetch cert from '+certUrl);
|
|
||||||
}
|
|
||||||
if (res.statusCode!==200) {
|
|
||||||
return handleErr(err, 'Failed to fetch cert from '+certUrl, res.body.toString());
|
|
||||||
}
|
|
||||||
if (body.toString()!==state.certificate.toString()) {
|
|
||||||
handleErr(null, 'Cert at '+certUrl+' did not match returned cert');
|
|
||||||
} else {
|
|
||||||
log('Successfully verified cert at '+certUrl);
|
|
||||||
log('Requesting issuer certificate...');
|
|
||||||
request.get({
|
|
||||||
url:links.up,
|
|
||||||
encoding:null
|
|
||||||
}, function(err, res, body) {
|
|
||||||
if (err || res.statusCode!==200) {
|
|
||||||
return handleErr(err, 'Failed to fetch issuer certificate');
|
|
||||||
}
|
|
||||||
state.caCert=certBufferToPem(body);
|
|
||||||
log('Requesting issuer certificate: done');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function done() {
|
|
||||||
var cert;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cert=certBufferToPem(state.certificate);
|
|
||||||
} catch(e) {
|
|
||||||
console.error(e.stack);
|
|
||||||
//cb(new Error("Could not write output files. Please check permissions!"));
|
|
||||||
handleErr(e, 'Could not write output files. Please check permissions!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
cert: cert
|
|
||||||
, key: state.certPrivateKeyPem
|
|
||||||
, ca: state.caCert
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleErr(err, text, info) {
|
|
||||||
log(text, err, info);
|
|
||||||
cb(err || new Error(text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function certBufferToPem(cert) {
|
|
||||||
cert=toStandardB64(cert.toString('base64'));
|
|
||||||
cert=cert.match(/.{1,64}/g).join('\n');
|
|
||||||
return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getCert;
|
return getCert;
|
||||||
|
|||||||
72
lib/le-acme-request.js
Normal file
72
lib/le-acme-request.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*!
|
||||||
|
* le-acme-core
|
||||||
|
* Author: Kelly Johnson
|
||||||
|
* Copyright 2017
|
||||||
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const request = require('request');
|
||||||
|
const pkgJSON = require('../package.json');
|
||||||
|
const version = pkgJSON.version;
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const uaDefaults = {
|
||||||
|
pkg: `Greenlock/${version}`
|
||||||
|
, os: ` (${os.type()}; ${process.arch} ${os.platform()} ${os.release()})`
|
||||||
|
, node: ` Node.js/${process.version}`
|
||||||
|
, user: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentUAProps;
|
||||||
|
|
||||||
|
function getUaString() {
|
||||||
|
let userAgent = '';
|
||||||
|
for (let key in currentUAProps) {
|
||||||
|
userAgent += currentUAProps[key];
|
||||||
|
}
|
||||||
|
return userAgent.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequest() {
|
||||||
|
return request.defaults({
|
||||||
|
headers: {
|
||||||
|
'User-Agent': getUaString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetUa() {
|
||||||
|
currentUAProps = {};
|
||||||
|
for (let key in uaDefaults) {
|
||||||
|
currentUAProps[key] = uaDefaults[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUaString(string) {
|
||||||
|
currentUAProps.user += ` ${string}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function omitUaProperties(opts) {
|
||||||
|
if (opts.all) {
|
||||||
|
currentUAProps = {};
|
||||||
|
} else {
|
||||||
|
for (let key in opts) {
|
||||||
|
currentUAProps[key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our UA to begin with
|
||||||
|
resetUa();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
create: function create() {
|
||||||
|
// get deps and modify here if need be
|
||||||
|
return getRequest();
|
||||||
|
}
|
||||||
|
, addUaString: addUaString
|
||||||
|
, omitUaProperties: omitUaProperties
|
||||||
|
, resetUa: resetUa
|
||||||
|
, getUaString: getUaString
|
||||||
|
};
|
||||||
@ -1,115 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var crypto = require('crypto');
|
|
||||||
var forge = require('node-forge');
|
|
||||||
|
|
||||||
function binstrToB64(binstr) {
|
|
||||||
return new Buffer(binstr, 'binary').toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
function b64ToBinstr(b64) {
|
|
||||||
return new Buffer(b64, 'base64').toString('binary');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toAcmePrivateKey(forgePrivkey) {
|
|
||||||
//var forgePrivkey = forge.pki.privateKeyFromPem(privkeyPem);
|
|
||||||
|
|
||||||
return {
|
|
||||||
kty: "RSA"
|
|
||||||
, n: binstrToB64(forgePrivkey.n)
|
|
||||||
, e: binstrToB64(forgePrivkey.e)
|
|
||||||
, d: binstrToB64(forgePrivkey.d)
|
|
||||||
, p: binstrToB64(forgePrivkey.p)
|
|
||||||
, q: binstrToB64(forgePrivkey.q)
|
|
||||||
, dp: binstrToB64(forgePrivkey.dP)
|
|
||||||
, dq: binstrToB64(forgePrivkey.dQ)
|
|
||||||
, qi: binstrToB64(forgePrivkey.qInv)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toForgePrivateKey(forgePrivkey) {
|
|
||||||
return forge.pki.rsa.setPrivateKey(
|
|
||||||
b64ToBinstr(forgePrivkey.n)
|
|
||||||
, b64ToBinstr(forgePrivkey.e)
|
|
||||||
, b64ToBinstr(forgePrivkey.d)
|
|
||||||
, b64ToBinstr(forgePrivkey.p)
|
|
||||||
, b64ToBinstr(forgePrivkey.q)
|
|
||||||
, b64ToBinstr(forgePrivkey.dp)
|
|
||||||
, b64ToBinstr(forgePrivkey.dq)
|
|
||||||
, b64ToBinstr(forgePrivkey.qi)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// WARNING: with forge this takes 20+ minutes on a Raspberry Pi!!!
|
|
||||||
// It takes SEVERAL seconds even on a nice macbook pro
|
|
||||||
function generateRsaKeypair(bitlen, exp, cb) {
|
|
||||||
var pki = forge.pki;
|
|
||||||
var keypair = pki.rsa.generateKeyPair({ bits: bitlen, e: exp });
|
|
||||||
var pems = {
|
|
||||||
publicKeyPem: pki.publicKeyToPem(keypair.publicKey) // ascii PEM: ----BEGIN...
|
|
||||||
, privateKeyPem: pki.privateKeyToPem(keypair.privateKey) // ascii PEM: ----BEGIN...
|
|
||||||
};
|
|
||||||
|
|
||||||
// I would have chosen sha1 or sha2... but whatever
|
|
||||||
pems.publicKeyMd5 = crypto.createHash('md5').update(pems.publicKeyPem).digest('hex');
|
|
||||||
// json { n: ..., e: ..., iq: ..., etc }
|
|
||||||
pems.privateKeyJwk = toAcmePrivateKey(keypair.privateKey);
|
|
||||||
// deprecate
|
|
||||||
pems.privateKeyJson = pems.privateKeyJwk;
|
|
||||||
|
|
||||||
// TODO thumbprint
|
|
||||||
|
|
||||||
cb(null, pems);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseAccountPrivateKey(pkj, cb) {
|
|
||||||
var pki = forge.pki;
|
|
||||||
|
|
||||||
Object.keys(pkj).forEach(function (key) {
|
|
||||||
pkj[key] = new Buffer(pkj[key], 'base64');
|
|
||||||
});
|
|
||||||
|
|
||||||
var priv;
|
|
||||||
var pubPem;
|
|
||||||
|
|
||||||
try {
|
|
||||||
priv = toForgePrivateKey(
|
|
||||||
pkj.n // modulus
|
|
||||||
, pkj.e // exponent
|
|
||||||
, pkj.p
|
|
||||||
, pkj.q
|
|
||||||
, pkj.dp
|
|
||||||
, pkj.dq
|
|
||||||
, pkj.qi
|
|
||||||
, pkj.d
|
|
||||||
);
|
|
||||||
} catch(e) {
|
|
||||||
cb(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pubPem = pki.publicKeyToPem(priv.publicKey);
|
|
||||||
cb(null, {
|
|
||||||
publicKeyPem: pubPem // ascii PEM: ----BEGIN...
|
|
||||||
, privateKeyPem: pki.privateKeyToPem(priv.privateKey) // ascii PEM: ----BEGIN...
|
|
||||||
// json { n: ..., e: ..., iq: ..., etc }
|
|
||||||
, privateKeyJwt: pkj
|
|
||||||
// deprecate
|
|
||||||
, privateKeyJson: pkj
|
|
||||||
// I would have chosen sha1 or sha2... but whatever
|
|
||||||
, publicKeyMd5: crypto.createHash('md5').update(pubPem).digest('hex')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.generateRsaKeypair = generateRsaKeypair;
|
|
||||||
module.exports.privateJwkToPems = parseAccountPrivateKey;
|
|
||||||
module.exports.privatePemToJwk = toAcmePrivateKey;
|
|
||||||
|
|
||||||
// TODO deprecate
|
|
||||||
module.exports.toAcmePrivateKey = toAcmePrivateKey;
|
|
||||||
module.exports.parseAccountPrivateKey = parseAccountPrivateKey;
|
|
||||||
@ -1,367 +0,0 @@
|
|||||||
// Copyright 2014 ISRG. All rights reserved
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var crypto = require("crypto");
|
|
||||||
var forge = require("node-forge");
|
|
||||||
var util = require("./acme-util.js");
|
|
||||||
|
|
||||||
var TOKEN_SIZE = 16;
|
|
||||||
//var NONCE_SIZE = 16;
|
|
||||||
|
|
||||||
function bytesToBuffer(bytes) {
|
|
||||||
return new Buffer(forge.util.bytesToHex(bytes), "hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
function bufferToBytes(buf) {
|
|
||||||
return forge.util.hexToBytes(buf.toString("hex"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function bytesToBase64(bytes) {
|
|
||||||
return util.b64enc(bytesToBuffer(bytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64ToBytes(base64) {
|
|
||||||
return bufferToBytes(util.b64dec(base64));
|
|
||||||
}
|
|
||||||
|
|
||||||
function bnToBase64(bn) {
|
|
||||||
var hex = bn.toString(16);
|
|
||||||
if (hex.length % 2 === 1) { hex = "0" + hex; }
|
|
||||||
return util.b64enc(new Buffer(hex, "hex"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64ToBn(base64) {
|
|
||||||
return new forge.jsbn.BigInteger(util.b64dec(base64).toString("hex"), 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
function importPrivateKey(privateKey) {
|
|
||||||
return forge.pki.rsa.setPrivateKey(
|
|
||||||
base64ToBn(privateKey.n),
|
|
||||||
base64ToBn(privateKey.e), base64ToBn(privateKey.d),
|
|
||||||
base64ToBn(privateKey.p), base64ToBn(privateKey.q),
|
|
||||||
base64ToBn(privateKey.dp),base64ToBn(privateKey.dq),
|
|
||||||
base64ToBn(privateKey.qi));
|
|
||||||
}
|
|
||||||
|
|
||||||
function importPublicKey(publicKey) {
|
|
||||||
return forge.pki.rsa.setPublicKey(
|
|
||||||
base64ToBn(publicKey.n),
|
|
||||||
base64ToBn(publicKey.e));
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportPrivateKey(privateKey) {
|
|
||||||
return {
|
|
||||||
"kty": "RSA",
|
|
||||||
"n": bnToBase64(privateKey.n),
|
|
||||||
"e": bnToBase64(privateKey.e),
|
|
||||||
"d": bnToBase64(privateKey.d),
|
|
||||||
"p": bnToBase64(privateKey.p),
|
|
||||||
"q": bnToBase64(privateKey.q),
|
|
||||||
"dp": bnToBase64(privateKey.dP),
|
|
||||||
"dq": bnToBase64(privateKey.dQ),
|
|
||||||
"qi": bnToBase64(privateKey.qInv)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportPublicKey(publicKey) {
|
|
||||||
return {
|
|
||||||
"kty": "RSA",
|
|
||||||
"n": bnToBase64(publicKey.n),
|
|
||||||
"e": bnToBase64(publicKey.e)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A note on formats:
|
|
||||||
// * Keys are always represented as JWKs
|
|
||||||
// * Signature objects are in ACME format
|
|
||||||
// * Certs and CSRs are base64-encoded
|
|
||||||
module.exports = {
|
|
||||||
///// RANDOM STRINGS
|
|
||||||
|
|
||||||
randomString: function(nBytes) {
|
|
||||||
return bytesToBase64(forge.random.getBytesSync(nBytes));
|
|
||||||
},
|
|
||||||
|
|
||||||
randomSerialNumber: function() {
|
|
||||||
return forge.util.bytesToHex(forge.random.getBytesSync(4));
|
|
||||||
},
|
|
||||||
|
|
||||||
newToken: function() {
|
|
||||||
return this.randomString(TOKEN_SIZE);
|
|
||||||
},
|
|
||||||
|
|
||||||
///// SHA-256
|
|
||||||
|
|
||||||
sha256: function(buf) {
|
|
||||||
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
||||||
},
|
|
||||||
|
|
||||||
///// KEY PAIR MANAGEMENT
|
|
||||||
|
|
||||||
generateKeyPair: function(bits) {
|
|
||||||
var keyPair = forge.pki.rsa.generateKeyPair({bits: bits, e: 0x10001});
|
|
||||||
return {
|
|
||||||
privateKey: exportPrivateKey(keyPair.privateKey),
|
|
||||||
publicKey: exportPublicKey(keyPair.publicKey)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
importPemPrivateKey: function(pem) {
|
|
||||||
var key = forge.pki.privateKeyFromPem(pem);
|
|
||||||
return {
|
|
||||||
privateKey: exportPrivateKey(key),
|
|
||||||
publicKey: exportPublicKey(key)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
importPemCertificate: function(pem) {
|
|
||||||
return forge.pki.certificateFromPem(pem);
|
|
||||||
},
|
|
||||||
|
|
||||||
privateKeyToPem: function(privateKey) {
|
|
||||||
var priv = importPrivateKey(privateKey);
|
|
||||||
return forge.pki.privateKeyToPem(priv);
|
|
||||||
},
|
|
||||||
|
|
||||||
certificateToPem: function(certificate) {
|
|
||||||
var derCert = base64ToBytes(certificate);
|
|
||||||
var cert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
|
|
||||||
return forge.pki.certificateToPem(cert);
|
|
||||||
},
|
|
||||||
|
|
||||||
certificateRequestToPem: function(csr) {
|
|
||||||
var derReq = base64ToBytes(csr);
|
|
||||||
var c = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derReq));
|
|
||||||
return forge.pki.certificateRequestToPem(c);
|
|
||||||
},
|
|
||||||
|
|
||||||
thumbprint: function(publicKey) {
|
|
||||||
// Only handling RSA keys
|
|
||||||
var input = bytesToBuffer('{"e":"'+ publicKey.e + '","kty":"RSA","n":"'+ publicKey.n +'"}');
|
|
||||||
return util.b64enc(crypto.createHash('sha256').update(input).digest());
|
|
||||||
},
|
|
||||||
|
|
||||||
///// SIGNATURE GENERATION / VERIFICATION
|
|
||||||
|
|
||||||
generateSignature: function(keyPair, payload, nonce) {
|
|
||||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
|
||||||
|
|
||||||
// Compute JWS signature
|
|
||||||
var protectedHeader = "";
|
|
||||||
if (nonce) {
|
|
||||||
protectedHeader = JSON.stringify({nonce: nonce});
|
|
||||||
}
|
|
||||||
var protected64 = util.b64enc(new Buffer(protectedHeader));
|
|
||||||
var payload64 = util.b64enc(payload);
|
|
||||||
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
|
|
||||||
var signatureInput = bufferToBytes(signatureInputBuf);
|
|
||||||
var md = forge.md.sha256.create();
|
|
||||||
md.update(signatureInput);
|
|
||||||
var sig = privateKey.sign(md);
|
|
||||||
|
|
||||||
return {
|
|
||||||
header: {
|
|
||||||
alg: "RS256",
|
|
||||||
jwk: keyPair.publicKey,
|
|
||||||
},
|
|
||||||
protected: protected64,
|
|
||||||
payload: payload64,
|
|
||||||
signature: util.b64enc(bytesToBuffer(sig)),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
verifySignature: function(jws) {
|
|
||||||
var key;
|
|
||||||
|
|
||||||
if (jws.protected) {
|
|
||||||
if (!jws.header) {
|
|
||||||
jws.header = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(jws.protected);
|
|
||||||
var protectedJSON = util.b64dec(jws.protected).toString();
|
|
||||||
console.log(protectedJSON);
|
|
||||||
var protectedObj = JSON.parse(protectedJSON);
|
|
||||||
for (key in protectedObj) {
|
|
||||||
jws.header[key] = protectedObj[key];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("error unmarshaling json: "+e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes validSignature(sig)
|
|
||||||
if (!jws.header.jwk || (jws.header.jwk.kty !== "RSA")) {
|
|
||||||
// Unsupported key type
|
|
||||||
console.log("Unsupported key type");
|
|
||||||
return false;
|
|
||||||
} else if (!jws.header.alg || !jws.header.alg.match(/^RS/)) {
|
|
||||||
// Unsupported algorithm
|
|
||||||
console.log("Unsupported alg: "+jws.header.alg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute signature input
|
|
||||||
var protected64 = (jws.protected)? jws.protected : "";
|
|
||||||
var payload64 = (jws.payload)? jws.payload : "";
|
|
||||||
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
|
|
||||||
var signatureInput = bufferToBytes(signatureInputBuf);
|
|
||||||
|
|
||||||
// Compute message digest
|
|
||||||
var md;
|
|
||||||
switch (jws.header.alg) {
|
|
||||||
case "RS1": md = forge.md.sha1.create(); break;
|
|
||||||
case "RS256": md = forge.md.sha256.create(); break;
|
|
||||||
case "RS384": md = forge.md.sha384.create(); break;
|
|
||||||
case "RS512": md = forge.md.sha512.create(); break;
|
|
||||||
default: return false; // Unsupported algorithm
|
|
||||||
}
|
|
||||||
md.update(signatureInput);
|
|
||||||
|
|
||||||
// Import the key and signature
|
|
||||||
var publicKey = importPublicKey(jws.header.jwk);
|
|
||||||
var sig = bufferToBytes(util.b64dec(jws.signature));
|
|
||||||
|
|
||||||
return publicKey.verify(md.digest().bytes(), sig);
|
|
||||||
},
|
|
||||||
|
|
||||||
///// CSR GENERATION / VERIFICATION
|
|
||||||
|
|
||||||
generateCSR: function(keyPair, names) {
|
|
||||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
|
||||||
var publicKey = importPublicKey(keyPair.publicKey);
|
|
||||||
|
|
||||||
// Create and sign the CSR
|
|
||||||
var csr = forge.pki.createCertificationRequest();
|
|
||||||
csr.publicKey = publicKey;
|
|
||||||
csr.setSubject([{ name: 'commonName', value: names[0] }]);
|
|
||||||
|
|
||||||
var sans = [];
|
|
||||||
var i;
|
|
||||||
|
|
||||||
for (i in names) {
|
|
||||||
sans.push({ type: 2, value: names[i] });
|
|
||||||
}
|
|
||||||
csr.setAttributes([{
|
|
||||||
name: 'extensionRequest',
|
|
||||||
extensions: [{name: 'subjectAltName', altNames: sans}]
|
|
||||||
}]);
|
|
||||||
|
|
||||||
csr.sign(privateKey, forge.md.sha256.create());
|
|
||||||
|
|
||||||
// Convert CSR -> DER -> Base64
|
|
||||||
var der = forge.asn1.toDer(forge.pki.certificationRequestToAsn1(csr));
|
|
||||||
return util.b64enc(bytesToBuffer(der));
|
|
||||||
},
|
|
||||||
|
|
||||||
verifiedCommonName: function(csr_b64) {
|
|
||||||
var der = bufferToBytes(util.b64dec(csr_b64));
|
|
||||||
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
|
|
||||||
|
|
||||||
if (!csr.verify()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i=0; i<csr.subject.attributes.length; ++i) {
|
|
||||||
if (csr.subject.attributes[i].name === "commonName") {
|
|
||||||
return csr.subject.attributes[i].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
///// CERTIFICATE GENERATION
|
|
||||||
|
|
||||||
// 'ca' parameter includes information about the CA
|
|
||||||
// {
|
|
||||||
// distinguishedName: /* forge-formatted DN */
|
|
||||||
// keyPair: {
|
|
||||||
// publicKey: /* JWK */
|
|
||||||
// privateKey: /* JWK */
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
generateCertificate: function(ca, serialNumber, csr_b64) {
|
|
||||||
var der = bufferToBytes(util.b64dec(csr_b64));
|
|
||||||
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
|
|
||||||
|
|
||||||
// Extract the public key and common name
|
|
||||||
var publicKey = csr.publicKey;
|
|
||||||
var commonName = null;
|
|
||||||
for (var i=0; i<csr.subject.attributes.length; ++i) {
|
|
||||||
if (csr.subject.attributes[i].name === "commonName") {
|
|
||||||
commonName = csr.subject.attributes[i].value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!commonName) { return false; }
|
|
||||||
|
|
||||||
// Create the certificate
|
|
||||||
var cert = forge.pki.createCertificate();
|
|
||||||
cert.publicKey = publicKey;
|
|
||||||
cert.serialNumber = serialNumber;
|
|
||||||
|
|
||||||
// 1-year validity
|
|
||||||
cert.validity.notBefore = new Date();
|
|
||||||
cert.validity.notAfter = new Date();
|
|
||||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
|
||||||
|
|
||||||
cert.setSubject([{ name: "commonName", value: commonName }]);
|
|
||||||
cert.setIssuer(ca.distinguishedName);
|
|
||||||
cert.setExtensions([
|
|
||||||
{ name: "basicConstraints", cA: false },
|
|
||||||
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
|
||||||
{ name: "extKeyUsage", serverAuth: true },
|
|
||||||
{ name: "subjectAltName", altNames: [{ type: 2, value: commonName }] }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Import signing key and sign
|
|
||||||
var privateKey = importPrivateKey(ca.keyPair.privateKey);
|
|
||||||
cert.sign(privateKey);
|
|
||||||
|
|
||||||
// Return base64-encoded DER
|
|
||||||
der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
|
|
||||||
return bytesToBuffer(der);
|
|
||||||
},
|
|
||||||
|
|
||||||
generateDvsniCertificate: function(keyPair, nonceName, zName) {
|
|
||||||
var cert = forge.pki.createCertificate();
|
|
||||||
cert.publicKey = importPublicKey(keyPair.publicKey);
|
|
||||||
cert.serialNumber = '01';
|
|
||||||
cert.validity.notBefore = new Date();
|
|
||||||
cert.validity.notAfter = new Date();
|
|
||||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
|
||||||
cert.setSubject([{ name: "commonName", value: nonceName }]);
|
|
||||||
cert.setIssuer([{ name: "commonName", value: nonceName }]);
|
|
||||||
cert.setExtensions([
|
|
||||||
{ name: "basicConstraints", cA: false },
|
|
||||||
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
|
||||||
{ name: "extKeyUsage", serverAuth: true },
|
|
||||||
{ name: "subjectAltName", altNames: [
|
|
||||||
{ type: 2, value: nonceName },
|
|
||||||
{ type: 2, value: zName }
|
|
||||||
]}
|
|
||||||
]);
|
|
||||||
cert.sign(importPrivateKey(keyPair.privateKey));
|
|
||||||
|
|
||||||
// Return base64-encoded DER, as above
|
|
||||||
var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
|
|
||||||
return util.b64enc(bytesToBuffer(der));
|
|
||||||
},
|
|
||||||
|
|
||||||
///// TLS CONTEXT GENERATION
|
|
||||||
|
|
||||||
createContext: function(keyPair, cert) {
|
|
||||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
|
||||||
var derCert = bufferToBytes(util.b64dec(cert));
|
|
||||||
var realCert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
|
|
||||||
return crypto.createCredentials({
|
|
||||||
key: forge.pki.privateKeyToPem(privateKey),
|
|
||||||
cert: forge.pki.certificateToPem(realCert)
|
|
||||||
}).context;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {};
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var crypto = require('crypto');
|
|
||||||
var ursa = require('ursa');
|
|
||||||
var forge = require('node-forge');
|
|
||||||
|
|
||||||
function binstr2b64(binstr) {
|
|
||||||
return new Buffer(binstr, 'binary').toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toAcmePrivateKey(privkeyPem) {
|
|
||||||
var forgePrivkey = forge.pki.privateKeyFromPem(privkeyPem);
|
|
||||||
|
|
||||||
return {
|
|
||||||
kty: "RSA"
|
|
||||||
, n: binstr2b64(forgePrivkey.n)
|
|
||||||
, e: binstr2b64(forgePrivkey.e)
|
|
||||||
, d: binstr2b64(forgePrivkey.d)
|
|
||||||
, p: binstr2b64(forgePrivkey.p)
|
|
||||||
, q: binstr2b64(forgePrivkey.q)
|
|
||||||
, dp: binstr2b64(forgePrivkey.dP)
|
|
||||||
, dq: binstr2b64(forgePrivkey.dQ)
|
|
||||||
, qi: binstr2b64(forgePrivkey.qInv)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateRsaKeypair(bitlen, exp, cb) {
|
|
||||||
var keypair = ursa.generatePrivateKey(bitlen /*|| 2048*/, exp /*65537*/);
|
|
||||||
var pems = {
|
|
||||||
publicKeyPem: keypair.toPublicPem().toString('ascii') // ascii PEM: ----BEGIN...
|
|
||||||
, privateKeyPem: keypair.toPrivatePem().toString('ascii') // ascii PEM: ----BEGIN...
|
|
||||||
};
|
|
||||||
|
|
||||||
// I would have chosen sha1 or sha2... but whatever
|
|
||||||
pems.publicKeyMd5 = crypto.createHash('md5').update(pems.publicKeyPem).digest('hex');
|
|
||||||
// json { n: ..., e: ..., iq: ..., etc }
|
|
||||||
pems.privateKeyJwk = toAcmePrivateKey(pems.privateKeyPem);
|
|
||||||
pems.privateKeyJson = pems.privateKeyJwk;
|
|
||||||
|
|
||||||
// TODO thumbprint
|
|
||||||
|
|
||||||
cb(null, pems);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseAccountPrivateKey(pkj, cb) {
|
|
||||||
Object.keys(pkj).forEach(function (key) {
|
|
||||||
pkj[key] = new Buffer(pkj[key], 'base64');
|
|
||||||
});
|
|
||||||
|
|
||||||
var priv;
|
|
||||||
|
|
||||||
try {
|
|
||||||
priv = ursa.createPrivateKeyFromComponents(
|
|
||||||
pkj.n // modulus
|
|
||||||
, pkj.e // exponent
|
|
||||||
, pkj.p
|
|
||||||
, pkj.q
|
|
||||||
, pkj.dp
|
|
||||||
, pkj.dq
|
|
||||||
, pkj.qi
|
|
||||||
, pkj.d
|
|
||||||
);
|
|
||||||
} catch(e) {
|
|
||||||
cb(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
privateKeyPem: priv.toPrivatePem.toString('ascii')
|
|
||||||
, publicKeyPem: priv.toPublicPem.toString('ascii')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.generateRsaKeypair = generateRsaKeypair;
|
|
||||||
module.exports.privateJwkToPems = parseAccountPrivateKey;
|
|
||||||
module.exports.privatePemToJwk = toAcmePrivateKey;
|
|
||||||
|
|
||||||
// TODO deprecate
|
|
||||||
module.exports.toAcmePrivateKey = toAcmePrivateKey;
|
|
||||||
module.exports.parseAccountPrivateKey = parseAccountPrivateKey;
|
|
||||||
48
lib/node.js
48
lib/node.js
@ -1,48 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var request = require('request');
|
|
||||||
var leUtils = require('./acme-util');
|
|
||||||
var leCrypto = require('./letsencrypt-node-crypto');
|
|
||||||
var leExtra = require('./letsencrypt-forge-extra');
|
|
||||||
var leForge = require('./letsencrypt-forge');
|
|
||||||
var leUrsa;
|
|
||||||
|
|
||||||
try {
|
|
||||||
leUrsa = require('./letsencrypt-ursa');
|
|
||||||
} catch(e) {
|
|
||||||
leUrsa = {};
|
|
||||||
// things will run a little slower on keygen, but it'll work on windows
|
|
||||||
// (but don't try this on raspberry pi - 20+ MINUTES key generation)
|
|
||||||
}
|
|
||||||
|
|
||||||
// order of crypto precdence is
|
|
||||||
// * native
|
|
||||||
// * ursa
|
|
||||||
// * forge extra (the new one aimed to be less-forgey)
|
|
||||||
// * forge (fallback)
|
|
||||||
Object.keys(leUrsa).forEach(function (key) {
|
|
||||||
if (!leCrypto[key]) {
|
|
||||||
leCrypto[key] = leUrsa[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(leExtra).forEach(function (key) {
|
|
||||||
if (!leCrypto[key]) {
|
|
||||||
leCrypto[key] = leExtra[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(leForge).forEach(function (key) {
|
|
||||||
if (!leCrypto[key]) {
|
|
||||||
leCrypto[key] = leForge[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.request = request;
|
|
||||||
module.exports.leCrypto = leCrypto;
|
|
||||||
module.exports.leUtils = leUtils;
|
|
||||||
@ -8,34 +8,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
module.exports.create = function (deps) {
|
module.exports.create = function (deps) {
|
||||||
var NOOP=function () {}, log=NOOP;
|
var NOOP=function () {}, log=NOOP;
|
||||||
var request=deps.request;
|
var acmeRequest = deps.acmeRequest;
|
||||||
var importPemPrivateKey=deps.leCrypto.importPemPrivateKey;
|
var RSA = deps.RSA;
|
||||||
var Acme = deps.Acme;
|
var Acme = deps.Acme;
|
||||||
|
|
||||||
function registerNewAccount(options, cb) {
|
function registerNewAccount(options, cb) {
|
||||||
var state = {};
|
|
||||||
|
|
||||||
if (!options.accountPrivateKeyPem) {
|
|
||||||
return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem"));
|
|
||||||
}
|
|
||||||
if (!options.agreeToTerms) {
|
|
||||||
cb(new Error("options.agreeToTerms must be function (tosUrl, fn => (err, true))"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!options.newRegUrl) {
|
|
||||||
cb(new Error("options.newRegUrl must be the a new registration url"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!options.email) {
|
|
||||||
cb(new Error("options.email must be an email"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.accountKeyPem=options.accountPrivateKeyPem;
|
|
||||||
state.accountKeyPair=importPemPrivateKey(state.accountKeyPem);
|
|
||||||
state.acme=new Acme(state.accountKeyPair);
|
|
||||||
|
|
||||||
register();
|
|
||||||
|
|
||||||
function register() {
|
function register() {
|
||||||
state.acme.post(options.newRegUrl, {
|
state.acme.post(options.newRegUrl, {
|
||||||
@ -47,7 +24,11 @@ module.exports.create = function (deps) {
|
|||||||
function getTerms(err, res) {
|
function getTerms(err, res) {
|
||||||
var links;
|
var links;
|
||||||
|
|
||||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
if (err) {
|
||||||
|
return handleErr(err, 'Registration request failed: ' + err.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.floor(res.statusCode/100)!==2) {
|
||||||
return handleErr(err, 'Registration request failed: ' + res.body.toString('utf8'));
|
return handleErr(err, 'Registration request failed: ' + res.body.toString('utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +55,7 @@ module.exports.create = function (deps) {
|
|||||||
state.agreeTerms = agree;
|
state.agreeTerms = agree;
|
||||||
state.termsUrl=links['terms-of-service'];
|
state.termsUrl=links['terms-of-service'];
|
||||||
log(state.termsUrl);
|
log(state.termsUrl);
|
||||||
request.get(state.termsUrl, getAgreement);
|
acmeRequest.create().get(state.termsUrl, getAgreement);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cb(null, null);
|
cb(null, null);
|
||||||
@ -106,20 +87,12 @@ module.exports.create = function (deps) {
|
|||||||
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
||||||
}
|
}
|
||||||
|
|
||||||
data = body;
|
if ('string' === typeof body || '{' === body[0] || '{' === String.fromCharCode(body[0])) {
|
||||||
// handle for node and browser
|
|
||||||
if ('string' === typeof body) {
|
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(body);
|
data = JSON.parse(body.toString('utf8'));
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// ignore
|
cb(e);
|
||||||
}
|
return;
|
||||||
} else {
|
|
||||||
// might be a buffer
|
|
||||||
data = body.toString('utf8');
|
|
||||||
if (!(data.length > 10)) {
|
|
||||||
// probably json
|
|
||||||
data = body;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +104,33 @@ module.exports.create = function (deps) {
|
|||||||
log(text, err, info);
|
log(text, err, info);
|
||||||
cb(err || new Error(text));
|
cb(err || new Error(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var state = {};
|
||||||
|
|
||||||
|
if (!options.accountKeypair) {
|
||||||
|
if (!options.accountPrivateKeyPem) {
|
||||||
|
return handleErr(new Error("options.accountKeypair must be an object with `privateKeyPem` and/or `privateKeyJwk`"));
|
||||||
|
}
|
||||||
|
console.warn("'accountPrivateKeyPem' is deprecated. Use options.accountKeypair.privateKeyPem instead.");
|
||||||
|
options.accountKeypair = RSA.import({ privateKeyPem: options.accountPrivateKeyPem });
|
||||||
|
}
|
||||||
|
if (!options.agreeToTerms) {
|
||||||
|
cb(new Error("options.agreeToTerms must be function (tosUrl, fn => (err, true))"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!options.newRegUrl) {
|
||||||
|
cb(new Error("options.newRegUrl must be the a new registration url"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!options.email) {
|
||||||
|
cb(new Error("options.email must be an email"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.accountKeypair = options.accountKeypair;
|
||||||
|
state.acme=new Acme(state.accountKeypair);
|
||||||
|
|
||||||
|
register();
|
||||||
}
|
}
|
||||||
|
|
||||||
return registerNewAccount;
|
return registerNewAccount;
|
||||||
|
|||||||
60
node.js
60
node.js
@ -1,35 +1,53 @@
|
|||||||
/*!
|
/*!
|
||||||
* letiny-core
|
* letiny-core
|
||||||
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
|
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var defaults = {
|
||||||
|
productionServerUrl: 'https://acme-v01.api.letsencrypt.org/directory'
|
||||||
|
, stagingServerUrl: 'https://acme-staging.api.letsencrypt.org/directory'
|
||||||
|
, acmeChallengePrefix: '/.well-known/acme-challenge/'
|
||||||
|
, knownEndpoints: [ 'new-authz', 'new-cert', 'new-reg', 'revoke-cert', 'key-change' ]
|
||||||
|
, challengeType: 'http-01'
|
||||||
|
, rsaKeySize: 2048
|
||||||
|
};
|
||||||
|
|
||||||
function create(deps) {
|
function create(deps) {
|
||||||
var LeCore = {};
|
deps = deps || {};
|
||||||
|
deps.LeCore = {};
|
||||||
|
|
||||||
// Note: these are NOT DEFAULTS
|
Object.keys(defaults).forEach(function (key) {
|
||||||
// They are de facto standards that you may
|
deps[key] = defaults[key];
|
||||||
// or may not use in your implementation
|
deps.LeCore[key] = defaults[key];
|
||||||
LeCore.productionServerUrl = "https://acme-v01.api.letsencrypt.org/directory";
|
});
|
||||||
LeCore.stagingServerUrl = "https://acme-staging.api.letsencrypt.org/directory";
|
|
||||||
LeCore.acmeChallengePrefix = "/.well-known/acme-challenge/";
|
|
||||||
LeCore.configDir = "/etc/letsencrypt/";
|
|
||||||
LeCore.logsDir = "/var/log/letsencrypt/";
|
|
||||||
LeCore.workDir = "/var/lib/letsencrypt/";
|
|
||||||
LeCore.knownEndpoints = ['new-authz', 'new-cert', 'new-reg', 'revoke-cert'];
|
|
||||||
|
|
||||||
deps.LeCore = LeCore;
|
deps.RSA = deps.RSA || require('rsa-compat').RSA;
|
||||||
deps.Acme = LeCore.Acme = require('./lib/acme-client').create(deps);
|
deps.acmeRequest = require('./lib/le-acme-request');
|
||||||
|
deps.Acme = require('./lib/acme-client').create(deps);
|
||||||
|
|
||||||
LeCore.getAcmeUrls = require('./lib/get-acme-urls').create(deps);
|
deps.LeCore.Acme = deps.Acme;
|
||||||
LeCore.registerNewAccount = require('./lib/register-new-account').create(deps);
|
deps.LeCore.acmeRequest = deps.acmeRequest;
|
||||||
LeCore.getCertificate = require('./lib/get-certificate').create(deps);
|
deps.LeCore.getAcmeUrls = require('./lib/get-acme-urls').create(deps);
|
||||||
|
deps.LeCore.registerNewAccount = require('./lib/register-new-account').create(deps);
|
||||||
|
deps.LeCore.getCertificate = require('./lib/get-certificate').create(deps);
|
||||||
|
deps.LeCore.getOptions = function () {
|
||||||
|
var defs = {};
|
||||||
|
|
||||||
LeCore.leCrypto = deps.leCrypto;
|
Object.keys(defaults).forEach(function (key) {
|
||||||
|
defs[key] = defs[deps] || defaults[key];
|
||||||
|
});
|
||||||
|
|
||||||
return LeCore;
|
return defs;
|
||||||
|
};
|
||||||
|
|
||||||
|
return deps.LeCore;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = create(require('./lib/node'));
|
// TODO make this the official usage
|
||||||
module.exports.create = create;
|
module.exports.ACME = { create: create };
|
||||||
|
|
||||||
|
Object.keys(defaults).forEach(function (key) {
|
||||||
|
module.exports.ACME[key] = defaults[key];
|
||||||
|
});
|
||||||
|
|||||||
29
package.json
29
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "letiny-core",
|
"name": "le-acme-core",
|
||||||
"version": "1.0.1",
|
"version": "2.1.4",
|
||||||
"description": "A framework for building letsencrypt clients, forked from letiny",
|
"description": "A framework for building letsencrypt clients, forked from letiny",
|
||||||
"main": "node.js",
|
"main": "node.js",
|
||||||
"browser": "browser.js",
|
"browser": "browser.js",
|
||||||
@ -8,35 +8,34 @@
|
|||||||
"example": "example",
|
"example": "example",
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"test": "node example/letsencrypt.js"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Daplie/letiny-core.git"
|
"url": "git+https://git.coolaj86.com/coolaj86/le-acme-core.js.git"
|
||||||
},
|
},
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Daplie/letiny-core/issues"
|
"url": "https://git.coolaj86.com/coolaj86/le-acme-core.js/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Daplie/letiny-core#readme",
|
"homepage": "https://git.coolaj86.com/coolaj86/le-acme-core.js#readme",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"le-acme",
|
||||||
|
"le-acme-",
|
||||||
"tiny",
|
"tiny",
|
||||||
"acme",
|
"acme",
|
||||||
"letsencrypt",
|
"letsencrypt",
|
||||||
"client",
|
"client",
|
||||||
"pem",
|
"pem",
|
||||||
|
"jwk",
|
||||||
"pfx"
|
"pfx"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-forge": "^0.6.38",
|
"request": "^2.74.0",
|
||||||
"request": "^2.55.0"
|
"rsa-compat": "^1.3.2"
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"ursa": "^0.9.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "^2.3.3",
|
"better-assert": "^1.0.2",
|
||||||
"better-assert": "^1.0.2"
|
"chai": "^3.5.0",
|
||||||
|
"chai-string": "^1.3.0",
|
||||||
|
"request-debug": "^0.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
test/test-request.js
Normal file
74
test/test-request.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*!
|
||||||
|
* le-acme-core
|
||||||
|
* Author: Kelly Johnson
|
||||||
|
* Copyright 2017
|
||||||
|
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const acmeRequest = require('../lib/le-acme-request');
|
||||||
|
const debugRequest = require('request-debug');
|
||||||
|
const chai = require('chai');
|
||||||
|
chai.use(require('chai-string'));
|
||||||
|
const expect = chai.expect;
|
||||||
|
|
||||||
|
const productId = 'Greenlock';
|
||||||
|
const UA = 'User-Agent';
|
||||||
|
|
||||||
|
function checkRequest(req, done, tester) {
|
||||||
|
debugRequest(req, function dbg(type, data, r) {
|
||||||
|
if (type !== 'request') return; // Only interested in the request
|
||||||
|
expect(data.headers).to.have.property(UA);
|
||||||
|
let uaString = data.headers[UA];
|
||||||
|
tester(uaString);
|
||||||
|
req.stopDebugging();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
req('http://www.google.com', function (error, response, body) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('le-acme-request', function () {
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
acmeRequest.resetUa();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build User-Agent string', function () {
|
||||||
|
let uaString = acmeRequest.getUaString();
|
||||||
|
expect(uaString).to.startsWith(productId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have proper User-Agent in request', function (done) {
|
||||||
|
let request = acmeRequest.create();
|
||||||
|
checkRequest(request, done, function (uaString) {
|
||||||
|
expect(uaString).to.startsWith(productId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add custom string to User Agent', function (done) {
|
||||||
|
let testStr = 'check it';
|
||||||
|
acmeRequest.addUaString(testStr);
|
||||||
|
let request = acmeRequest.create();
|
||||||
|
checkRequest(request, done, function (uaString) {
|
||||||
|
// Added space to ensure str was properly appended
|
||||||
|
expect(uaString).to.endsWith(` ${testStr}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove all items from User Agent', function (done) {
|
||||||
|
acmeRequest.omitUaProperties({all: true});
|
||||||
|
let request = acmeRequest.create();
|
||||||
|
checkRequest(request, done, function (uaString) {
|
||||||
|
expect(uaString).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove one item from User Agent', function (done) {
|
||||||
|
acmeRequest.omitUaProperties({pkg: true});
|
||||||
|
const request = acmeRequest.create();
|
||||||
|
checkRequest(request, done, function (uaString) {
|
||||||
|
expect(uaString).to.not.have.string(productId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,5 @@
|
|||||||
var forge=require('node-forge'), assert=require('better-assert'), fs=require('fs'),
|
var forge=require('node-forge'), assert=require('better-assert'), fs=require('fs'),
|
||||||
letiny=require('../lib/client'), config=require('./config.json'),
|
letiny=require('../'), config=require('./config.json'),
|
||||||
res, newReg='https://acme-staging.api.letsencrypt.org/acme/new-reg';
|
res, newReg='https://acme-staging.api.letsencrypt.org/acme/new-reg';
|
||||||
|
|
||||||
config.newReg=config.newReg || newReg;
|
config.newReg=config.newReg || newReg;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user