acme2: dns authorization

This commit is contained in:
Girish Ramakrishnan
2018-09-10 20:50:36 -07:00
parent 2f38a4018c
commit d0dde04695
3 changed files with 141 additions and 54 deletions
+137 -40
View File
@@ -4,6 +4,7 @@ var assert = require('assert'),
async = require('async'), async = require('async'),
crypto = require('crypto'), crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'), debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
execSync = require('safetydance').child_process.execSync, execSync = require('safetydance').child_process.execSync,
fs = require('fs'), fs = require('fs'),
path = require('path'), path = require('path'),
@@ -60,6 +61,7 @@ function Acme2(options) {
this.keyId = null; this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL; this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {}; this.directory = {};
this.performHttpAuthorization = !!options.performHttpAuthorization;
} }
Acme2.prototype.getNonce = function (callback) { Acme2.prototype.getNonce = function (callback) {
@@ -244,34 +246,19 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
}, callback); }, callback);
}; };
Acme2.prototype.prepareHttpChallenge = function (challenge, callback) { Acme2.prototype.getKeyAuthorization = function (token) {
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
assert.strictEqual(typeof callback, 'function');
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
var token = challenge.token;
assert(util.isBuffer(this.accountKeyPem)); assert(util.isBuffer(this.accountKeyPem));
var jwk = { let jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537 e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA', kty: 'RSA',
n: b64(getModulus(this.accountKeyPem)) n: b64(getModulus(this.accountKeyPem))
}; };
var shasum = crypto.createHash('sha256'); let shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk)); shasum.update(JSON.stringify(jwk));
var thumbprint = urlBase64Encode(shasum.digest('base64')); let thumbprint = urlBase64Encode(shasum.digest('base64'));
var keyAuthorization = token + '.' + thumbprint; return token + '.' + thumbprint;
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
callback();
});
}; };
Acme2.prototype.notifyChallengeReady = function (challenge, callback) { Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
@@ -280,7 +267,7 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
debug('notifyChallengeReady: %s was met', challenge.url); debug('notifyChallengeReady: %s was met', challenge.url);
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8'); const keyAuthorization = this.getKeyAuthorization(challenge.token);
var payload = { var payload = {
resource: 'challenge', resource: 'challenge',
@@ -403,16 +390,128 @@ Acme2.prototype.downloadCertificate = function (domain, certUrl, callback) {
}); });
}; };
Acme2.prototype.getAuthorization = function (url, callback) { Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
superagent.get(url).timeout(30 * 1000).end(function (error, response) { assert.strictEqual(typeof hostname, 'string');
if (error && !error.response) return callback(error); assert.strictEqual(typeof domain, 'string');
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode)); assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(null, response.body); debug('acmeFlow: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
callback(null, challenge);
}); });
}; };
Acme2.prototype.acmeFlow = function (domain, callback) { Acme2.prototype.cleanupHttpChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
safe.fs.unlinkSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
callback();
};
Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
let challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
const subdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
debug(`prepareDnsChallenge: update ${subdomain}} with ${txtValue}`);
domains.upsertDnsRecords(subdomain, domain, 'TXT', [ txtValue ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
callback(null, challenge);
});
};
Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
const subdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
debug(`prepareDnsChallenge: remove ${subdomain} with ${txtValue}`);
domains.removeDnsRecords(subdomain, domain, 'TXT', [ txtValue ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
callback(null, challenge);
});
callback();
};
Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof callback, 'function');
const that = this;
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
if (that.performHttpAuthorization) {
that.prepareHttpChallenge(hostname, domain, authorization, callback);
} else {
that.prepareDnsChallenge(hostname, domain, authorization, callback);
}
});
};
Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
if (this.performHttpAuthorization) {
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
} else {
this.cleanupDnsChallenge(hostname, domain, challenge, callback);
}
};
Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -431,29 +530,27 @@ Acme2.prototype.acmeFlow = function (domain, callback) {
this.registerUser(function (error) { this.registerUser(function (error) {
if (error) return callback(error); if (error) return callback(error);
that.newOrder(domain, function (error, order, orderUrl) { that.newOrder(hostname, function (error, order, orderUrl) {
if (error) return callback(error); if (error) return callback(error);
async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) { async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) {
debug(`acmeFlow: authorizing ${authorizationUrl}`); debug(`acmeFlow: authorizing ${authorizationUrl}`);
that.getAuthorization(authorizationUrl, function (error, authorization) { that.prepareChallenge(hostname, domain, authorizationUrl, function (error, challenge) {
if (error) return iteratorCallback(error); if (error) return iteratorCallback(error);
debug('acmeFlow: challenges: %j', authorization);
var httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([ async.waterfall([
that.prepareHttpChallenge.bind(that, challenge),
that.notifyChallengeReady.bind(that, challenge), that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge), that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, domain), that.createKeyAndCsr.bind(that, hostname),
that.signCertificate.bind(that, domain, order.finalize), that.signCertificate.bind(that, hostname, order.finalize),
that.waitForOrder.bind(that, orderUrl), that.waitForOrder.bind(that, orderUrl),
that.downloadCertificate.bind(that, domain) that.downloadCertificate.bind(that, hostname)
], iteratorCallback); ], function (error) {
that.cleanupChallenge(hostname, domain, challenge, function () {
iteratorCallback(error);
});
});
}); });
}, callback); }, callback);
}); });
@@ -488,7 +585,7 @@ Acme2.prototype.getCertificate = function (hostname, domain, callback) {
this.getDirectory(function (error) { this.getDirectory(function (error) {
if (error) return callback(error); if (error) return callback(error);
that.acmeFlow(hostname, function (error) { that.acmeFlow(hostname, domain, function (error) {
if (error) return callback(error); if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR; var outdir = paths.APP_CERTS_DIR;
+3 -4
View File
@@ -89,10 +89,9 @@ function getCertApi(domain, callback) {
var api = result.tlsConfig.provider === 'caas' ? caas : acme2; var api = result.tlsConfig.provider === 'caas' ? caas : acme2;
var options = { }; var options = { };
if (result.tlsConfig.provider === 'caas') { if (result.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
options.prod = true; options.prod = result.tlsConfig.provider.match(/.*-prod/) !== null;
} else { // acme options.performHttpAuthorization = result.provider.match(/noop|manual|wildcard/) !== null;
options.prod = result.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
} }
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197) // registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
+1 -10
View File
@@ -131,16 +131,7 @@ describe('Certificates', function () {
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) { reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null); expect(error).to.be(null);
expect(api._name).to.be('caas'); expect(api._name).to.be('caas');
expect(options.prod).to.be(true); expect(options).to.eql({ email: 'support@cloudron.io' });
done();
});
});
it('returns prod caas for dev cloudron', function (done) {
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
done(); done();
}); });
}); });