refactor and docs
This commit is contained in:
parent
b6dc41c704
commit
e651755417
129
README.md
129
README.md
|
@ -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).
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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");
|
|
@ -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",
|
"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",
|
||||||
|
|
Loading…
Reference in New Issue