# letiny-core A framework for building letsencrypt clients, forked from `letiny`. Supports all of: * node with `ursa` (works fast) * node with `forge` (works on windows) * browser WebCrypto (not implemented, but... Let's Encrypt over WebRTC anyone?) * any javascript implementation ### 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`](https://github.com/Daplie/node-letsencrypt) (compatible with the official client) * `letiny` (lightweight client cli) * [`letsencrypt-express`](https://github.com/Daplie/letsencrypt-express) (automatic https for express) ## Install & Usage: ```bash npm install --save letiny-core ``` 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` * 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` ### Demo You can see this working for yourself, but you'll need to be on an internet connected computer with a domain. Get a temporary domain for testing ```bash npm install -g ddns-cli ddns --random --email user@example.com --agree ``` Note: use **YOUR EMAIL** and accept the terms of service (run `ddns --help` to see them). Install letiny-core and its dependencies. **Note**: it's okay if you're on windows and `ursa` fails to compile. It'll still work. ```bash git clone https://github.com/Daplie/letiny-core.git ~/letiny-core pushd ~/letiny-core npm install ``` Run the demo: ```bash node examples/letsencrypt.js user@example.com example.com ``` Note: use **YOUR TEMPORARY DOMAIN** and **YOUR EMAIL**. ## API The Goodies ```javascript // Accounts LeCore.registerNewAccount(options, cb) // returns "regr" registration data { newRegUrl: '' // no defaults, specify acmeUrls.newAuthz , email: '' // valid email (server checks MX records) , accountPrivateKeyPem: '' // callback to allow user interaction for tosUrl , agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end) } // Registration LeCore.getCertificate(options, cb) // returns (err, pems={ key, cert, ca }) { newAuthzUrl: '' // specify acmeUrls.newAuthz , newCertUrl: '' // specify acmeUrls.newCert , domainPrivateKeyPem: '' , accountPrivateKeyPem: '' , domains: ['example.com'] , setChallenge: fn (hostname, key, val, cb) , removeChallenge: fn (hostname, key, cb) } // Discovery URLs LeCore.getAcmeUrls(acmeDiscoveryUrl, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert}) ``` Helpers & Stuff ```javascript // Constants LeCore.productionServerUrl // https://acme-v01.api.letsencrypt.org/directory LeCore.stagingServerUrl // https://acme-staging.api.letsencrypt.org/directory LeCore.acmeChallengePrefix // /.well-known/acme-challenge/ LeCore.configDir // /etc/letsencrypt/ LeCore.logsDir // /var/log/letsencrypt/ LeCore.workDir // /var/lib/letsencrypt/ LeCore.knownEndpoints // new-authz, new-cert, new-reg, revoke-cert // HTTP Client Helpers 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 // Note: some of these are not async, // but they will be soon. Don't rely // on their API yet. // Crypto Helpers LeCore.leCrypto generateRsaKeypair(bitLen, exponent, cb); // returns { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } thumbprint(lePubKey) // generates public key thumbprint generateSignature(lePrivKey, bodyBuf, nonce) // generates a signature privateJwkToPems(jwk) // { n: '...', e: '...', iq: '...', ... } to PEMs privatePemToJwk // PEM to JWK (see line above) importPemPrivateKey(privateKeyPem) // (internal) returns abstract private key ``` For testing and development, you can also inject the dependencies you want to use: ```javascript LeCore = LeCore.create({ request: require('request') , leCrypto: rquire('./lib/letsencrypt-forge') }); // now uses node `request` (could also use jQuery or Angular in the browser) LeCore.getAcmeUrls(discoveryUrl, function (err, urls) { console.log(urls); }); ``` ## Example Below you'll find a stripped-down example. You can see the full example in the example folder. * [example/](https://github.com/Daplie/letiny-core/blob/master/example/) #### Register Account & Domain This is how you **register an ACME account** and **get an HTTPS certificate** ```javascript 'use strict'; var LeCore = require('letiny-core'); 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() { LeCore.registerNewAccount( { newRegUrl: acmeUrls.newReg , email: email , accountPrivateKeyPem: accountPrivateKeyPem , agreeToTerms: function (tosUrl, done) { // agree to the exact version of these terms done(null, tosUrl); } } , function (err, regr) { LeCore.getCertificate( { newAuthzUrl: acmeUrls.newAuthz , newCertUrl: acmeUrls.newCert , domainPrivateKeyPem: domainPrivateKeyPem , accountPrivateKeyPem: accountPrivateKeyPem , domains: domains , setChallenge: challengeStore.set , removeChallenge: challengeStore.remove } , function (err, certs) { // Note: you should save certs to disk (or db) certStore.set(domains[0], certs, function () { // ... }); } ); } ); } ``` **But wait**, there's more! See [example/letsencrypt.js](https://github.com/Daplie/letiny-core/blob/master/example/letsencrypt.js) #### Run a Server on 80, 443, and 5001 (https/tls) 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 ```javascript var https = require('https'); var http = require('http'); var LeCore = deps.LeCore; var httpsOptions = deps.httpsOptions; var challengeStore = deps.challengeStore; var certStore = deps.certStore; // // Challenge Handler // function acmeResponder(req, res) { if (0 !== req.url.indexOf(LeCore.acmeChallengePrefix)) { res.end('Hello World!'); return; } var key = req.url.slice(LeCore.acmeChallengePrefix.length); challengeStore.get(req.hostname, key, function (err, val) { res.end(val || 'Error'); }); } // // 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()); }); ``` **But wait**, there's more! See [example/serve.js](https://github.com/Daplie/letiny-core/blob/master/example/serve.js) #### Put some storage in place Finally, you need an implementation of `challengeStore`: ```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); } }; 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); } }; ``` **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) ## Authors * ISRG * Anatol Sommer (https://github.com/anatolsommer) * AJ ONeal (https://daplie.com) ## 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).