2015-12-15 22:07:02 +00:00
|
|
|
# letiny-core
|
2015-12-13 16:50:04 +00:00
|
|
|
|
2015-12-15 22:07:02 +00:00
|
|
|
A framework for building letsencrypt clients, forked from `letiny`.
|
|
|
|
|
2015-12-16 03:33:33 +00:00
|
|
|
Supports all of:
|
|
|
|
|
2015-12-15 22:07:02 +00:00
|
|
|
* node with `ursa` (works fast)
|
2015-12-16 00:58:36 +00:00
|
|
|
* node with `forge` (works on windows)
|
2015-12-16 03:33:33 +00:00
|
|
|
* browser WebCrypto (not implemented, but... Let's Encrypt over WebRTC anyone?)
|
2015-12-15 22:07:02 +00:00
|
|
|
* any javascript implementation
|
2015-12-13 16:50:04 +00:00
|
|
|
|
2015-12-16 01:33:05 +00:00
|
|
|
### 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` (100% compatible with the official client)
|
|
|
|
* `letiny` (lightweight client)
|
|
|
|
* `letsencrypt-express` (automatic https for express)
|
|
|
|
|
2015-12-13 16:50:04 +00:00
|
|
|
## Usage:
|
|
|
|
|
2015-12-15 22:07:02 +00:00
|
|
|
```bash
|
|
|
|
npm install --save letiny-core
|
2015-12-13 16:50:04 +00:00
|
|
|
```
|
|
|
|
|
2015-12-16 00:58:36 +00:00
|
|
|
You will follow these steps to obtain certificates:
|
|
|
|
|
|
|
|
* discover ACME registration urls with `getAcmeUrls`
|
|
|
|
* register a user account with `registerNewAccount`
|
|
|
|
* implement a method to agree to the terms of service as `agreeToTos`
|
|
|
|
* get certificates with `getCertificate`
|
2015-12-16 01:18:40 +00:00
|
|
|
* implement a method to store the challenge token as `setChallenge`
|
|
|
|
* implement a method to get the challenge token as `getChallenge`
|
|
|
|
* implement a method to remove the challenge token as `removeChallenge`
|
2015-12-16 00:58:36 +00:00
|
|
|
|
2015-12-16 04:18:01 +00:00
|
|
|
#### Register Account & Domain
|
|
|
|
|
2015-12-16 04:34:16 +00:00
|
|
|
**But wait**, there's more!
|
|
|
|
See [example/letsencrypt.js](https://github.com/Daplie/letiny-core/blob/master/example/letsencrypt.js)
|
|
|
|
|
|
|
|
See
|
|
|
|
|
2015-12-15 22:07:02 +00:00
|
|
|
```javascript
|
|
|
|
'use strict';
|
|
|
|
|
2015-12-16 00:51:44 +00:00
|
|
|
var LeCore = require('letiny-core');
|
2015-12-16 01:18:40 +00:00
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
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 accountPrivateKeyPem = null;
|
|
|
|
var domainPrivateKeyPem = null;
|
|
|
|
var acmeUrls = null;
|
|
|
|
|
|
|
|
LeCore.leCrypto.generateRsaKeypair(2048, 65537, function (err, pems) {
|
|
|
|
// ...
|
|
|
|
LeCore.getAcmeUrls(acmeDiscoveryUrl, function (err, urls) {
|
|
|
|
// ...
|
|
|
|
runDemo();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function runDemo() {
|
2015-12-16 00:51:44 +00:00
|
|
|
LeCore.registerNewAccount(
|
2015-12-16 04:32:40 +00:00
|
|
|
{ newRegUrl: acmeUrls.newReg
|
|
|
|
, email: email
|
|
|
|
, accountPrivateKeyPem: accountPrivateKeyPem
|
|
|
|
, agreeToTerms: function (tosUrl, done) {
|
|
|
|
|
|
|
|
// agree to the exact version of these terms
|
|
|
|
done(null, tosUrl);
|
|
|
|
}
|
2015-12-16 00:51:44 +00:00
|
|
|
}
|
2015-12-16 04:32:40 +00:00
|
|
|
, function (err, regr) {
|
2015-12-16 00:51:44 +00:00
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
console.log('Registering New Certificate');
|
|
|
|
LeCore.getCertificate(
|
|
|
|
{ newAuthzUrl: acmeUrls.newAuthz
|
|
|
|
, newCertUrl: acmeUrls.newCert
|
2015-12-16 00:51:44 +00:00
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
, domainPrivateKeyPem: domainPrivateKeyPem
|
|
|
|
, accountPrivateKeyPem: accountPrivateKeyPem
|
|
|
|
, domains: domains
|
2015-12-16 03:23:34 +00:00
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
, setChallenge: challengeStore.set
|
|
|
|
, removeChallenge: challengeStore.remove
|
|
|
|
}
|
|
|
|
, function (err, certs) {
|
2015-12-16 03:23:34 +00:00
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
// Note: you should save certs to disk (or db)
|
|
|
|
certStore.set(domains[0], certs, function () {
|
2015-12-16 00:51:44 +00:00
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
// ...
|
|
|
|
|
|
|
|
});
|
2015-12-16 00:51:44 +00:00
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2015-12-16 00:51:44 +00:00
|
|
|
);
|
2015-12-16 04:32:40 +00:00
|
|
|
}
|
2015-12-13 16:50:04 +00:00
|
|
|
```
|
|
|
|
|
2015-12-16 04:18:01 +00:00
|
|
|
#### Run a Server on 80, 443, and 5001 (https/tls)
|
|
|
|
|
2015-12-16 01:18:40 +00:00
|
|
|
That will fail unless you have a webserver running on 80 and 443 (or 5001)
|
|
|
|
to respond to `/.well-known/acme-challenge/xxxxxxxx` with the proper token
|
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
**But wait**, there's more!
|
|
|
|
See [example/serve.js](https://github.com/Daplie/letiny-core/blob/master/example/serve.js)
|
|
|
|
|
2015-12-16 01:18:40 +00:00
|
|
|
```javascript
|
2015-12-16 04:32:40 +00:00
|
|
|
var https = require('https');
|
2015-12-16 01:18:40 +00:00
|
|
|
var http = require('http');
|
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
|
|
|
|
var LeCore = deps.LeCore;
|
|
|
|
var httpsOptions = deps.httpsOptions;
|
|
|
|
var challengeStore = deps.challengeStore;
|
|
|
|
var certStore = deps.certStore;
|
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
// Challenge Handler
|
|
|
|
//
|
2015-12-16 01:18:40 +00:00
|
|
|
function acmeResponder(req, res) {
|
2015-12-16 04:32:40 +00:00
|
|
|
if (0 !== req.url.indexOf(LeCore.acmeChallengePrefix)) {
|
2015-12-16 01:18:40 +00:00
|
|
|
res.end('Hello World!');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
var key = req.url.slice(LeCore.acmeChallengePrefix.length);
|
|
|
|
|
|
|
|
challengeStore.get(req.hostname, key, function (err, val) {
|
|
|
|
res.end(val || 'Error');
|
|
|
|
});
|
2015-12-16 01:18:40 +00:00
|
|
|
}
|
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// Server
|
|
|
|
//
|
|
|
|
https.createServer(httpsOptions, acmeResponder).listen(5001, function () {
|
|
|
|
console.log('Listening https on', this.address());
|
|
|
|
});
|
|
|
|
http.createServer(acmeResponder).listen(80, function () {
|
|
|
|
console.log('Listening http on', this.address());
|
|
|
|
});
|
2015-12-16 01:18:40 +00:00
|
|
|
```
|
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
#### Put some storage in place
|
|
|
|
|
2015-12-16 01:18:40 +00:00
|
|
|
Finally, you need an implementation of `challengeStore`:
|
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
**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)
|
2015-12-16 04:18:01 +00:00
|
|
|
|
2015-12-16 01:18:40 +00:00
|
|
|
```javascript
|
|
|
|
var challengeCache = {};
|
|
|
|
var challengeStore = {
|
|
|
|
set: function (hostname, key, value, cb) {
|
|
|
|
challengeCache[key] = value;
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
, get: function (hostname, key, cb) {
|
|
|
|
cb(null, challengeCache[key]);
|
|
|
|
}
|
|
|
|
, remove: function (hostname, key, cb) {
|
|
|
|
delete challengeCache[key];
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
};
|
2015-12-16 04:32:40 +00:00
|
|
|
|
|
|
|
var certCache = {};
|
|
|
|
var certStore = {
|
|
|
|
set: function (hostname, certs, cb) {
|
|
|
|
certCache[hostname] = certs;
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
, get: function (hostname, cb) {
|
|
|
|
cb(null, certCache[hostname]);
|
|
|
|
}
|
|
|
|
, remove: function (hostname, cb) {
|
|
|
|
delete certCache[hostname];
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
};
|
2015-12-16 01:18:40 +00:00
|
|
|
```
|
|
|
|
|
2015-12-15 22:07:02 +00:00
|
|
|
## API
|
|
|
|
|
2015-12-16 00:51:44 +00:00
|
|
|
The Goodies
|
|
|
|
|
2015-12-16 00:13:07 +00:00
|
|
|
```javascript
|
2015-12-16 00:51:44 +00:00
|
|
|
{ newRegUrl: '...' // no defaults, specify LeCore.nproductionServerUrl
|
|
|
|
|
2015-12-16 04:32:40 +00:00
|
|
|
// Accounts
|
2015-12-16 00:51:44 +00:00
|
|
|
LeCore.registerNewAccount(options, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert})
|
|
|
|
|
|
|
|
{ newRegUrl: '...' // no defaults, specify LeCore.newAuthz
|
|
|
|
, email: '...' // valid email (server checks MX records)
|
|
|
|
, agreeToTerms: fn (tosUrl, cb) {} // callback to allow user interaction for tosUrl
|
|
|
|
// cb(err=null, agree=tosUrl) // must specify agree=tosUrl to continue (or falsey to end)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Registration
|
|
|
|
LeCore.getCertificate(options, cb)
|
|
|
|
|
|
|
|
{ newAuthzUrl: '...' // no defaults, specify acmeUrls.newAuthz
|
|
|
|
```
|
2015-12-15 22:07:02 +00:00
|
|
|
|
2015-12-16 00:51:44 +00:00
|
|
|
Helpers & Stuff
|
2015-12-15 22:07:02 +00:00
|
|
|
|
2015-12-16 00:51:44 +00:00
|
|
|
```javascript
|
|
|
|
// Constants
|
|
|
|
LeCore.productionServerUrl // https://acme-v01.api.letsencrypt.org/directory
|
|
|
|
LeCore.stagingServerUrl // https://acme-staging.api.letsencrypt.org/directory
|
2015-12-16 01:18:40 +00:00
|
|
|
LeCore.acmeChallengePrefix // /.well-known/acme-challenge/
|
2015-12-16 00:51:44 +00:00
|
|
|
LeCore.configDir // /etc/letsencrypt/
|
|
|
|
LeCore.logsDir // /var/log/letsencrypt/
|
|
|
|
LeCore.workDir // /var/lib/letsencrypt/
|
2015-12-16 02:51:38 +00:00
|
|
|
LeCore.knownEndpoints // new-authz, new-cert, new-reg, revoke-cert
|
2015-12-16 00:51:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
// HTTP Client Helpers
|
2015-12-16 00:13:07 +00:00
|
|
|
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
|
2015-12-15 22:07:02 +00:00
|
|
|
|
2015-12-16 00:51:44 +00:00
|
|
|
// Note: some of these are not async,
|
|
|
|
// but they will be soon. Don't rely
|
|
|
|
// on their API yet.
|
|
|
|
|
|
|
|
// Crypto Helpers
|
2015-12-15 22:07:02 +00:00
|
|
|
LeCore.leCrypto
|
2015-12-16 00:51:44 +00:00
|
|
|
generateRsaKeypair(bitLen, exponent, cb); // returns { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 }
|
|
|
|
thumbprint(lePubKey) // generates public key thumbprint
|
2015-12-16 00:13:07 +00:00
|
|
|
generateSignature(lePrivKey, bodyBuf, nonce) // generates a signature
|
2015-12-16 00:51:44 +00:00
|
|
|
privateJwkToPems(jwk) // { n: '...', e: '...', iq: '...', ... } to PEMs
|
|
|
|
privatePemToJwk // PEM to JWK (see line above)
|
|
|
|
importPemPrivateKey(privateKeyPem) // (internal) returns abstract private key
|
2015-12-13 16:50:04 +00:00
|
|
|
```
|
|
|
|
|
2015-12-15 22:07:02 +00:00
|
|
|
For testing and development, you can also inject the dependencies you want to use:
|
|
|
|
|
|
|
|
```javascript
|
2015-12-16 00:51:44 +00:00
|
|
|
LeCore = LeCore.create({
|
2015-12-15 22:07:02 +00:00
|
|
|
request: require('request')
|
|
|
|
, leCrypto: rquire('./lib/letsencrypt-forge')
|
|
|
|
});
|
2015-12-16 00:51:44 +00:00
|
|
|
|
|
|
|
// now uses node `request` (could also use jQuery or Angular in the browser)
|
2015-12-16 00:58:36 +00:00
|
|
|
LeCore.getAcmeUrls(discoveryUrl, function (err, urls) {
|
|
|
|
console.log(urls);
|
2015-12-16 00:51:44 +00:00
|
|
|
});
|
2015-12-15 22:07:02 +00:00
|
|
|
```
|
2015-12-13 16:50:04 +00:00
|
|
|
|
2015-12-16 01:18:40 +00:00
|
|
|
## Authors
|
|
|
|
|
|
|
|
* ISRG
|
|
|
|
* Anatol Sommer (https://github.com/anatolsommer)
|
|
|
|
* AJ ONeal <aj@daplie.com> (https://daplie.com)
|
|
|
|
|
2015-12-13 16:50:04 +00:00
|
|
|
## Licence
|
2015-12-15 22:07:02 +00:00
|
|
|
|
2015-12-13 16:50:04 +00:00
|
|
|
MPL 2.0
|
|
|
|
|
2015-12-15 22:07:02 +00:00
|
|
|
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).
|