refactor and docs

This commit is contained in:
AJ ONeal 2015-12-15 22:07:02 +00:00
parent b6dc41c704
commit e651755417
8 changed files with 321 additions and 204 deletions

129
README.md
View File

@ -1,94 +1,57 @@
# letiny # letiny-core
Tiny acme client library and CLI to obtain ssl certificates (without using external commands like openssl).
A framework for building letsencrypt clients, forked from `letiny`.
* browser
* node with `forge` (works on windows)
* node with `ursa` (works fast)
* any javascript implementation
## Usage: ## Usage:
`npm install letiny`
```bash
npm install --save letiny-core
```
### Using the "webroot" option ```javascript
This will create a file in `/var/www/example.com/.well-known/acme-challenge/` to verify the domain. 'use strict';
```js
require('letiny').getCert({ var leCore = require('leCore');
email:'me@example.com',
domains:['example.com', 'www.example.com'], leCore.
webroot:'/var/www/example.com', ```
certFile:'./cert.pem',
keyFile:'./key.pem', ## API
caFile:'./ca.pem',
agreeTerms:true ```
}, function(err, cert, key, cacert) { LeCore.registerNewAccount();
console.log(err || cert+'\n'+key+'\n'+cacert);
LeCore.getCertificate();
LeCore.Acme // Signs requests with JWK
acme = new Acme(lePrivateKey) // privateKey format is abstract
acme.post(url, body, cb) // POST with signature
acme.parseLinks(link) // (internal) parses 'link' header
acme.getNonce(url, cb) // (internal) HEAD request to get 'replay-nonce' strings
LeCore.leCrypto
generateSignature(lePrivateKey, nodeBufferBody, nonceString)
```
For testing and development, you can also inject the dependencies you want to use:
```javascript
leCore.create({
request: require('request')
, leCrypto: rquire('./lib/letsencrypt-forge')
}); });
``` ```
### Using the "challenge" option
This allows you to provide the challenge data on your own, so you can obtain certificates on-the-fly within your software.
```js
require('letiny').getCert({
email:'me@example.com',
domains:'example.com',
challenge:function(domain, path, data, done) {
// make http://+domain+path serving "data"
done();
},
certFile:'./cert.pem',
keyFile:'./key.pem',
caFile:'./ca.pem',
agreeTerms:true
}, function(err, cert, key, cacert) {
console.log(err || cert+'\n'+key+'\n'+cacert);
});
```
### Options
#### Required:
* `email`: Your email adress
* `domains`: Comma seperated string or array
* `agreeTerms`: You need to agree the terms
* `webroot` (string) or `challenge` (function)
#### Optional:
* `certFile`: Path to save certificate
* `keyFile`: Path to save private key
* `caFile`: Path to save issuer certificate
* `pfxFile`: Path to save PKCS#12 certificate
* `pfxPassword`: Password for PKCS#12 certificate
* `aes`: (boolean), use AES instead of 3DES for PKCS#12 certificate
* `newReg`: URL, use *https://acme-staging.api.letsencrypt.org/acme/new-reg* for testing
## Command line interface
```sudo npm install letiny -g```
#### Options:
```
-h, --help output usage information
-e, --email <email> your email address
-w, --webroot <path> path for webroot verification
-m, --manual use manual verification
-d, --domains <domains> domains (comma seperated)
-c, --cert <path> path to save your certificate (cert.pem)
-k, --key <path> path to save your private key (privkey.pem)
-i, --ca <path> path to save issuer certificate (cacert.pem)
--pfx <path> path to save PKCS#12 certificate (optional)
--password <password> password for PKCS#12 certificate (optional)
--aes use AES instead of 3DES for PKCS#12
--agree agree terms of the ACME CA (required)
--newreg <URL> optional AMCE server newReg URL
--debug print debug information
```
When --pfx is used without --cert, --key and --ca no .pem files will be created.
#### Examples:
```
letiny -e me@example.com -w /var/www/example.com -d example.com --agree
letiny -e me@example.com -m -d example.com -c cert.pem -k key.pem -i ca.pem --agree
letiny -e me@example.com -m -d example.com,www.example.com --agree
letiny -e me@example.com -m -d example.com --pfx cert.pfx --password secret --agree
letiny --email me@example.com --webroot ./ --domains example.com --agree
```
## Licence ## Licence
MPL 2.0 MPL 2.0
All of the code is available under the MPL-2.0.
Some of the files are original work not modified from `letiny`
and are made available under MIT as well (check file headers).

View File

@ -4,20 +4,21 @@
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js * Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
* MPL 2.0 * MPL 2.0
*/ */
'use strict'; 'use strict';
var NOOP=function () {}; module.exports.create = function (deps) {
var log=NOOP;
var request=require('request');
var cryptoUtil=require('./crypto-util');
function Acme(privateKey) { var NOOP=function () {};
var log=NOOP;
var request=require('request');
var generateSignature=deps.leCrypto.generateSignature;
function Acme(privateKey) {
this.privateKey=privateKey; this.privateKey=privateKey;
this.nonces=[]; this.nonces=[];
} }
Acme.prototype.getNonce=function(url, cb) { Acme.prototype.getNonce=function(url, cb) {
var self=this; var self=this;
request.head({ request.head({
@ -35,9 +36,9 @@ Acme.prototype.getNonce=function(url, cb) {
cb(new Error('Failed to get nonce for request')); cb(new Error('Failed to get nonce for request'));
}); });
}; };
Acme.prototype.post=function(url, body, cb) { Acme.prototype.post=function(url, body, cb) {
var self=this, payload, jws, signed; var self=this, payload, jws, signed;
if (this.nonces.length===0) { if (this.nonces.length===0) {
@ -52,7 +53,7 @@ Acme.prototype.post=function(url, body, cb) {
log('Using nonce: '+this.nonces[0]); log('Using nonce: '+this.nonces[0]);
payload=JSON.stringify(body, null, 2); payload=JSON.stringify(body, null, 2);
jws=cryptoUtil.generateSignature( jws=generateSignature(
this.privateKey, new Buffer(payload), this.nonces.shift() this.privateKey, new Buffer(payload), this.nonces.shift()
); );
signed=JSON.stringify(jws, null, 2); signed=JSON.stringify(jws, null, 2);
@ -99,9 +100,9 @@ Acme.prototype.post=function(url, body, cb) {
cb(err, res, body); cb(err, res, body);
}); });
}; };
Acme.parseLink = function parseLink(link) { Acme.parseLink = function parseLink(link) {
var links; var links;
try { try {
links=link.split(',').map(function(link) { links=link.split(',').map(function(link) {
@ -127,6 +128,7 @@ Acme.parseLink = function parseLink(link) {
} catch(err) { } catch(err) {
return null; return null;
} }
}; };
module.exports = Acme; return Acme;
};

View File

@ -2,6 +2,7 @@
// This Source Code Form is subject to the terms of the Mozilla Public // 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 // 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/. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
'use strict';
var crypto = require("crypto"); var crypto = require("crypto");
var forge = require("node-forge"); var forge = require("node-forge");

View File

@ -0,0 +1,8 @@
/*!
* letsencrypt-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 = {};

85
lib/letsencrypt-ursa.js Normal file
View File

@ -0,0 +1,85 @@
/*!
* letsencrypt-core
* Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
* Apache-2.0 OR MIT (and hence also MPL 2.0)
*/
'use strict';
var crypto = require('crypto');
var ursa = require('ursa');
var forge = require('node-forge');
function binstr2b64(binstr) {
return new Buffer(binstr, 'binary').toString('base64');
}
function toAcmePrivateKey(privkeyPem) {
var forgePrivkey = forge.pki.privateKeyFromPem(privkeyPem);
return {
kty: "RSA"
, n: binstr2b64(forgePrivkey.n)
, e: binstr2b64(forgePrivkey.e)
, d: binstr2b64(forgePrivkey.d)
, p: binstr2b64(forgePrivkey.p)
, q: binstr2b64(forgePrivkey.q)
, dp: binstr2b64(forgePrivkey.dP)
, dq: binstr2b64(forgePrivkey.dQ)
, qi: binstr2b64(forgePrivkey.qInv)
};
}
function generateRsaKeypair(bitlen, exp, cb) {
var keypair = ursa.generatePrivateKey(bitlen /*|| 2048*/, exp /*65537*/);
var pems = {
publicKeyPem: keypair.toPublicPem().toString('ascii') // ascii PEM: ----BEGIN...
, privateKeyPem: keypair.toPrivatePem().toString('ascii') // ascii PEM: ----BEGIN...
};
// I would have chosen sha1 or sha2... but whatever
pems.publicKeyMd5 = crypto.createHash('md5').update(pems.publicKeyPem).digest('hex');
// json { n: ..., e: ..., iq: ..., etc }
pems.privateKeyJwk = toAcmePrivateKey(pems.privateKeyPem);
pems.privateKeyJson = pems.privateKeyJwk;
// TODO thumbprint
cb(null, pems);
}
function parseAccountPrivateKey(pkj, cb) {
Object.keys(pkj).forEach(function (key) {
pkj[key] = new Buffer(pkj[key], 'base64');
});
var priv;
try {
priv = ursa.createPrivateKeyFromComponents(
pkj.n // modulus
, pkj.e // exponent
, pkj.p
, pkj.q
, pkj.dp
, pkj.dq
, pkj.qi
, pkj.d
);
} catch(e) {
cb(e);
return;
}
cb(null, {
privateKeyPem: priv.toPrivatePem.toString('ascii')
, publicKeyPem: priv.toPublicPem.toString('ascii')
});
}
module.exports.generateRsaKeypair = generateRsaKeypair;
module.exports.privateJwkToPems = parseAccountPrivateKey;
module.exports.privatePemToJwk = toAcmePrivateKey;
// TODO deprecate
module.exports.toAcmePrivateKey = toAcmePrivateKey;
module.exports.parseAccountPrivateKey = parseAccountPrivateKey;

38
lib/node.js Normal file
View File

@ -0,0 +1,38 @@
/*!
* letsencrypt-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 leCrypto = require('./letsencrypt-node-crypto');
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 (fallback)
Object.keys(leUrsa).forEach(function (key) {
if (!leCrypto[key]) {
leCrypto[key] = leUrsa[key];
}
});
Object.keys(leForge).forEach(function (key) {
if (!leCrypto[key]) {
leCrypto[key] = leForge[key];
}
});
module.exports.request = request;
module.exports.leCrypto = leCrypto;

20
node.js Normal file
View File

@ -0,0 +1,20 @@
/*!
* letsencrypt-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';
function create(deps) {
var LeCore = {};
LeCore.leCrypto = deps.leCrypto;
LeCore.Acme = require('./lib/acme-client').create(deps);
LeCore.registerNewAccount = require('./lib/register-new-account').create(deps);
LeCore.getCertificate = require('./lib/get-certificate').create(deps);
return LeCore;
}
module.exports = create(require('./lib/node'));
module.exports.create = create;

View File

@ -1,30 +1,30 @@
{ {
"name": "letiny", "name": "letiny-core",
"version": "0.0.5-beta", "version": "1.0.0",
"description": "Tiny ACME client library and CLI", "description": "Tiny ACME client library and CLI",
"author": "Anatol Sommer <anatol@anatol.at>", "authors": [
"Anatol Sommer <anatol@anatol.at>",
"AJ ONeal <aj@daplie.com>"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/anatolsommer/letiny.git" "url": "https://github.com/Daplie/letiny-core.git"
}, },
"keywords": [ "keywords": [
"tiny", "tiny",
"acme", "acme",
"letsencrypt", "letsencrypt",
"client", "client",
"cli", "pem",
"pfx" "pfx"
], ],
"bin": {
"letiny": "./lib/cli.js"
},
"dependencies": { "dependencies": {
"colors": "^1.1.0", "node-forge": "^0.6.38",
"mkdirp": "^0.5.1", "request": "^2.55.0"
"node-forge": "^0.6.21", },
"request": "^2.55.0", "optionalDependencies": {
"commander": "^2.9.0" "ursa": "^0.9.1"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^2.3.3", "mocha": "^2.3.3",