diff --git a/README.md b/README.md index ed1e892..2c6c45e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,59 @@ acme-v2.js (draft 11) ========== +| [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) +| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) +| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) + | Sponsored by [ppl](https://ppl.family) -A framework for building letsencrypt v2 (IETF ACME draft 11) clients, successor to `le-acme-core.js`. +A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`. +Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). -Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 +## Looking for Quick 'n' Easy™? + +If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). +If you're looking to *build a webserver*, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). + +* [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +* [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) + +## How to build ACME clients + +As this is intended to build ACME clients, there is not a simple 2-line example. + +I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example: + +```bash +node examples/cli.js +``` + +The example cli has the following prompts: + +``` +What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) +What challenge will you be testing today? http-01 or dns-01? [http-01] +What email should we use? (optional) +What API style would you like to test? v1-compat or promise? [v1-compat] + +Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' + +echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' + +Then hit the 'any' key to continue... +``` + +When you've completed the challenge you can hit a key to continue the process. + +If you place the certificate you receive back in `tests/fullchain.pem` +you can then test it with `examples/https-server.js`. + +``` +examples/cli.js +examples/genkeypair.js +tests/compat.js +``` ## Let's Encrypt Directory URLs @@ -136,7 +184,11 @@ Todo Changelog --------- -* v1.0.0 +* v1.0.2 + * use `options.contact` to provide raw contact array + * made `options.email` optional + * file cleanup +* v1.0.1 * Compat API is ready for use * Eliminate debug logging * Apr 10, 2018 - tested backwards-compatibility using greenlock.js diff --git a/test.js b/examples/cli.js similarity index 51% rename from test.js rename to examples/cli.js index 6490e34..f26354a 100644 --- a/test.js +++ b/examples/cli.js @@ -7,6 +7,8 @@ var rl = readline.createInterface({ output: process.stdout }); +require('./genkeypair.js'); + function getWeb() { rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { web = (web||'').trim().split(/,/g); @@ -35,12 +37,31 @@ function getEmail(web, chType) { email = (email||'').trim(); if (!email) { email = null; } + getApiStyle(web, chType, email); + }); +} + +function getApiStyle(web, chType, email) { + var defaultStyle = 'compat'; + rl.question('What API style would you like to test? v1-compat or promise? [v1-compat] ', function (apiStyle) { + apiStyle = (apiStyle||'').trim(); + if (!apiStyle) { apiStyle = 'v1-compat'; } + rl.close(); - var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }); - var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); - //require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); - //require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); - require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); + + var RSA = require('rsa-compat').RSA; + var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/account.privkey.pem') }); + var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/privkey.pem') }); + var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + + if ('promise' === apiStyle) { + require('../tests/promise.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } else if ('cb' === apiStyle) { + require('../tests/cb.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } else { + if ('v1-compat' !== apiStyle) { console.warn("Didn't understand '" + apiStyle + "', using 'v1-compat' instead..."); } + require('../tests/compat.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } }); } diff --git a/examples/genkeypair.js b/examples/genkeypair.js new file mode 100644 index 0000000..2c7e3c6 --- /dev/null +++ b/examples/genkeypair.js @@ -0,0 +1,22 @@ +var RSA = require('rsa-compat').RSA; +var fs = require('fs'); + +if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem); + }); +} + +if (!fs.existsSync(__dirname + '/../tests/privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/privkey.pem', privkeyPem); + }); +} diff --git a/examples/http-server.js b/examples/http-server.js new file mode 100644 index 0000000..4195455 --- /dev/null +++ b/examples/http-server.js @@ -0,0 +1,7 @@ +'use strict'; + +var http = require('http'); +var express = require('express'); +var server = http.createServer(express.static('../tests')).listen(80, function () { + console.log('Listening on', this.address()); +}); diff --git a/examples/https-server.js b/examples/https-server.js new file mode 100644 index 0000000..5dd2c2c --- /dev/null +++ b/examples/https-server.js @@ -0,0 +1,11 @@ +'use strict'; + +var https = require('https'); +var server = https.createServer({ + key: require('fs').readFileSync('../tests/privkey.pem') +, cert: require('fs').readFileSync('../tests/fullchain.pem') +}, function (req, res) { + res.end("Hello, World!"); +}).listen(443, function () { + console.log('Listening on', this.address()); +}); diff --git a/genkeypair.js b/genkeypair.js deleted file mode 100644 index f029ade..0000000 --- a/genkeypair.js +++ /dev/null @@ -1,18 +0,0 @@ -var RSA = require('rsa-compat').RSA; -var fs = require('fs'); - -RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); - - fs.writeFileSync(__dirname + '/account.privkey.pem', privkeyPem); -}); - -RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); - - fs.writeFileSync(__dirname + '/privkey.pem', privkeyPem); -}); diff --git a/node.js b/node.js index c743db9..c64d8df 100644 --- a/node.js +++ b/node.js @@ -112,10 +112,16 @@ ACME._registerAccount = function (me, options) { } var jwk = me.RSA.exportPublicJwk(options.accountKeypair); + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = [ 'mailto:' + options.email ] + } var body = { termsOfServiceAgreed: tosUrl === me._tos , onlyReturnExisting: false - , contact: [ 'mailto:' + options.email ] + , contact: contact }; if (options.externalAccount) { body.externalAccountBinding = me.RSA.signJws( @@ -150,6 +156,8 @@ ACME._registerAccount = function (me, options) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { + var account = resp.body; + me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; // the account id url @@ -157,15 +165,33 @@ ACME._registerAccount = function (me, options) { if (me.debug) console.debug('[DEBUG] new account location:'); if (me.debug) console.debug(location); if (me.debug) console.debug(resp.toJSON()); - return resp.body; + + /* + { + id: 5925245, + key: + { kty: 'RSA', + n: 'tBr7m1hVaUNQjUeakznGidnrYyegVUQrsQjNrcipljI9Vxvxd0baHc3vvRZWFyFO5BlS7UDl-KHQdbdqb-MQzfP6T2sNXsOHARQ41pCGY5BYzIPRJF0nD48-CY717is-7BKISv8rf9yx5iSjvK1wZ3Ke3YIpxzK2fWRqccVxXQ92VYioxOfGObACgEUSvdoEttWV2B0Uv4Sdi6zZbk5eo2zALvyGb1P4fKVfQycGLXC41AyhHOAuTqzNCyIkiWEkbfh2lZNcYClP2epS0pHRFXYyjJN6-c8InfM3PISo4k6Qew65HZ-oqUow0tTIgNwuen9q5O6Hc73GvU-2npGJVQ', + e: 'AQAB' }, + contact: [], + initialIp: '198.199.82.211', + createdAt: '2018-04-16T00:41:00.720584972Z', + status: 'valid' + } + */ + if (!account) { account = { _emptyResponse: true, key: {} }; } + account.key.kid = me._kid; + return account; }).then(resolve, reject); } if (me.debug) console.debug('[acme-v2] agreeToTerms'); if (1 === options.agreeToTerms.length) { + // newer promise API return options.agreeToTerms(me._tos).then(agree, reject); } else if (2 === options.agreeToTerms.length) { + // backwards compat cb API return options.agreeToTerms(me._tos, function (err, tosUrl) { if (!err) { agree(tosUrl); return; } reject(err); diff --git a/package.json b/package.json index 5983681..618cd9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.1", + "version": "1.0.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", diff --git a/test.cb.js b/tests/cb.js similarity index 91% rename from test.cb.js rename to tests/cb.js index f2c232c..550c285 100644 --- a/test.cb.js +++ b/tests/cb.js @@ -1,10 +1,8 @@ 'use strict'; -module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { - var RSA = require('rsa-compat').RSA; - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./').ACME.create({ RSA: RSA }); +module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + var acme2 = require('../').ACME.create({ RSA: RSA }); acme2.init(directoryUrl).then(function () { var options = { agreeToTerms: function (tosUrl, agree) { diff --git a/test.compat.js b/tests/compat.js similarity index 86% rename from test.compat.js rename to tests/compat.js index 73060bd..d0a66b1 100644 --- a/test.compat.js +++ b/tests/compat.js @@ -1,11 +1,9 @@ 'use strict'; -var RSA = require('rsa-compat').RSA; - -module.exports.run = function (web, chType, email, accountKeypair, domainKeypair) { +module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { console.log('[DEBUG] run', web, chType, email); - var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); + var acme2 = require('../compat.js').ACME.create({ RSA: RSA }); acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) { if (err) { console.log('err 1'); throw err; } @@ -44,8 +42,8 @@ module.exports.run = function (web, chType, email, accountKeypair, domainKeypair acme2.registerNewAccount(options, function (err, account) { if (err) { console.log('err 2'); throw err; } - console.log('account:'); - console.log(account); + if (options.debug) console.debug('account:'); + if (options.debug) console.log(account); acme2.getCertificate(options, function (err, fullchainPem) { if (err) { console.log('err 3'); throw err; } diff --git a/test.promise.js b/tests/promise.js similarity index 91% rename from test.promise.js rename to tests/promise.js index bf04f60..ee2a028 100644 --- a/test.promise.js +++ b/tests/promise.js @@ -1,10 +1,8 @@ 'use strict'; /* global Promise */ -module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { - var RSA = require('rsa-compat').RSA; - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./').ACME.create({ RSA: RSA }); +module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { + var acme2 = require('../').ACME.create({ RSA: RSA }); // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' acme2.init(directoryUrl).then(function () { var options = {