Compare commits

...

72 Commits

Author SHA1 Message Date
2fd9678ab5 v2.1.4: update links and urls 2018-11-20 14:39:54 -07:00
AJ ONeal
7a6c2ae573 v2.1.3 2018-05-12 16:54:19 -06:00
AJ ONeal
4758dc2bd2 remove unused package 2018-05-12 16:53:44 -06:00
d28d82130c Update 'README.md' 2018-04-18 16:16:50 +00:00
AJ ONeal
3a41c3006c update rsa-compat 2018-03-21 15:21:53 -06:00
bfe1737b9b Update 'README.md' 2018-01-12 02:43:04 +00:00
Tim Caswell
9172d4c98e Bump version to 2.1.1 2017-06-28 12:52:22 -05:00
Tim Caswell
530b25f691 Merge branch 'remove-url-warning' into 'master'
Remove warning about unknown keys in le urls

See merge request !6
2017-06-28 09:59:08 -06:00
Tim Caswell
d85f4070f3 Remove warning about unknown keys in le urls 2017-06-27 12:34:52 -05:00
tigerbot
51bcc1f20a v2.1.0 2017-04-26 18:50:21 -06:00
tigerbot
f79c62032c Merge remote-tracking branch 'MaitreyaBuddha/master' into master 2017-04-26 16:55:55 -06:00
tigerbot
3ed2d45d3d Merge remote-tracking branch 'MaitreyaBuddha/userAgent' into master 2017-04-14 13:16:30 -06:00
tigerbot
d0e20a44cd renamed httpsOptions to tlsOptions 2017-04-10 14:21:09 -06:00
Kelly Johnson
10978ab99a https://git.daplie.com/Daplie/le-acme-core/issues/17
Add User-Agent header to request object used by le-acme-core.
Expose options to add/remove information from header
Add mocha tests
2017-03-11 13:49:12 -08:00
Kelly Johnson
72fb7b7c07 https://git.daplie.com/Daplie/le-acme-core/issues/20
example/letsencrypt.js now “works” up until the point of not having a server to give proper challenge response
2017-03-11 11:36:07 -08:00
Drew Warren
72646ced80 v2.0.9 2017-01-17 13:50:35 -07:00
Drew Warren
9f01021948 Change GitHub to GitLab 2017-01-17 13:49:59 -07:00
Drew Warren
f63070ce54 v2.0.8 2017-01-17 12:49:45 -07:00
Drew Warren
6126222e8f Merge branch 'dev' into 'master'
Separate handling errors

Closes #16

See merge request !2
2017-01-17 19:09:40 +00:00
Drew Warren
7288d14fac Separate handling errors
Prevent res.body from being called if res is not defined.
Close #16
2017-01-17 12:01:02 -07:00
AJ ONeal
681c0edc71 Merge branch 'master' into 'master'
Add key-change URL support

