From 11f2d37044f28432d8b8005abe356a3662d3a62a Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 8 Nov 2017 12:05:38 -0700 Subject: [PATCH] implemented dns-01 ACME challenges --- README.md | 6 ++ lib/ddns/challenge-responder.js | 122 ++++++++++++++++++++++++++++++++ lib/ddns/index.js | 2 + lib/tcp/index.js | 12 ++-- lib/tcp/tls.js | 3 +- 5 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 lib/ddns/challenge-responder.js diff --git a/README.md b/README.md index fcf263b..ac2d160 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,12 @@ tls: challenge_type: 'http-01' ``` +**NOTE:** If you specify `dns-01` as the challenge type there must also be a +[DDNS module](#ddns) defined for all of the relevant domains (though not all +domains handled by a single TLS module need to be handled by the same DDNS +module). The DDNS module provides all of the information needed to actually +set the DNS records needed to verify ownership. + ### tcp The tcp system handles both *raw* and *tls-terminated* tcp network traffic diff --git a/lib/ddns/challenge-responder.js b/lib/ddns/challenge-responder.js new file mode 100644 index 0000000..f82d635 --- /dev/null +++ b/lib/ddns/challenge-responder.js @@ -0,0 +1,122 @@ +'use strict'; + +// Much of this file was based on the `le-challenge-ddns` library (which we are not using +// here because it's method of setting records requires things we don't really want). +module.exports.create = function (deps, conf, utils) { + + function getReleventSessionId(domain) { + var sessId; + + utils.iterateAllModules(function (mod, domainList) { + // We return a truthy value in these cases because of the way the iterate function + // handles modules grouped by domain. By returning true we are saying these domains + // are "handled" and so if there are multiple modules we won't be given the rest. + if (sessId) { return true; } + if (domainList.indexOf(domain) < 0) { return true; } + + // But if the domains are relevant but we don't know how to handle the module we + // return false to allow us to look at any other modules that might exist here. + if (mod.type !== 'dns@oauth3.org') { return false; } + + sessId = mod.tokenId || mod.token_id; + return true; + }); + + return sessId; + } + + function get(args, domain, challenge, done) { + done(new Error("Challenge.get() does not need an implementation for dns-01. (did you mean Challenge.loopback?)")); + } + // same as get, but external + function loopback(args, domain, challenge, done) { + var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; + require('dns').resolveTxt(challengeDomain, done); + } + + var activeChallenges = {}; + async function removeAsync(args, domain) { + var data = activeChallenges[domain]; + if (!data) { + console.warn(new Error('cannot remove DNS challenge for ' + domain + ': already removed')); + return; + } + + var session = await utils.getSession(data.sessId); + var directives = await deps.OAUTH3.discover(session.token.aud); + var apiOpts = { + api: 'dns.unset' + , session: session + , type: 'TXT' + , value: data.keyAuthDigest + }; + await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, data.splitDomain)); + + delete activeChallenges[domain]; + } + async function setAsync(args, domain, challenge, keyAuth) { + if (activeChallenges[domain]) { + await removeAsync(args, domain, challenge); + } + + var sessId = getReleventSessionId(domain); + if (!sessId) { + throw new Error('no DDNS module handles the domain ' + domain); + } + var session = await utils.getSession(sessId); + var directives = await deps.OAUTH3.discover(session.token.aud); + + // I'm not sure what role challenge is supposed to play since even in the library + // this code is based on it was never used, but check for it anyway because ... + if (!challenge || keyAuth) { + console.warn(new Error('DDNS challenge missing challenge or keyAuth')); + } + var keyAuthDigest = require('crypto').createHash('sha256').update(keyAuth || '').digest('base64') + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + + var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; + var splitDomain = (await utils.splitDomains(directives.api, [challengeDomain]))[0]; + + var apiOpts = { + api: 'dns.set' + , session: session + , type: 'TXT' + , value: keyAuthDigest + , ttl: args.ttl || 0 + }; + await deps.OAUTH3.api(directives.api, Object.assign({}, apiOpts, splitDomain)); + + activeChallenges[domain] = { + sessId + , keyAuthDigest + , splitDomain + }; + + return new Promise(res => setTimeout(res, 1000)); + } + + // It might be slightly easier to use arguments and apply, but the library that will use + // this function counts the arguments we expect. + function set(a, b, c, d, done) { + setAsync(a, b, c, d).then(result => done(null, result), done); + } + function remove(a, b, c, done) { + removeAsync(a, b, c).then(result => done(null, result), done); + } + + function getOptions() { + return { + oauth3: 'oauth3.org' + , debug: conf.debug + , acmeChallengeDns: '_acme-challenge.' + }; + } + + return { + getOptions + , set + , get + , remove + , loopback + }; +}; diff --git a/lib/ddns/index.js b/lib/ddns/index.js index adce60d..f7cabe6 100644 --- a/lib/ddns/index.js +++ b/lib/ddns/index.js @@ -8,6 +8,7 @@ module.exports.create = function (deps, conf) { var utils = require('./utils').create(deps, conf); var loopback = require('./loopback').create(deps, conf, utils); var dnsCtrl = require('./dns-ctrl').create(deps, conf, utils); + var challenge = require('./challenge-responder').create(deps, conf, utils); var tunnelClients = require('./tunnel-client-manager').create(deps, conf, utils); var loopbackDomain; @@ -312,5 +313,6 @@ module.exports.create = function (deps, conf) { , getDeviceAddresses: dnsCtrl.getDeviceAddresses , recheckPubAddr: recheckPubAddr , updateConf: updateConf + , challenge }; }; diff --git a/lib/tcp/index.js b/lib/tcp/index.js index fb69fc9..eb7fbbf 100644 --- a/lib/tcp/index.js +++ b/lib/tcp/index.js @@ -159,11 +159,13 @@ module.exports.create = function (deps, config) { }); } - modules = {}; - modules.tcpHandler = tcpHandler; - modules.proxy = require('./proxy-conn').create(deps, config); - modules.tls = require('./tls').create(deps, config, modules); - modules.http = require('./http').create(deps, config, modules); + process.nextTick(function () { + modules = {}; + modules.tcpHandler = tcpHandler; + modules.proxy = require('./proxy-conn').create(deps, config); + modules.tls = require('./tls').create(deps, config, modules); + modules.http = require('./http').create(deps, config, modules); + }); function updateListeners() { var current = listeners.list(); diff --git a/lib/tcp/tls.js b/lib/tcp/tls.js index 60868a0..34d899f 100644 --- a/lib/tcp/tls.js +++ b/lib/tcp/tls.js @@ -86,8 +86,7 @@ module.exports.create = function (deps, config, tcpMods) { , challenges: { 'http-01': require('le-challenge-fs').create({ debug: config.debug }) , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug }) - // TODO dns-01 - //, 'dns-01': require('le-challenge-ddns').create({ debug: config.debug }) + , 'dns-01': deps.ddns.challenge } , challengeType: 'http-01'