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