Compare commits
No commits in common. "master" and "v0.0.3-beta" have entirely different histories.
master
...
v0.0.3-bet
33
.gitignore
vendored
33
.gitignore
vendored
@ -1,33 +0,0 @@
|
|||||||
letsencrypt.work
|
|
||||||
letsencrypt.logs
|
|
||||||
letsencrypt.config
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directory
|
|
||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
|
||||||
node_modules
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
3
AUTHORS
3
AUTHORS
@ -1,3 +0,0 @@
|
|||||||
ISRG
|
|
||||||
Anatol Sommer <anatol@anatol.at>
|
|
||||||
AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)
|
|
||||||
17
History.md
17
History.md
@ -1,20 +1,3 @@
|
|||||||
1.0.0 / 2015-12-15
|
|
||||||
==================
|
|
||||||
|
|
||||||
Stripped down to core components,
|
|
||||||
included example, and tested.
|
|
||||||
|
|
||||||
0.0.5-beta / 2015-12
|
|
||||||
=======================
|
|
||||||
|
|
||||||
* Added fork option
|
|
||||||
* Added accountKey and privateKey options
|
|
||||||
|
|
||||||
0.0.4-beta / 2015-12-13
|
|
||||||
=======================
|
|
||||||
|
|
||||||
* Small code improvements
|
|
||||||
|
|
||||||
0.0.3-beta / 2015-12-13
|
0.0.3-beta / 2015-12-13
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
|||||||
387
README.md
387
README.md
@ -1,333 +1,94 @@
|
|||||||
# le-acme-core
|
# letiny
|
||||||
|
Tiny acme client library and CLI to obtain ssl certificates (without using external commands like openssl).
|
||||||
|
|
||||||
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 -->
|
## Usage:
|
||||||
|
`npm install letiny`
|
||||||
|
|
||||||
A framework for building letsencrypt clients, forked from `letiny`.
|
|
||||||
|
|
||||||
Supports all of:
|
### Using the "webroot" option
|
||||||
|
This will create a file in `/var/www/example.com/.well-known/acme-challenge/` to verify the domain.
|
||||||
* node with `ursa` (works fast)
|
```js
|
||||||
* node with `forge` (works on windows)
|
require('letiny').getCert({
|
||||||
* browser WebCrypto (not implemented, but... Let's Encrypt over WebRTC anyone?)
|
email:'me@example.com',
|
||||||
* any javascript implementation
|
domains:['example.com', 'www.example.com'],
|
||||||
|
webroot:'/var/www/example.com',
|
||||||
# NEW: Let's Encrypt v2 Support
|
certFile:'./cert.pem',
|
||||||
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)
|
keyFile:'./key.pem',
|
||||||
|
caFile:'./ca.pem',
|
||||||
### These aren't the droids you're looking for
|
agreeTerms:true
|
||||||
|
}, function(err, cert, key, cacert) {
|
||||||
This is a library / framework for building letsencrypt clients.
|
console.log(err || cert+'\n'+key+'\n'+cacert);
|
||||||
You probably want one of these pre-built clients instead:
|
|
||||||
|
|
||||||
* [`letsencrypt`](https://git.coolaj86.com/coolaj86/greenlock.js) (compatible with the official client)
|
|
||||||
* `letiny` (lightweight client cli)
|
|
||||||
* [`letsencrypt-express`](https://git.coolaj86.com/coolaj86/greenlock-express.js) (automatic https for express)
|
|
||||||
|
|
||||||
## Install & Usage:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
### Using the "challenge" option
|
||||||
|
This allows you to provide the challenge data on your own, so you can obtain certificates on-the-fly within your software.
|
||||||
* discover ACME registration urls with `getAcmeUrls`
|
```js
|
||||||
* register a user account with `registerNewAccount`
|
require('letiny').getCert({
|
||||||
* implement a method to agree to the terms of service as `agreeToTos`
|
email:'me@example.com',
|
||||||
* get certificates with `getCertificate`
|
domains:'example.com',
|
||||||
* implement a method to store the challenge token as `setChallenge`
|
challenge:function(domain, path, data, done) {
|
||||||
* implement a method to get the challenge token as `getChallenge`
|
// make http://+domain+path serving "data"
|
||||||
* implement a method to remove the challenge token as `removeChallenge`
|
done();
|
||||||
|
},
|
||||||
### Demo
|
certFile:'./cert.pem',
|
||||||
|
keyFile:'./key.pem',
|
||||||
You can see this working for yourself, but you'll need to be on an internet connected computer with a domain.
|
caFile:'./ca.pem',
|
||||||
|
agreeTerms:true
|
||||||
Get a temporary domain for testing
|
}, function(err, cert, key, cacert) {
|
||||||
|
console.log(err || cert+'\n'+key+'\n'+cacert);
|
||||||
```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
|
|
||||||
|
|
||||||
This is how you **register an ACME account** and **get an HTTPS certificate**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var ACME = require('le-acme-core').ACME.create();
|
|
||||||
var RSA = require('rsa-compat').RSA;
|
|
||||||
|
|
||||||
var email = 'user@example.com'; // CHANGE TO YOUR EMAIL
|
|
||||||
var domains = 'example.com'; // CHANGE TO YOUR DOMAIN
|
|
||||||
var acmeDiscoveryUrl = ACME.stagingServerUrl; // CHANGE to production, when ready
|
|
||||||
|
|
||||||
var accountKeypair = null; // { privateKeyPem: null, privateKeyJwk: null };
|
|
||||||
var domainKeypair = null; // same as above
|
|
||||||
var acmeUrls = null;
|
|
||||||
|
|
||||||
RSA.generateKeypair(2048, 65537, function (err, keypair) {
|
|
||||||
accountKeypair = keypair;
|
|
||||||
// ...
|
|
||||||
ACME.getAcmeUrls(acmeDiscoveryUrl, function (err, urls) {
|
|
||||||
// ...
|
|
||||||
runDemo();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function runDemo() {
|
|
||||||
ACME.registerNewAccount(
|
|
||||||
{ newRegUrl: acmeUrls.newReg
|
|
||||||
, email: email
|
|
||||||
, accountKeypair: accountKeypair
|
|
||||||
, agreeToTerms: function (tosUrl, done) {
|
|
||||||
|
|
||||||
// agree to the exact version of these terms
|
|
||||||
done(null, tosUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, function (err, regr) {
|
|
||||||
|
|
||||||
ACME.getCertificate(
|
|
||||||
{ newAuthzUrl: acmeUrls.newAuthz
|
|
||||||
, newCertUrl: acmeUrls.newCert
|
|
||||||
|
|
||||||
, domainKeypair: domainKeypair
|
|
||||||
, accountKeypair: accountKeypair
|
|
||||||
, domains: domains
|
|
||||||
|
|
||||||
, setChallenge: challengeStore.set
|
|
||||||
, removeChallenge: challengeStore.remove
|
|
||||||
}
|
|
||||||
, function (err, certs) {
|
|
||||||
|
|
||||||
// Note: you should save certs to disk (or db)
|
|
||||||
certStore.set(domains[0], certs, function () {
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var https = require('https');
|
|
||||||
var http = require('http');
|
|
||||||
|
|
||||||
|
|
||||||
var LeCore = deps.LeCore;
|
|
||||||
var tlsOptions = deps.tlsOptions;
|
|
||||||
var challengeStore = deps.challengeStore;
|
|
||||||
var certStore = deps.certStore;
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Challenge Handler
|
|
||||||
//
|
|
||||||
function acmeResponder(req, res) {
|
|
||||||
if (0 !== req.url.indexOf(LeCore.acmeChallengePrefix)) {
|
|
||||||
res.end('Hello World!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = req.url.slice(LeCore.acmeChallengePrefix.length);
|
|
||||||
|
|
||||||
challengeStore.get(req.hostname, key, function (err, val) {
|
|
||||||
res.end(val || 'Error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Server
|
|
||||||
//
|
|
||||||
https.createServer(tlsOptions, acmeResponder).listen(5001, function () {
|
|
||||||
console.log('Listening https on', this.address());
|
|
||||||
});
|
|
||||||
http.createServer(acmeResponder).listen(80, function () {
|
|
||||||
console.log('Listening http on', this.address());
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**But wait**, there's more!
|
### Options
|
||||||
See [example/serve.js](https://git.coolaj86.com/coolaj86/le-acme-core.js/blob/master/example/serve.js)
|
#### Required:
|
||||||
|
* `email`: Your email adress
|
||||||
|
* `domains`: Comma seperated string or array
|
||||||
|
* `agreeTerms`: You need to agree the terms
|
||||||
|
* `webroot` (string) or `challenge` (function)
|
||||||
|
|
||||||
#### Put some storage in place
|
#### Optional:
|
||||||
|
* `certFile`: Path to save certificate
|
||||||
|
* `keyFile`: Path to save private key
|
||||||
|
* `caFile`: Path to save issuer certificate
|
||||||
|
* `pfxFile`: Path to save PKCS#12 certificate
|
||||||
|
* `pfxPassword`: Password for PKCS#12 certificate
|
||||||
|
* `aes`: (boolean), use AES instead of 3DES for PKCS#12 certificate
|
||||||
|
* `newReg`: URL, use *https://acme-staging.api.letsencrypt.org/acme/new-reg* for testing
|
||||||
|
|
||||||
Finally, you need an implementation of `challengeStore`:
|
|
||||||
|
|
||||||
```javascript
|
## Command line interface
|
||||||
var challengeCache = {};
|
```sudo npm install letiny -g```
|
||||||
var challengeStore = {
|
#### Options:
|
||||||
set: function (hostname, key, value, cb) {
|
```
|
||||||
challengeCache[key] = value;
|
-h, --help output usage information
|
||||||
cb(null);
|
-e, --email <email> your email address
|
||||||
}
|
-w, --webroot <path> path for webroot verification
|
||||||
, get: function (hostname, key, cb) {
|
-m, --manual use manual verification
|
||||||
cb(null, challengeCache[key]);
|
-d, --domains <domains> domains (comma seperated)
|
||||||
}
|
-c, --cert <path> path to save your certificate (cert.pem)
|
||||||
, remove: function (hostname, key, cb) {
|
-k, --key <path> path to save your private key (privkey.pem)
|
||||||
delete challengeCache[key];
|
-i, --ca <path> path to save issuer certificate (cacert.pem)
|
||||||
cb(null);
|
--pfx <path> path to save PKCS#12 certificate (optional)
|
||||||
}
|
--password <password> password for PKCS#12 certificate (optional)
|
||||||
};
|
--aes use AES instead of 3DES for PKCS#12
|
||||||
|
--agree agree terms of the ACME CA (required)
|
||||||
|
--newreg <URL> optional AMCE server newReg URL
|
||||||
|
--debug print debug information
|
||||||
|
```
|
||||||
|
When --pfx is used without --cert, --key and --ca no .pem files will be created.
|
||||||
|
|
||||||
var certCache = {};
|
#### Examples:
|
||||||
var certStore = {
|
```
|
||||||
set: function (hostname, certs, cb) {
|
letiny -e me@example.com -w /var/www/example.com -d example.com --agree
|
||||||
certCache[hostname] = certs;
|
letiny -e me@example.com -m -d example.com -c cert.pem -k key.pem -i ca.pem --agree
|
||||||
cb(null);
|
letiny -e me@example.com -m -d example.com,www.example.com --agree
|
||||||
}
|
letiny -e me@example.com -m -d example.com --pfx cert.pfx --password secret --agree
|
||||||
, get: function (hostname, cb) {
|
letiny --email me@example.com --webroot ./ --domains example.com --agree
|
||||||
cb(null, certCache[hostname]);
|
|
||||||
}
|
|
||||||
, remove: function (hostname, cb) {
|
|
||||||
delete certCache[hostname];
|
|
||||||
cb(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**But wait**, there's more!
|
|
||||||
See
|
|
||||||
|
|
||||||
* [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)
|
|
||||||
|
|
||||||
## Authors
|
|
||||||
|
|
||||||
* ISRG
|
|
||||||
* Anatol Sommer (https://github.com/anatolsommer)
|
|
||||||
* AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com)
|
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
MPL 2.0
|
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`
|
|
||||||
and are made available under MIT and Apache-2.0 as well (check file headers).
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
throw new Error("NOT IMPLEMENTED");
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// It's good to have a place to store the certificates so you can,
|
|
||||||
// y'know, use them! :-)
|
|
||||||
|
|
||||||
// you receive a hostname and must give back an object
|
|
||||||
// with a public cert chain and a private key
|
|
||||||
|
|
||||||
var certCache = {};
|
|
||||||
var certStore = {
|
|
||||||
set: function (hostname, certs, cb) {
|
|
||||||
certCache[hostname] = certs;
|
|
||||||
cb(null);
|
|
||||||
}
|
|
||||||
, get: function (hostname, cb) {
|
|
||||||
cb(null, certCache[hostname]);
|
|
||||||
}
|
|
||||||
, remove: function (hostname, cb) {
|
|
||||||
delete certCache[hostname];
|
|
||||||
cb(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = certStore;
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// Finally, you need an implementation of `challengeStore`:
|
|
||||||
|
|
||||||
// Note:
|
|
||||||
// key is the xxxx part of `/.well-known/acme-challenge/xxxx`
|
|
||||||
// value is what is needs to be return the the requesting server
|
|
||||||
//
|
|
||||||
// it is very common to store this is a directory as a file
|
|
||||||
// (and you can totally do that if you want to, no big deal)
|
|
||||||
// but that's super inefficient considering that you need it
|
|
||||||
// for all of 500ms and there's no sense in that.
|
|
||||||
|
|
||||||
var challengeCache = {};
|
|
||||||
var challengeStore = {
|
|
||||||
set: function (hostname, key, value, cb) {
|
|
||||||
challengeCache[key] = value;
|
|
||||||
cb(null);
|
|
||||||
}
|
|
||||||
, get: function (hostname, key, cb) {
|
|
||||||
cb(null, challengeCache[key]);
|
|
||||||
}
|
|
||||||
, remove: function (hostname, key, cb) {
|
|
||||||
delete challengeCache[key];
|
|
||||||
cb(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = challengeStore;
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
//var LeCore = require('letiny-core');
|
|
||||||
var LeCore = require('../').ACME.create();
|
|
||||||
|
|
||||||
var email = process.argv[2] || 'user@example.com'; // CHANGE TO YOUR EMAIL
|
|
||||||
var domains = [process.argv[3] || 'example.com']; // CHANGE TO YOUR DOMAIN
|
|
||||||
var acmeDiscoveryUrl = LeCore.stagingServerUrl;
|
|
||||||
|
|
||||||
var challengeStore = require('./challenge-store');
|
|
||||||
var certStore = require('./cert-store');
|
|
||||||
var serve = require('./serve');
|
|
||||||
var closer;
|
|
||||||
|
|
||||||
var accountKeypair = null;
|
|
||||||
var domainKeypair = null;
|
|
||||||
var acmeUrls = null;
|
|
||||||
|
|
||||||
|
|
||||||
console.log('Using server', acmeDiscoveryUrl);
|
|
||||||
console.log('Creating account for', email, 'and registering certificates for', domains, 'to that account');
|
|
||||||
init();
|
|
||||||
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
getPrivateKeys(function () {
|
|
||||||
|
|
||||||
console.log('Getting Acme Urls');
|
|
||||||
LeCore.getAcmeUrls(acmeDiscoveryUrl, function (err, urls) {
|
|
||||||
// in production choose LeCore.productionServerUrl
|
|
||||||
|
|
||||||
console.log('Got Acme Urls', err, urls);
|
|
||||||
acmeUrls = urls;
|
|
||||||
runDemo();
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrivateKeys(cb) {
|
|
||||||
console.log('Generating Account Keypair');
|
|
||||||
const RSA = require('rsa-compat').RSA;
|
|
||||||
RSA.generateKeypair(2048, 65537, {}, function (err, pems) {
|
|
||||||
|
|
||||||
accountKeypair = pems;
|
|
||||||
console.log('Generating Domain Keypair');
|
|
||||||
RSA.generateKeypair(2048, 65537, {}, function (err, pems2) {
|
|
||||||
|
|
||||||
domainKeypair = pems2;
|
|
||||||
cb();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function runDemo() {
|
|
||||||
console.log('Registering New Account');
|
|
||||||
LeCore.registerNewAccount(
|
|
||||||
{ newRegUrl: acmeUrls.newReg
|
|
||||||
, email: email
|
|
||||||
, accountKeypair: accountKeypair
|
|
||||||
, agreeToTerms: function (tosUrl, done) {
|
|
||||||
|
|
||||||
// agree to the exact version of these terms
|
|
||||||
console.log('[tosUrl]:', tosUrl);
|
|
||||||
done(null, tosUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, function (err, regr) {
|
|
||||||
|
|
||||||
// Note: you should save the registration
|
|
||||||
// record to disk (or db)
|
|
||||||
console.log('[regr]');
|
|
||||||
console.log(err || regr);
|
|
||||||
|
|
||||||
console.log('Registering New Certificate');
|
|
||||||
LeCore.getCertificate(
|
|
||||||
{ newAuthzUrl: acmeUrls.newAuthz
|
|
||||||
, newCertUrl: acmeUrls.newCert
|
|
||||||
|
|
||||||
, domainKeypair: domainKeypair
|
|
||||||
, accountKeypair: accountKeypair
|
|
||||||
, domains: domains
|
|
||||||
|
|
||||||
, setChallenge: challengeStore.set
|
|
||||||
, removeChallenge: challengeStore.remove
|
|
||||||
}
|
|
||||||
, function (err, certs) {
|
|
||||||
|
|
||||||
// Note: you should save certs to disk (or db)
|
|
||||||
certStore.set(domains[0], certs, function () {
|
|
||||||
|
|
||||||
console.log('[certs]');
|
|
||||||
console.log(err || certs);
|
|
||||||
closer();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Setup the Server
|
|
||||||
//
|
|
||||||
closer = serve.init({
|
|
||||||
LeCore: LeCore
|
|
||||||
, tlsOptions: {}
|
|
||||||
, challengeStore: challengeStore
|
|
||||||
, certStore: certStore
|
|
||||||
});
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
module.exports.init = function (deps) {
|
|
||||||
var tls = require('tls');
|
|
||||||
var https = require('https');
|
|
||||||
var http = require('http');
|
|
||||||
|
|
||||||
|
|
||||||
var LeCore = deps.LeCore;
|
|
||||||
var tlsOptions = deps.tlsOptions || deps.httpsOptions;
|
|
||||||
var challengeStore = deps.challengeStore;
|
|
||||||
var certStore = deps.certStore;
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Challenge Handler
|
|
||||||
//
|
|
||||||
function acmeResponder(req, res) {
|
|
||||||
if (0 !== req.url.indexOf(LeCore.acmeChallengePrefix)) {
|
|
||||||
res.end('Hello World!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = req.url.slice(LeCore.acmeChallengePrefix.length);
|
|
||||||
|
|
||||||
challengeStore.get(req.hostname, key, function (err, val) {
|
|
||||||
res.end(val || 'Error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// SNI Cert Handler
|
|
||||||
//
|
|
||||||
function certGetter(hostname, cb) {
|
|
||||||
console.log('SNICallback says hello!', hostname);
|
|
||||||
certStore.get(hostname, function (err, certs) {
|
|
||||||
if (!certs) {
|
|
||||||
cb(null, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: you should cache this context in memory
|
|
||||||
// so that you aren't creating a new one every time
|
|
||||||
var context = tls.createSecureContext({
|
|
||||||
cert: certs.cert.toString('ascii') + '\n' + certs.ca.toString('ascii')
|
|
||||||
, key: certs.key
|
|
||||||
});
|
|
||||||
|
|
||||||
cb(null, context);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Server
|
|
||||||
//
|
|
||||||
tlsOptions.SNICallback = certGetter;
|
|
||||||
https.createServer(tlsOptions, acmeResponder).listen(443, function () {
|
|
||||||
console.log('Listening https on', this.address());
|
|
||||||
});
|
|
||||||
https.createServer(tlsOptions, acmeResponder).listen(5001, function () {
|
|
||||||
console.log('Listening https on', this.address());
|
|
||||||
});
|
|
||||||
http.createServer(acmeResponder).listen(80, function () {
|
|
||||||
console.log('Listening http on', this.address());
|
|
||||||
});
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
// Note: we should just keep a handle on
|
|
||||||
// the servers and close them each with server.close()
|
|
||||||
process.exit(1);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,146 +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';
|
|
||||||
|
|
||||||
module.exports.create = function (deps) {
|
|
||||||
|
|
||||||
var NOOP = function () {
|
|
||||||
};
|
|
||||||
var log = NOOP;
|
|
||||||
var acmeRequest = deps.acmeRequest;
|
|
||||||
var RSA = deps.RSA;
|
|
||||||
var generateSignature = RSA.signJws;
|
|
||||||
|
|
||||||
function Acme(keypair) {
|
|
||||||
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=[];
|
|
||||||
}
|
|
||||||
|
|
||||||
Acme.prototype.getNonce=function(url, cb) {
|
|
||||||
var self=this;
|
|
||||||
|
|
||||||
acmeRequest.create().head({
|
|
||||||
url:url,
|
|
||||||
}, function(err, res/*, body*/) {
|
|
||||||
if (err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
if (res && 'replay-nonce' in res.headers) {
|
|
||||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
|
||||||
self.nonces.push(res.headers['replay-nonce']);
|
|
||||||
cb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(new Error('Failed to get nonce for request'));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Acme.prototype.post=function(url, body, cb) {
|
|
||||||
var self=this, payload, jws, signed;
|
|
||||||
|
|
||||||
if (this.nonces.length===0) {
|
|
||||||
this.getNonce(url, function(err) {
|
|
||||||
if (err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
self.post(url, body, cb);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Using nonce: '+this.nonces[0]);
|
|
||||||
payload=JSON.stringify(body, null, 2);
|
|
||||||
jws=generateSignature(
|
|
||||||
self.keypair, new Buffer(payload), this.nonces.shift()
|
|
||||||
);
|
|
||||||
signed=JSON.stringify(jws, null, 2);
|
|
||||||
|
|
||||||
log('Posting to '+url);
|
|
||||||
log(signed);
|
|
||||||
log('Payload:'+payload);
|
|
||||||
|
|
||||||
//process.exit(1);
|
|
||||||
//return;
|
|
||||||
return acmeRequest.create().post({
|
|
||||||
url: url
|
|
||||||
, body: signed
|
|
||||||
, encoding: null
|
|
||||||
}, function(err, res, body) {
|
|
||||||
var parsed;
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
console.error('[letiny-core/lib/acme-client.js] post');
|
|
||||||
console.error(err.stack);
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
if (res) {
|
|
||||||
log(('HTTP/1.1 '+res.statusCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(res.headers).forEach(function(key) {
|
|
||||||
var value, upcased;
|
|
||||||
value=res.headers[key];
|
|
||||||
upcased=key.charAt(0).toUpperCase()+key.slice(1);
|
|
||||||
log((upcased+': '+value));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
|
|
||||||
try {
|
|
||||||
parsed=JSON.parse(body);
|
|
||||||
log(JSON.stringify(parsed, null, 2));
|
|
||||||
} catch(err) {
|
|
||||||
log(body.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('replay-nonce' in res.headers) {
|
|
||||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
|
||||||
self.nonces.push(res.headers['replay-nonce']);
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(err, res, body);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Acme.parseLink = function parseLink(link) {
|
|
||||||
var links;
|
|
||||||
try {
|
|
||||||
links=link.split(',').map(function(link) {
|
|
||||||
var parts, url, info;
|
|
||||||
parts=link.trim().split(';');
|
|
||||||
url=parts.shift().replace(/[<>]/g, '');
|
|
||||||
info=parts.reduce(function(acc, p) {
|
|
||||||
var m=p.trim().match(/(.+) *= *"(.+)"/);
|
|
||||||
if (m) {
|
|
||||||
acc[m[1]]=m[2];
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
info.url=url;
|
|
||||||
return info;
|
|
||||||
}).reduce(function(acc, link) {
|
|
||||||
if ('rel' in link) {
|
|
||||||
acc[link.rel]=link.url;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
return links;
|
|
||||||
} catch(err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Acme;
|
|
||||||
};
|
|
||||||
73
lib/acme-util.js
Normal file
73
lib/acme-util.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// 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/.
|
||||||
|
|
||||||
|
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 (typeof(x) == "string") && !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 ((typeof(sig) == "object") &&
|
||||||
|
("alg" in sig) && (typeof(sig.alg) == "string") &&
|
||||||
|
("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 ((typeof(jwk) == "object") && ("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";
|
||||||
|
}
|
||||||
|
};
|
||||||
76
lib/cli.js
Normal file
76
lib/cli.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/local/bin/node
|
||||||
|
var app=require('commander'), letiny=require('./client'), examples=[
|
||||||
|
'letiny -e me@example.com -w /var/www/example.com -d example.com --agree',
|
||||||
|
'letiny -e me@example.com -m -d example.com -c cert.pem -k key.pem -i ca.pem --agree',
|
||||||
|
'letiny -e me@example.com -m -d example.com,www.example.com --agree',
|
||||||
|
'letiny --email me@example.com --webroot ./ --domains example.com --agree'
|
||||||
|
];
|
||||||
|
|
||||||
|
app
|
||||||
|
.option('-e, --email <email>', 'your email address')
|
||||||
|
.option('-w, --webroot <path>', 'path for webroot verification OR')
|
||||||
|
.option('-m, --manual', 'use manual verification')
|
||||||
|
.option('-d, --domains <domains>', 'domains (comma seperated)')
|
||||||
|
.option('-c, --cert <path>', 'path to save your certificate (cert.pem)')
|
||||||
|
.option('-k, --key <path>', 'path to save your private key (privkey.pem)')
|
||||||
|
.option('-i, --ca <path>', 'path to save issuer certificate (cacert.pem)')
|
||||||
|
.option('--pfx <path>', 'path to save PKCS#12 certificate (optional)')
|
||||||
|
.option('--password <password>', 'password for PKCS#12 certificate (optional)')
|
||||||
|
.option('--aes', 'use AES instead of 3DES for PKCS#12')
|
||||||
|
.option('--agree', 'agree terms of the ACME CA (required)')
|
||||||
|
.option('--newreg <URL>', 'optional AMCE server newReg URL')
|
||||||
|
.option('--debug', 'print debug information')
|
||||||
|
.on('--help', function() {
|
||||||
|
console.log(' Examples:\n\n '+examples.join('\n ')+'\n');
|
||||||
|
})
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
if (app.rawArgs.length<=2) {
|
||||||
|
return app.parse(['', '', '-h']);
|
||||||
|
} else if (!app.webroot && !app.manual) {
|
||||||
|
return console.log('Error: You need to use "--manual" or "--webroot <path>"');
|
||||||
|
} else if (!app.domains) {
|
||||||
|
return console.log('Error: You need to specify "--domains <domain>"');
|
||||||
|
} else if (!app.email) {
|
||||||
|
return console.log('Error: You need to specify your "--email <address>"');
|
||||||
|
} else if (!app.agree) {
|
||||||
|
return console.log('Error: You need to "--agree" the terms');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Generating keys and requesting certificate...');
|
||||||
|
|
||||||
|
letiny.getCert({
|
||||||
|
email:app.email,
|
||||||
|
domains:app.domains,
|
||||||
|
webroot:app.webroot,
|
||||||
|
challenge:manualVerification,
|
||||||
|
certFile:app.cert || (app.pfx ? false : 'cert.pem'),
|
||||||
|
keyFile:app.key || (app.pfx ? false : 'privkey.pem'),
|
||||||
|
caFile:app.ca || (app.pfx ? false : 'cacert.pem'),
|
||||||
|
pfxFile:app.pfx,
|
||||||
|
pfxPassword:app.password,
|
||||||
|
aes:app.aes,
|
||||||
|
newReg:app.newreg,
|
||||||
|
agreeTerms:app.agree,
|
||||||
|
debug:app.debug
|
||||||
|
}, function(err, cert, key, cacert) {
|
||||||
|
if (!err && cert && key && cacert) {
|
||||||
|
console.log('Files successfully saved.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
console.error('Error: ', err.stack || err || 'Something went wrong...');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
function manualVerification(domain, path, data, done) {
|
||||||
|
var rl=require('readline').createInterface({
|
||||||
|
input:process.stdin,
|
||||||
|
output:process.stdout
|
||||||
|
});
|
||||||
|
console.log('\nCreate this file: http://'+domain+path);
|
||||||
|
console.log(' containing this: '+data+'\n');
|
||||||
|
rl.question('Press ENTER when done or Ctrl+C to exit\n', function() {
|
||||||
|
rl.close();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
463
lib/client.js
Normal file
463
lib/client.js
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
/*!
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
var _DEBUG, NOOP=new Function(), log=NOOP,
|
||||||
|
mkdirp=require('mkdirp').sync, request=require('request'),
|
||||||
|
forge=require('node-forge'), pki=forge.pki,
|
||||||
|
cryptoUtil=require('./crypto-util'), util=require('./acme-util'),
|
||||||
|
fs=require('fs'), path=require('path');
|
||||||
|
|
||||||
|
function Acme(privateKey) {
|
||||||
|
this.privateKey=privateKey;
|
||||||
|
this.nonces=[];
|
||||||
|
}
|
||||||
|
|
||||||
|
Acme.prototype.getNonce=function(url, cb) {
|
||||||
|
var self=this;
|
||||||
|
|
||||||
|
request.head({
|
||||||
|
url:url,
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
if (res && 'replay-nonce' in res.headers) {
|
||||||
|
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||||
|
self.nonces.push(res.headers['replay-nonce']);
|
||||||
|
cb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(new Error('Failed to get nonce for request'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.prototype.post=function(url, body, cb) {
|
||||||
|
var self=this, payload, jws, signed;
|
||||||
|
|
||||||
|
if (this.nonces.length===0) {
|
||||||
|
this.getNonce(url, function(err) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
self.post(url, body, cb);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Using nonce: '+this.nonces[0]);
|
||||||
|
payload=JSON.stringify(body, null, 2);
|
||||||
|
jws=cryptoUtil.generateSignature(
|
||||||
|
this.privateKey, new Buffer(payload), this.nonces.shift()
|
||||||
|
);
|
||||||
|
signed=JSON.stringify(jws, null, 2);
|
||||||
|
|
||||||
|
log('Posting to '+url);
|
||||||
|
log(signed.green);
|
||||||
|
log('Payload:'+payload.blue);
|
||||||
|
|
||||||
|
return request.post({
|
||||||
|
url:url,
|
||||||
|
body:signed,
|
||||||
|
encoding:null
|
||||||
|
}, function(err, res, body) {
|
||||||
|
var parsed;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
if (res) {
|
||||||
|
log(('HTTP/1.1 '+res.statusCode).yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(res.headers).forEach(function(key) {
|
||||||
|
var value, upcased;
|
||||||
|
value=res.headers[key];
|
||||||
|
upcased=key.charAt(0).toUpperCase()+key.slice(1);
|
||||||
|
log((upcased+': '+value).yellow);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
|
||||||
|
try {
|
||||||
|
parsed=JSON.parse(body);
|
||||||
|
log(JSON.stringify(parsed, null, 2).cyan);
|
||||||
|
} catch(err) {
|
||||||
|
log(body.toString().cyan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('replay-nonce' in res.headers) {
|
||||||
|
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||||
|
self.nonces.push(res.headers['replay-nonce']);
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(err, res, body);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function getCert(options, cb) {
|
||||||
|
var state={
|
||||||
|
validatedDomains:[],
|
||||||
|
validAuthorizationURLs:[]
|
||||||
|
};
|
||||||
|
|
||||||
|
options.newReg=options.newReg || 'https://acme-v01.api.letsencrypt.org/acme/new-reg';
|
||||||
|
|
||||||
|
if (!options.email) {
|
||||||
|
return cb(new Error('No "email" option given!'));
|
||||||
|
}
|
||||||
|
if (typeof options.domains==='string') {
|
||||||
|
state.domains=options.domains.split(/[, ]+/);
|
||||||
|
} else if (options.domains && options.domains instanceof Array) {
|
||||||
|
state.domains=options.domains.slice();
|
||||||
|
} else {
|
||||||
|
return cb(new Error('No valid "domains" option given!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_DEBUG=options.debug)) {
|
||||||
|
if (!''.green) {
|
||||||
|
require('colors');
|
||||||
|
}
|
||||||
|
log=console.log.bind(console);
|
||||||
|
} else {
|
||||||
|
log=NOOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeAccountKeyPair();
|
||||||
|
|
||||||
|
function makeAccountKeyPair() {
|
||||||
|
var keypair;
|
||||||
|
log('Generating account keypair...');
|
||||||
|
keypair=pki.rsa.generateKeyPair(2048);
|
||||||
|
state.accountKeyPair=cryptoUtil.importPemPrivateKey(pki.privateKeyToPem(keypair.privateKey));
|
||||||
|
state.acme=new Acme(state.accountKeyPair);
|
||||||
|
makeKeyPair();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeKeyPair() {
|
||||||
|
var keypair;
|
||||||
|
log('Generating cert keypair...');
|
||||||
|
keypair=pki.rsa.generateKeyPair(2048);
|
||||||
|
state.certPrivateKeyPEM=pki.privateKeyToPem(keypair.privateKey);
|
||||||
|
state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPEM);
|
||||||
|
register();
|
||||||
|
}
|
||||||
|
|
||||||
|
function register() {
|
||||||
|
post(options.newReg, {
|
||||||
|
resource:'new-reg',
|
||||||
|
contact:['mailto:'+options.email]
|
||||||
|
}, getTerms);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTerms(err, res) {
|
||||||
|
var links;
|
||||||
|
|
||||||
|
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||||
|
return handleErr(err, 'Registration request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
links=parseLink(res.headers['link']);
|
||||||
|
if (!links || !('next' in links)) {
|
||||||
|
return handleErr(err, 'Server didn\'t provide information to proceed (1)');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.registrationURL=res.headers['location'];
|
||||||
|
state.newAuthorizationURL=links['next'];
|
||||||
|
state.termsRequired=('terms-of-service' in links);
|
||||||
|
|
||||||
|
if (state.termsRequired) {
|
||||||
|
state.termsURL=links['terms-of-service'];
|
||||||
|
log(state.termsURL);
|
||||||
|
request.get(state.termsURL, getAgreement);
|
||||||
|
} else {
|
||||||
|
getChallenges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgreement(err, res, body) {
|
||||||
|
if (err) {
|
||||||
|
return handleErr(err, 'Couldn\'t get agreement');
|
||||||
|
}
|
||||||
|
log('The CA requires your agreement to terms:\n'+state.termsURL);
|
||||||
|
sendAgreement();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAgreement() {
|
||||||
|
if (state.termsRequired && !options.agreeTerms) {
|
||||||
|
return handleErr(null, 'The CA requires your agreement to terms: '+state.termsURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Posting agreement to: '+state.registrationURL);
|
||||||
|
|
||||||
|
post(state.registrationURL, {
|
||||||
|
resource:'reg',
|
||||||
|
agreement:state.termsURL
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||||
|
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
||||||
|
} else {
|
||||||
|
nextDomain();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextDomain() {
|
||||||
|
if (state.domains.length > 0) {
|
||||||
|
getChallenges(state.domains.shift());
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
getCertificate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChallenges(domain) {
|
||||||
|
state.domain=domain;
|
||||||
|
|
||||||
|
post(state.newAuthorizationURL, {
|
||||||
|
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 || Math.floor(res.statusCode/100)!==2) {
|
||||||
|
return handleErr(err, 'Authorization request failed with code '+res.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
links=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.newCertificateURL=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=cryptoUtil.thumbprint(state.accountKeyPair.publicKey);
|
||||||
|
keyAuthorization=challenge.token+'.'+thumbprint;
|
||||||
|
challengePath='.well-known/acme-challenge/'+challenge.token;
|
||||||
|
state.responseURL=challenge['uri'];
|
||||||
|
state.path=challengePath;
|
||||||
|
|
||||||
|
if (options.webroot) {
|
||||||
|
try {
|
||||||
|
mkdirp(path.dirname(options.webroot+'/'+challengePath));
|
||||||
|
fs.writeFileSync(path.normalize(options.webroot+'/'+challengePath), keyAuthorization);
|
||||||
|
challengeDone();
|
||||||
|
} catch(err) {
|
||||||
|
handleErr(err, 'Could not write challange file to disk');
|
||||||
|
}
|
||||||
|
} else if (typeof options.challenge==='function') {
|
||||||
|
options.challenge(state.domain, '/'+challengePath, keyAuthorization, challengeDone);
|
||||||
|
} else {
|
||||||
|
return handleErr(null, 'No "challenge" function or "webroot" option given.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function challengeDone() {
|
||||||
|
post(state.responseURL, {
|
||||||
|
resource:'challenge',
|
||||||
|
keyAuthorization:keyAuthorization
|
||||||
|
}, function(err, res, body) {
|
||||||
|
ensureValidation(err, res, body, function unlink() {
|
||||||
|
if (options.webroot) {
|
||||||
|
fs.unlinkSync(path.normalize(options.webroot+'/'+challengePath));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains);
|
||||||
|
log('Requesting certificate...');
|
||||||
|
post(state.newCertificateURL, {
|
||||||
|
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=parseLink(res.headers['link']);
|
||||||
|
if (!links || !('up' in links)) {
|
||||||
|
return handleErr(err, 'Failed to fetch CA 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 CA certificate...');
|
||||||
|
request.get({
|
||||||
|
url:links['up'],
|
||||||
|
encoding:null
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (err || res.statusCode!==200) {
|
||||||
|
return handleErr(err, 'Failed to fetch CA certificate');
|
||||||
|
}
|
||||||
|
state.caCert=certBufferToPEM(body);
|
||||||
|
log('Requesting CA certificate: done');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
var cert, pfx;
|
||||||
|
try {
|
||||||
|
cert=certBufferToPEM(state.certificate);
|
||||||
|
if (options.certFile) {
|
||||||
|
fs.writeFileSync(options.certFile, cert);
|
||||||
|
}
|
||||||
|
if (options.keyFile) {
|
||||||
|
fs.writeFileSync(options.keyFile, state.certPrivateKeyPEM);
|
||||||
|
}
|
||||||
|
if (options.caFile) {
|
||||||
|
fs.writeFileSync(options.caFile, state.caCert);
|
||||||
|
}
|
||||||
|
if (options.pfxFile) {
|
||||||
|
try {
|
||||||
|
pfx=forge.pkcs12.toPkcs12Asn1(
|
||||||
|
pki.privateKeyFromPem(state.certPrivateKeyPEM),
|
||||||
|
[pki.certificateFromPem(cert), pki.certificateFromPem(state.caCert)],
|
||||||
|
options.pfxPassword || '',
|
||||||
|
options.aes ? {} : {algorithm:'3des'}
|
||||||
|
);
|
||||||
|
pfx=new Buffer(forge.asn1.toDer(pfx).toHex(), 'hex');
|
||||||
|
} catch(err) {
|
||||||
|
handleErr(err, 'Could not convert to PKCS#12');
|
||||||
|
}
|
||||||
|
fs.writeFileSync(options.pfxFile, pfx);
|
||||||
|
}
|
||||||
|
cb(null, cert, state.certPrivateKeyPEM, state.caCert);
|
||||||
|
} catch(err) {
|
||||||
|
handleErr(err, 'Could not write output files');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(url, body, cb) {
|
||||||
|
return state.acme.post(url, body, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleErr(err, text, info) {
|
||||||
|
log(text, err, info);
|
||||||
|
cb(err || new Error(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function certBufferToPEM(cert) {
|
||||||
|
cert=util.toStandardB64(cert.toString('base64'));
|
||||||
|
cert=cert.match(/.{1,64}/g).join('\n');
|
||||||
|
return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLink(link) {
|
||||||
|
var links;
|
||||||
|
try {
|
||||||
|
links=link.split(',').map(function(link) {
|
||||||
|
var parts, url, info;
|
||||||
|
parts=link.trim().split(';');
|
||||||
|
url=parts.shift().replace(/[<>]/g, '');
|
||||||
|
info=parts.reduce(function(acc, p) {
|
||||||
|
var m=p.trim().match(/(.+) *= *"(.+)"/);
|
||||||
|
if (m) {
|
||||||
|
acc[m[1]]=m[2];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
info['url']=url;
|
||||||
|
return info;
|
||||||
|
}).reduce(function(acc, link) {
|
||||||
|
if ('rel' in link) {
|
||||||
|
acc[link['rel']]=link['url'];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
return links;
|
||||||
|
} catch(err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getCert=getCert;
|
||||||
|
|
||||||
362
lib/crypto-util.js
Normal file
362
lib/crypto-util.js
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
// 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/.
|
||||||
|
|
||||||
|
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
|
||||||
|
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) {
|
||||||
|
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 = [];
|
||||||
|
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
|
||||||
|
var 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,60 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports.create = function (deps) {
|
|
||||||
var acmeRequest = deps.acmeRequest;
|
|
||||||
var knownUrls = deps.LeCore.knownEndpoints;
|
|
||||||
|
|
||||||
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
|
|
||||||
return acmeRequest.create()({
|
|
||||||
url: acmeDiscoveryUrl
|
|
||||||
, encoding: 'utf8'
|
|
||||||
}, function (err, resp) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = resp.body;
|
|
||||||
|
|
||||||
if ('string' === typeof data) {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
} catch(e) {
|
|
||||||
e.raw = data;
|
|
||||||
e.url = acmeDiscoveryUrl;
|
|
||||||
e.stack += '\n\nresponse data:\n'
|
|
||||||
+ data + '\n\nacmeDiscoveryUrl:' + acmeDiscoveryUrl;
|
|
||||||
cb(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!knownUrls.every(function (url) {
|
|
||||||
return data[url];
|
|
||||||
})) {
|
|
||||||
console.warn("This Let's Encrypt / ACME server is missing urls that this client may need.");
|
|
||||||
console.warn(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
newAuthz: data['new-authz']
|
|
||||||
, newCert: data['new-cert']
|
|
||||||
, newReg: data['new-reg']
|
|
||||||
, revokeCert: data['revoke-cert']
|
|
||||||
, keyChange: data['key-change']
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAcmeUrls;
|
|
||||||
};
|
|
||||||
@ -1,412 +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';
|
|
||||||
|
|
||||||
function _toStandardBase64(str) {
|
|
||||||
var b64 = str.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
|
|
||||||
|
|
||||||
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 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={
|
|
||||||
validatedDomains:[]
|
|
||||||
, validAuthorizationUrls:[]
|
|
||||||
, newAuthzUrl: options.newAuthzUrl
|
|
||||||
, 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) {
|
|
||||||
return handleErr(new Error("options.newAuthzUrl must be the authorization url"));
|
|
||||||
}
|
|
||||||
if (!options.newCertUrl) {
|
|
||||||
return handleErr(new Error("options.newCertUrl must be the new certificate url"));
|
|
||||||
}
|
|
||||||
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.domainKeypair) {
|
|
||||||
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) {
|
|
||||||
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
|
|
||||||
}
|
|
||||||
if (!options.removeChallenge) {
|
|
||||||
return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}"));
|
|
||||||
}
|
|
||||||
if (!(options.domains && options.domains.length)) {
|
|
||||||
return handleErr(new Error("options.domains must be an array of domains such as ['example.com', 'www.example.com']"));
|
|
||||||
}
|
|
||||||
|
|
||||||
state.domains = options.domains.slice(0); // copy array
|
|
||||||
try {
|
|
||||||
state.accountKeypair = options.accountKeypair;
|
|
||||||
state.certKeypair = options.domainKeypair;
|
|
||||||
state.acme = new Acme(state.accountKeypair);
|
|
||||||
} catch(err) {
|
|
||||||
return handleErr(err, 'Failed to parse privateKey');
|
|
||||||
}
|
|
||||||
|
|
||||||
nextDomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCert;
|
|
||||||
};
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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,137 +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';
|
|
||||||
module.exports.create = function (deps) {
|
|
||||||
var NOOP=function () {}, log=NOOP;
|
|
||||||
var acmeRequest = deps.acmeRequest;
|
|
||||||
var RSA = deps.RSA;
|
|
||||||
var Acme = deps.Acme;
|
|
||||||
|
|
||||||
function registerNewAccount(options, cb) {
|
|
||||||
|
|
||||||
function register() {
|
|
||||||
state.acme.post(options.newRegUrl, {
|
|
||||||
resource:'new-reg',
|
|
||||||
contact:['mailto:'+options.email]
|
|
||||||
}, getTerms);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTerms(err, res) {
|
|
||||||
var links;
|
|
||||||
|
|
||||||
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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
links=Acme.parseLink(res.headers.link);
|
|
||||||
if (!links || !('next' in links)) {
|
|
||||||
return handleErr(err, 'Server didn\'t provide information to proceed (1)');
|
|
||||||
}
|
|
||||||
|
|
||||||
state.registrationUrl=res.headers.location;
|
|
||||||
// TODO should we pass this along?
|
|
||||||
//state.newAuthorizationUrl=links.next;
|
|
||||||
state.termsRequired=('terms-of-service' in links);
|
|
||||||
|
|
||||||
if (state.termsRequired) {
|
|
||||||
state.termsUrl=links['terms-of-service'];
|
|
||||||
options.agreeToTerms(state.termsUrl, function (err, agree) {
|
|
||||||
if (err) {
|
|
||||||
return handleErr(err);
|
|
||||||
}
|
|
||||||
if (!agree) {
|
|
||||||
return handleErr(new Error("You must agree to the terms of use at '" + state.termsUrl + "'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
state.agreeTerms = agree;
|
|
||||||
state.termsUrl=links['terms-of-service'];
|
|
||||||
log(state.termsUrl);
|
|
||||||
acmeRequest.create().get(state.termsUrl, getAgreement);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cb(null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAgreement(err/*, res, body*/) {
|
|
||||||
if (err) {
|
|
||||||
return handleErr(err, 'Couldn\'t get agreement');
|
|
||||||
}
|
|
||||||
log('The CA requires your agreement to terms:\n'+state.termsUrl);
|
|
||||||
sendAgreement();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendAgreement() {
|
|
||||||
if (state.termsRequired && !state.agreeTerms) {
|
|
||||||
return handleErr(null, 'The CA requires your agreement to terms: '+state.termsUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Posting agreement to: '+state.registrationUrl);
|
|
||||||
|
|
||||||
state.acme.post(state.registrationUrl, {
|
|
||||||
resource:'reg',
|
|
||||||
agreement:state.termsUrl
|
|
||||||
}, function(err, res, body) {
|
|
||||||
var data;
|
|
||||||
|
|
||||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
|
||||||
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('string' === typeof body || '{' === body[0] || '{' === String.fromCharCode(body[0])) {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(body.toString('utf8'));
|
|
||||||
} catch(e) {
|
|
||||||
cb(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleErr(err, text, info) {
|
|
||||||
log(text, err, info);
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
53
node.js
53
node.js
@ -1,53 +0,0 @@
|
|||||||
/*!
|
|
||||||
* letiny-core
|
|
||||||
* Copyright(c) 2015 AJ ONeal <coolaj86@gmail.com> https://coolaj86.com
|
|
||||||
* Apache-2.0 OR MIT (and hence also MPL 2.0)
|
|
||||||
*/
|
|
||||||
'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) {
|
|
||||||
deps = deps || {};
|
|
||||||
deps.LeCore = {};
|
|
||||||
|
|
||||||
Object.keys(defaults).forEach(function (key) {
|
|
||||||
deps[key] = defaults[key];
|
|
||||||
deps.LeCore[key] = defaults[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
deps.RSA = deps.RSA || require('rsa-compat').RSA;
|
|
||||||
deps.acmeRequest = require('./lib/le-acme-request');
|
|
||||||
deps.Acme = require('./lib/acme-client').create(deps);
|
|
||||||
|
|
||||||
deps.LeCore.Acme = deps.Acme;
|
|
||||||
deps.LeCore.acmeRequest = deps.acmeRequest;
|
|
||||||
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 = {};
|
|
||||||
|
|
||||||
Object.keys(defaults).forEach(function (key) {
|
|
||||||
defs[key] = defs[deps] || defaults[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
return defs;
|
|
||||||
};
|
|
||||||
|
|
||||||
return deps.LeCore;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO make this the official usage
|
|
||||||
module.exports.ACME = { create: create };
|
|
||||||
|
|
||||||
Object.keys(defaults).forEach(function (key) {
|
|
||||||
module.exports.ACME[key] = defaults[key];
|
|
||||||
});
|
|
||||||
44
package.json
44
package.json
@ -1,41 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "le-acme-core",
|
"name": "letiny",
|
||||||
"version": "2.1.4",
|
"version": "0.0.3-beta",
|
||||||
"description": "A framework for building letsencrypt clients, forked from letiny",
|
"description": "Tiny ACME client library and CLI",
|
||||||
"main": "node.js",
|
"author": "Anatol Sommer <anatol@anatol.at>",
|
||||||
"browser": "browser.js",
|
"license": "MPL",
|
||||||
"directories": {
|
|
||||||
"example": "example",
|
|
||||||
"test": "test"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://git.coolaj86.com/coolaj86/le-acme-core.js.git"
|
|
||||||
},
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://git.coolaj86.com/coolaj86/le-acme-core.js/issues"
|
|
||||||
},
|
|
||||||
"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",
|
"cli",
|
||||||
"jwk",
|
|
||||||
"pfx"
|
"pfx"
|
||||||
],
|
],
|
||||||
|
"bin": {
|
||||||
|
"letiny": "./lib/cli.js"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"request": "^2.74.0",
|
"colors": "^1.1.0",
|
||||||
"rsa-compat": "^1.3.2"
|
"mkdirp": "^0.5.1",
|
||||||
|
"node-forge": "^0.6.21",
|
||||||
|
"request": "^2.55.0",
|
||||||
|
"commander": "^2.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"better-assert": "^1.0.2",
|
"mocha": "^2.3.3",
|
||||||
"chai": "^3.5.0",
|
"better-assert": "^1.0.2"
|
||||||
"chai-string": "^1.3.0",
|
|
||||||
"request-debug": "^0.2.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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('../'), config=require('./config.json'),
|
letiny=require('../lib/client'), 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