See merge request !1
2017-01-16 23:02:15 +00:00
Rodrigo López Dato
f350ae44c1
Add key-change URL support
Fixes warning when retrieving LE URLs on staging or prod
2017-01-12 19:37:34 -03:00
AJ ONeal
6b1b168e5a auto-update banner 2016-12-30 02:40:45 -07:00
AJ ONeal
a97c5933d6 auto-update ad 2016-12-30 00:52:44 -07:00
AJ ONeal
a8b9817415 Update README.md 2016-11-25 10:38:39 -07:00
AJ ONeal
fe635a965c whitespace 2016-09-02 09:27:31 -06:00
AJ ONeal
8436b615cb v2.0.7 2016-09-02 09:22:25 -06:00
AJ ONeal
fbaa77cb4c update deps for #12 2016-09-02 09:22:18 -06:00
AJ ONeal
528cec03a8 v2.0.6 2016-08-11 08:41:33 -06:00
AJ ONeal
e3d4add0b9 fix https://github.com/Daplie/letsencrypt-cli/issues/20, use corret error object e instead of err 2016-08-11 08:41:30 -06:00
AJ ONeal
218497ab0e fix link 2016-08-09 16:02:47 -04:00
AJ ONeal
764c614940 v2.0.5 2016-08-09 16:02:01 -04:00
AJ ONeal
80613b98e2 update deps 2016-08-09 16:01:56 -04:00
AJ ONeal
4050bd2a82 v2.0.4 2016-08-09 15:58:36 -04:00
AJ ONeal
17df564f69 minor cleanup 2016-08-09 15:58:11 -04:00
AJ ONeal
60e4ed8f7b fix deps scope 2016-08-08 19:04:31 -04:00
AJ ONeal
420351da62 export defaults and change quotes 2016-08-08 16:55:06 -04:00
AJ ONeal
903ebf0491 remove deprecated uses 2016-08-08 14:12:23 -04:00
AJ ONeal
4c7c21a751 link to letiny-core 2016-08-08 12:04:44 -06:00
AJ ONeal
01f283b7fd add getOptions 2016-08-08 12:02:53 -06:00
AJ ONeal
c1513fe120 letiny-core -> le-acme-core 2016-08-08 11:58:08 -06:00
AJ ONeal
4e5d373055 bump 2016-08-06 18:56:09 -06:00
AJ ONeal
572621086e fix #11 output pems with CRLF, end in CRLF 2016-08-06 18:55:19 -06:00
AJ ONeal
b980fde859 bump 2016-08-04 16:36:50 -06:00
AJ ONeal
4ded1088c8 remove cruft 2016-08-04 16:35:57 -06:00
AJ ONeal
5d970344db forgot to bump rsa-compat 2016-08-03 23:07:54 -04:00
AJ ONeal
82a456f7c8 breaking changes, bump major 2016-08-03 22:11:01 -04:00
AJ ONeal
e7a36123ca minor bugfixes and lots of whitespace adjustment 2016-08-03 22:10:26 -04:00
AJ ONeal
b5b516a131 update comments 2016-08-01 21:47:48 -04:00
AJ ONeal
761202ae8e disambiguate 2016-08-01 21:19:17 -04:00
AJ ONeal
2f607314b9 disambiguate 2016-08-01 21:16:25 -04:00
AJ ONeal
e8acedd6d7 backwards compat 2016-08-01 20:31:43 -04:00
AJ ONeal
36acd623ca update docs (privateKey -> keypair) 2016-08-01 20:31:34 -04:00
AJ ONeal
8e6d31436c rename privateKey -> keypair 2016-08-01 20:26:19 -04:00
AJ ONeal
e52f043cfe rename privateKey -> keypair 2016-08-01 20:25:46 -04:00
AJ ONeal
3bab5857dd backwards compat 2016-08-01 20:07:54 -04:00
AJ ONeal
ee48f4a477 normalize all *Pem *Key* etc to keypair and *Keypair 2016-08-01 20:00:04 -04:00
AJ ONeal
2c7b7e1bcf slimming down 2016-08-01 14:10:37 -04:00
AJ ONeal
7504268047 moving to rsa-compat 2016-08-01 12:48:24 -04:00
AJ ONeal
55707a417d moving to rsa-compat 2016-08-01 05:53:50 -04:00
AJ ONeal
e0909ad0ca crypto.createCredentials is deprecated. Use tls.createSecureContext instead 2016-06-02 10:35:30 -06:00
AJ ONeal
526e19b23e concatenate non-parseable response body to e.stack 2016-04-04 11:45:25 -06:00
AJ ONeal
764096a0b2 always show not ok error 2016-02-16 10:08:12 -07:00
AJ ONeal
44fb789543 v1.0.5 2016-02-15 16:18:34 -07:00
AJ ONeal
4c2e638aed Merge pull request #4 from martinheidegger/detailed-error-message
Fixed the error message
2016-02-15 16:14:31 -07:00
Martin Heidegger
ad71d8bb97 Added array null check 2016-02-16 01:41:02 +09:00
Martin Heidegger
acb400ef7f Fixed the error message 2016-02-16 01:30:05 +09:00
AJ ONeal
3babc27847 Merge pull request #3 from martinheidegger/detailed-error-message
Added error handler details.
2016-02-15 08:46:08 -07:00
Martin Heidegger
8ec66c2a1f Added error handler details. 2016-02-15 23:06:41 +09:00
AJ ONeal
6a8592994e v1.0.4 2016-02-11 15:22:59 -05:00
AJ ONeal
e0202a559e handle setChallenge failure 2016-02-11 15:22:41 -05:00
AJ ONeal
7eace7e37e better debugging 2016-02-10 15:41:48 -05:00
23 changed files with 560 additions and 1062 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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
View File

@ -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).

View File

@ -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';

View File

@ -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';

View File

@ -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
});

View File

@ -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);
};
};

View File

@ -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,42 +63,45 @@ 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;
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).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());
}
}

View File

@ -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";
}
};

View File

@ -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);
});
};

View File

@ -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']
});
});
}

View File

@ -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,49 +102,33 @@ 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;
var links;
var authz;
var httpChallenges;
var challenge;
var thumbprint;
var keyAuthorization;
if (err) {
return handleErr(err);
}
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;
}
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);
function challengeDone() {
state.acme.post(state.responseUrl, {
resource:'challenge',
keyAuthorization:keyAuthorization
resource: 'challenge',
keyAuthorization: keyAuthorization
}, function(err, res, body) {
if (!err && res.body) {
try {
@ -186,21 +145,55 @@ 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();
return handleErr(err, 'Authorization status request failed ('+res.statusCode+')');
return handleErr(err, 'Authorization status request failed ('
+ (res && res.statusCode || err.code || err.message || err) + ')');
}
authz=body;
if (authz.status==='pending') {
setTimeout(function() {
request({
acmeRequest.create()({
method: 'GET'
, url: state.authorizationUrl
}, function(err, res, body) {
@ -223,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);
@ -231,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',
@ -275,7 +278,7 @@ module.exports.create = function (deps) {
state.certificate=body;
certUrl=res.headers.location;
request({
acmeRequest.create()({
method: 'GET'
, url: certUrl
, encoding: null
@ -301,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
});
}
@ -349,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
View 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
};

View File

@ -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;

View File

@ -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;
}
};

View File

@ -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 = {};

View File

@ -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;

View File

@ -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;

View File

@ -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
View File

@ -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];
});

View File

@ -1,6 +1,6 @@
{
"name": "letiny-core",
"version": "1.0.3",
"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
View 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);
});
});
});

View File

@ -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;