refactor and docs
This commit is contained in:
parent
b6dc41c704
commit
e651755417
129
README.md
129
README.md
|
@ -1,94 +1,57 @@
|
|||
# letiny
|
||||
Tiny acme client library and CLI to obtain ssl certificates (without using external commands like openssl).
|
||||
# letiny-core
|
||||
|
||||
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:
|
||||
`npm install letiny`
|
||||
|
||||
```bash
|
||||
npm install --save letiny-core
|
||||
```
|
||||
|
||||
### Using the "webroot" option
|
||||
This will create a file in `/var/www/example.com/.well-known/acme-challenge/` to verify the domain.
|
||||
```js
|
||||
require('letiny').getCert({
|
||||
email:'me@example.com',
|
||||
domains:['example.com', 'www.example.com'],
|
||||
webroot:'/var/www/example.com',
|
||||
certFile:'./cert.pem',
|
||||
keyFile:'./key.pem',
|
||||
caFile:'./ca.pem',
|
||||
agreeTerms:true
|
||||
}, function(err, cert, key, cacert) {
|
||||
console.log(err || cert+'\n'+key+'\n'+cacert);
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
var leCore = require('leCore');
|
||||
|
||||
leCore.
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
LeCore.registerNewAccount();
|
||||
|
||||
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
|
||||
|
||||
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).
|
||||
|
|
|
@ -4,129 +4,131 @@
|
|||
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
|
||||
* MPL 2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var NOOP=function () {};
|
||||
var log=NOOP;
|
||||
var request=require('request');
|
||||
var cryptoUtil=require('./crypto-util');
|
||||
module.exports.create = function (deps) {
|
||||
|
||||
function Acme(privateKey) {
|
||||
this.privateKey=privateKey;
|
||||
this.nonces=[];
|
||||
}
|
||||
var NOOP=function () {};
|
||||
var log=NOOP;
|
||||
var request=require('request');
|
||||
var generateSignature=deps.leCrypto.generateSignature;
|
||||
|
||||
Acme.prototype.getNonce=function(url, cb) {
|
||||
var self=this;
|
||||
function Acme(privateKey) {
|
||||
this.privateKey=privateKey;
|
||||
this.nonces=[];
|
||||
}
|
||||
|
||||
request.head({
|
||||
url:url,
|
||||
}, function(err, res/*, body*/) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (res && 'replay-nonce' in res.headers) {
|
||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||
self.nonces.push(res.headers['replay-nonce']);
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
Acme.prototype.getNonce=function(url, cb) {
|
||||
var self=this;
|
||||
|
||||
cb(new Error('Failed to get nonce for request'));
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.post=function(url, body, cb) {
|
||||
var self=this, payload, jws, signed;
|
||||
|
||||
if (this.nonces.length===0) {
|
||||
this.getNonce(url, function(err) {
|
||||
request.head({
|
||||
url:url,
|
||||
}, function(err, res/*, body*/) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
self.post(url, body, cb);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log('Using nonce: '+this.nonces[0]);
|
||||
payload=JSON.stringify(body, null, 2);
|
||||
jws=cryptoUtil.generateSignature(
|
||||
this.privateKey, new Buffer(payload), this.nonces.shift()
|
||||
);
|
||||
signed=JSON.stringify(jws, null, 2);
|
||||
|
||||
log('Posting to '+url);
|
||||
log(signed.green);
|
||||
log('Payload:'+payload.blue);
|
||||
|
||||
return request.post({
|
||||
url:url,
|
||||
body:signed,
|
||||
encoding:null
|
||||
}, function(err, res, body) {
|
||||
var parsed;
|
||||
|
||||
if (err) {
|
||||
console.error(err.stack);
|
||||
return cb(err);
|
||||
}
|
||||
if (res) {
|
||||
log(('HTTP/1.1 '+res.statusCode).yellow);
|
||||
}
|
||||
|
||||
Object.keys(res.headers).forEach(function(key) {
|
||||
var value, upcased;
|
||||
value=res.headers[key];
|
||||
upcased=key.charAt(0).toUpperCase()+key.slice(1);
|
||||
log((upcased+': '+value).yellow);
|
||||
});
|
||||
|
||||
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
|
||||
try {
|
||||
parsed=JSON.parse(body);
|
||||
log(JSON.stringify(parsed, null, 2).cyan);
|
||||
} catch(err) {
|
||||
log(body.toString().cyan);
|
||||
if (res && 'replay-nonce' in res.headers) {
|
||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||
self.nonces.push(res.headers['replay-nonce']);
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
cb(new Error('Failed to get nonce for request'));
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.post=function(url, body, cb) {
|
||||
var self=this, payload, jws, signed;
|
||||
|
||||
if (this.nonces.length===0) {
|
||||
this.getNonce(url, function(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
self.post(url, body, cb);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ('replay-nonce' in res.headers) {
|
||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||
self.nonces.push(res.headers['replay-nonce']);
|
||||
}
|
||||
log('Using nonce: '+this.nonces[0]);
|
||||
payload=JSON.stringify(body, null, 2);
|
||||
jws=generateSignature(
|
||||
this.privateKey, new Buffer(payload), this.nonces.shift()
|
||||
);
|
||||
signed=JSON.stringify(jws, null, 2);
|
||||
|
||||
cb(err, res, body);
|
||||
});
|
||||
};
|
||||
log('Posting to '+url);
|
||||
log(signed.green);
|
||||
log('Payload:'+payload.blue);
|
||||
|
||||
Acme.parseLink = function parseLink(link) {
|
||||
var links;
|
||||
try {
|
||||
links=link.split(',').map(function(link) {
|
||||
var parts, url, info;
|
||||
parts=link.trim().split(';');
|
||||
url=parts.shift().replace(/[<>]/g, '');
|
||||
info=parts.reduce(function(acc, p) {
|
||||
var m=p.trim().match(/(.+) *= *"(.+)"/);
|
||||
if (m) {
|
||||
acc[m[1]]=m[2];
|
||||
return request.post({
|
||||
url:url,
|
||||
body:signed,
|
||||
encoding:null
|
||||
}, function(err, res, body) {
|
||||
var parsed;
|
||||
|
||||
if (err) {
|
||||
console.error(err.stack);
|
||||
return cb(err);
|
||||
}
|
||||
if (res) {
|
||||
log(('HTTP/1.1 '+res.statusCode).yellow);
|
||||
}
|
||||
|
||||
Object.keys(res.headers).forEach(function(key) {
|
||||
var value, upcased;
|
||||
value=res.headers[key];
|
||||
upcased=key.charAt(0).toUpperCase()+key.slice(1);
|
||||
log((upcased+': '+value).yellow);
|
||||
});
|
||||
|
||||
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
|
||||
try {
|
||||
parsed=JSON.parse(body);
|
||||
log(JSON.stringify(parsed, null, 2).cyan);
|
||||
} catch(err) {
|
||||
log(body.toString().cyan);
|
||||
}
|
||||
}
|
||||
|
||||
if ('replay-nonce' in res.headers) {
|
||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||
self.nonces.push(res.headers['replay-nonce']);
|
||||
}
|
||||
|
||||
cb(err, res, body);
|
||||
});
|
||||
};
|
||||
|
||||
Acme.parseLink = function parseLink(link) {
|
||||
var links;
|
||||
try {
|
||||
links=link.split(',').map(function(link) {
|
||||
var parts, url, info;
|
||||
parts=link.trim().split(';');
|
||||
url=parts.shift().replace(/[<>]/g, '');
|
||||
info=parts.reduce(function(acc, p) {
|
||||
var m=p.trim().match(/(.+) *= *"(.+)"/);
|
||||
if (m) {
|
||||
acc[m[1]]=m[2];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
info.url=url;
|
||||
return info;
|
||||
}).reduce(function(acc, link) {
|
||||
if ('rel' in link) {
|
||||
acc[link.rel]=link.url;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
info.url=url;
|
||||
return info;
|
||||
}).reduce(function(acc, link) {
|
||||
if ('rel' in link) {
|
||||
acc[link.rel]=link.url;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return links;
|
||||
} catch(err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return links;
|
||||
} catch(err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Acme;
|
||||
return Acme;
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// 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");
|
|
@ -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 = {};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
26
package.json
26
package.json
|
@ -1,30 +1,30 @@
|
|||
{
|
||||
"name": "letiny",
|
||||
"version": "0.0.5-beta",
|
||||
"name": "letiny-core",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/anatolsommer/letiny.git"
|
||||
"url": "https://github.com/Daplie/letiny-core.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tiny",
|
||||
"acme",
|
||||
"letsencrypt",
|
||||
"client",
|
||||
"cli",
|
||||
"pem",
|
||||
"pfx"
|
||||
],
|
||||
"bin": {
|
||||
"letiny": "./lib/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.1.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-forge": "^0.6.21",
|
||||
"request": "^2.55.0",
|
||||
"commander": "^2.9.0"
|
||||
"node-forge": "^0.6.38",
|
||||
"request": "^2.55.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"ursa": "^0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^2.3.3",
|
||||
|
|
Loading…
Reference in New Issue