letsencrypt =========== Let's Encrypt for node.js This allows you to get Free SSL Certificates for Automatic HTTPS. NOT YET PUBLISHED ============ * Dec 12 2015: gettin' really close * Dec 11 2015: almost done (node-letsencrypt-python complete) * Dec 10 2015: began tinkering Install ======= ```bash npm install --save letsencrypt ``` Right now this uses [`letsencrypt-python`](https://github.com/Daplie/node-letsencrypt-python), but it's built to be able to use a pure javasript version. ```bash # install the python client (takes 2 minutes normally, 20 on a rasberry pi) git clone https://github.com/letsencrypt/letsencrypt pushd letsencrypt ./letsencrypt-auto ``` Usage ===== * `Letsencrypt.create(backend, bkDefaults);` * { webrootPath, configDir, fullchainTpl, privkeyTpl } * `le.middleware();` * `le.sniCallback(hostname, function (err, tlsContext) {});` * `le.register({ domains, email, agreeTos, ... })` returns promise ```javascript var leBinPath = require('os').homedir() + '/.local/share/letsencrypt/bin/letsencrypt'; var lep = require('letsencrypt-python').create(leBinPath); // backend-specific defaults // Note: For legal reasons you should NOT set email or agreeTos as a default var bkDefaults = { webroot: true , webrootPath: __dirname, '/acme-challenge' , fullchainTpl: '/live/:hostname/fullchain.pem' , privkeyTpl: '/live/:hostname/fullchain.pem' , configDir: '/etc/letsencrypt' , logsDir: '/var/log/letsencrypt' , workDir: '/var/lib/letsencrypt' , text: true }; var leConfig = { , webrootPath: __dirname, '/acme-challenge' , configDir: '/etc/letsencrypt' }; var le = require('letsencrypt').create(le, bkDefaults, leConfig); ``` ```javascript var leBinPath = '/home/user/.local/share/letsencrypt/bin/letsencrypt'; var lep = require('letsencrypt-python').create(leBinPath); // backend-specific defaults // Note: For legal reasons you should NOT set email or agreeTos as a default var bkDefaults = { webroot: true , webrootPath: __dirname, '/acme-challenge' , fullchainTpl: '/live/:hostname/fullchain.pem' , privkeyTpl: '/live/:hostname/fullchain.pem' , configDir: '/etc/letsencrypt' , logsDir: '/var/log/letsencrypt' , workDir: '/var/lib/letsencrypt' , text: true }; var leConfig = { , webrootPath: __dirname, '/acme-challenge' , configDir: '/etc/letsencrypt' }; var le = require('letsencrypt').create(le, bkDefaults, leConfig); var localCerts = require('localhost.daplie.com-certificates'); var express = require('express'); var app = express(); app.use(le.middleware); server = require('http').createServer(); server.on('request', app); server.listen(80, function () { console.log('Listening http', server.address()); }); tlsServer = require('https').createServer({ key: localCerts.key , cert: localCerts.cert , SNICallback: le.SNICallback }); tlsServer.on('request', app); tlsServer.listen(443, function () { console.log('Listening http', server.address()); }); le.register({ , domains: ['example.com'] , agreeTos: true , email: 'user@example.com' }).then(function () { server.close(); tlsServer.close(); }); ``` ``` lep.register('certonly', { , domains: ['example.com'] , agreeTos: true , email: 'user@example.com' , configDir: '/etc/letsencrypt' , logsDir: '/var/log/letsencrypt' , workDir: '/var/lib/letsencrypt' , text: true }); ``` ``` // if you would like to register new domains on-the-fly // you can use this function to return the user to which // it should be registered (or null if none) , needsRegistration: function (hostname, cb) { cb(null, { agreeTos: true , email: 'user@example.com' }); } ``` Backends -------- * [`letsencrypt-python`](https://github.com/Daplie/node-letsencrypt-python) (complete) * [`lejs`](https://github.com/Daplie/node-lejs) (in progress) #### How to write a backend A backend must implement (or be wrapped to implement) this API: * fetch(hostname, cb) will cb(err, certs) will get registered certs or null unless there is an error * register(args, challengeCb, done) will register and or renew a cert * args = `{ domains, email, agreeTos }` MUST check that agreeTos === true * challengeCb = `function (challenge, cb) { }` handle challenge as needed, call cb() This is what `args` looks like: ```javascript { domains: ['example.com', 'www.example.com'] , email: 'user@email.com' , agreeTos: true , configDir: '/etc/letsencrypt' , fullchainTpl: '/live/:hostname/fullchain.pem' // :hostname will be replaced with the domainname , privkeyTpl: '/live/:hostname/privkey.pem' // :hostname } ``` This is what the implementation should look like: (it's expected that the client will follow the same conventions as the python client, but it's not necessary) ```javascript return { fetch: function (args, cb) { // NOTE: should return an error if args.domains cannot be satisfied with a single cert // (usually example.com and www.example.com will be handled on the same cert, for example) if (errorHappens) { // return an error if there is an actual error (db, etc) cb(err); return; } // return null if there is no error, nor a certificate else if (!cert) { cb(null, null); return; } // NOTE: if the certificate is available but expired it should be // returned and the calling application will decide to renew when // it is convenient // NOTE: the application should handle caching, not the library // return the cert with metadata cb(null, { cert: "/*contcatonated certs in pem format: cert + intermediate*/" , key: "/*private keypair in pem format*/" , renewedAt: new Date() // fs.stat cert.pem should also work , expiresIn: 90 * 60 // assumes 90-days unless specified }); } , register: function (args, challengeCallback, completeCallback) { // **MUST** reject if args.agreeTos is not true // once you're ready for the caller to know the challenge if (challengeCallback) { challengeCallback(challenge, function () { continueRegistration(); }) } else { continueRegistration(); } function continueRegistration() { // it is not neccessary to to return the certificates here // the client will call fetch() when it needs them completeCallback(err); } } }; ``` See Also ======== * [Let's Encrypt in (exactly) 90 seconds with Caddy](https://daplie.com/articles/lets-encrypt-in-literally-90-seconds/) * Let's Encrypt for golang [lego](https://github.com/xenolf/lego) LICENSE ======= Dual-licensed MIT and Apache-2.0 See LICENSE