Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec2b1da8c | |||
| 190c2b2756 | |||
| 7c975384cd | |||
| fe042891a3 | |||
| a9b594373d | |||
| 5edc3cde2a | |||
| a636731764 | |||
| b4433af9b5 |
+70
-59
@@ -4,7 +4,6 @@
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme'),
|
||||
fs = require('fs'),
|
||||
@@ -18,7 +17,6 @@ var assert = require('assert'),
|
||||
|
||||
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
|
||||
CA_ORIGIN = CA_PROD,
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -54,14 +52,23 @@ AcmeError.FORBIDDEN = 'Forbidden';
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
|
||||
function getNonce(callback) {
|
||||
superagent.get(CA_ORIGIN + '/directory', function (error, response) {
|
||||
function Acme(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
|
||||
this.accountKeyPem = null; // Buffer
|
||||
|
||||
this.chainPem = options.prod ? safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt') : new Buffer('');
|
||||
}
|
||||
|
||||
Acme.prototype.getNonce = function (callback) {
|
||||
superagent.get(this.caOrigin + '/directory', function (error, response) {
|
||||
if (error) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
|
||||
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
function urlBase64Encode(string) {
|
||||
@@ -73,13 +80,13 @@ function b64(str) {
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function sendSignedRequest(url, accountKeyPem, payload, callback) {
|
||||
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var privateKey = ursa.createPrivateKey(accountKeyPem);
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
|
||||
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
@@ -92,7 +99,7 @@ function sendSignedRequest(url, accountKeyPem, payload, callback) {
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
getNonce(function (error, nonce) {
|
||||
this.getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
|
||||
@@ -116,10 +123,9 @@ function sendSignedRequest(url, accountKeyPem, payload, callback) {
|
||||
callback(null, res);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function registerUser(accountKeyPem, email, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
Acme.prototype.registerUser = function (email, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -131,7 +137,7 @@ function registerUser(accountKeyPem, email, callback) {
|
||||
|
||||
debug('registerUser: %s', email);
|
||||
|
||||
sendSignedRequest(CA_ORIGIN + '/acme/new-reg', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return callback(new AcmeError(AcmeError.ALREADY_EXISTS, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
@@ -140,10 +146,9 @@ function registerUser(accountKeyPem, email, callback) {
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function registerDomain(accountKeyPem, domain, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
Acme.prototype.registerDomain = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -157,7 +162,7 @@ function registerDomain(accountKeyPem, domain, callback) {
|
||||
|
||||
debug('registerDomain: %s', domain);
|
||||
|
||||
sendSignedRequest(CA_ORIGIN + '/acme/new-authz', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
@@ -166,10 +171,9 @@ function registerDomain(accountKeyPem, domain, callback) {
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function prepareHttpChallenge(accountKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -177,7 +181,8 @@ function prepareHttpChallenge(accountKeyPem, challenge, callback) {
|
||||
|
||||
var token = challenge.token;
|
||||
|
||||
var privateKey = ursa.createPrivateKey(accountKeyPem);
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
|
||||
|
||||
var jwk = {
|
||||
e: b64(privateKey.getExponent()),
|
||||
@@ -197,10 +202,9 @@ function prepareHttpChallenge(accountKeyPem, challenge, callback) {
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function notifyChallengeReady(accountKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
Acme.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -213,15 +217,15 @@ function notifyChallengeReady(accountKeyPem, challenge, callback) {
|
||||
keyAuthorization: keyAuthorization
|
||||
};
|
||||
|
||||
sendSignedRequest(challenge.uri, accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function waitForChallenge(challenge, callback) {
|
||||
Acme.prototype.waitForChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -250,11 +254,10 @@ function waitForChallenge(challenge, callback) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
function signCertificate(accountKeyPem, domain, csrDer, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
Acme.prototype.signCertificate = function (domain, csrDer, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(util.isBuffer(csrDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -268,7 +271,7 @@ function signCertificate(accountKeyPem, domain, csrDer, callback) {
|
||||
|
||||
debug('signCertificate: sending new-cert request');
|
||||
|
||||
sendSignedRequest(CA_ORIGIN + '/acme/new-cert', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
@@ -281,9 +284,9 @@ function signCertificate(accountKeyPem, domain, csrDer, callback) {
|
||||
|
||||
return callback(null, result.headers.location);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function createKeyAndCsr(domain, callback) {
|
||||
Acme.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -305,14 +308,15 @@ function createKeyAndCsr(domain, callback) {
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
callback(null, csrDer);
|
||||
}
|
||||
};
|
||||
|
||||
function downloadCertificate(domain, certUrl, callback) {
|
||||
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var that = this;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
@@ -332,20 +336,17 @@ function downloadCertificate(domain, certUrl, callback) {
|
||||
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
|
||||
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var chainPem = safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt');
|
||||
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var certificateFile = path.join(outdir, domain + '.cert');
|
||||
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
|
||||
var fullChainPem = Buffer.concat([certificatePem, that.chainPem]);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file saved at %s', certificateFile);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function acmeFlow(domain, callback) {
|
||||
Acme.prototype.acmeFlow = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -354,23 +355,23 @@ function acmeFlow(domain, callback) {
|
||||
// we cannot use owner email because we don't have it yet (the admin cert is fetched before activation)
|
||||
// one option is to update the owner email when a second cert is requested (https://github.com/ietf-wg-acme/acme/issues/30)
|
||||
var email = 'admin@cloudron.io';
|
||||
var accountKeyPem;
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, accountKeyPem);
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
debug('getCertificate: using existing acme account key');
|
||||
accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
}
|
||||
|
||||
registerUser(accountKeyPem, email, function (error) {
|
||||
var that = this;
|
||||
that.registerUser(email, function (error) {
|
||||
if (error && error.reason !== AcmeError.ALREADY_EXISTS) return callback(error);
|
||||
|
||||
registerDomain(accountKeyPem, domain, function (error, result) {
|
||||
that.registerDomain(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('acmeFlow: challenges: %j', result);
|
||||
@@ -380,30 +381,31 @@ function acmeFlow(domain, callback) {
|
||||
var challenge = httpChallenges[0];
|
||||
|
||||
async.waterfall([
|
||||
prepareHttpChallenge.bind(null, accountKeyPem, challenge),
|
||||
notifyChallengeReady.bind(null, accountKeyPem, challenge),
|
||||
waitForChallenge.bind(null, challenge),
|
||||
createKeyAndCsr.bind(null, domain),
|
||||
signCertificate.bind(null, accountKeyPem, domain),
|
||||
downloadCertificate.bind(null, domain)
|
||||
that.prepareHttpChallenge.bind(that, challenge),
|
||||
that.notifyChallengeReady.bind(that, challenge),
|
||||
that.waitForChallenge.bind(that, challenge),
|
||||
that.createKeyAndCsr.bind(that, domain),
|
||||
that.signCertificate.bind(that, domain),
|
||||
that.downloadCertificate.bind(that, domain)
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getCertificate(domain, callback) {
|
||||
Acme.prototype.getCertificate = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var certUrl = safe.fs.readFileSync(path.join(outdir, domain + '.url'), 'utf8');
|
||||
var certificateGetter;
|
||||
|
||||
if (certUrl) {
|
||||
debug('getCertificate: renewing existing cert for %s from %s', domain, certUrl);
|
||||
certificateGetter = downloadCertificate.bind(null, domain, certUrl);
|
||||
certificateGetter = this.downloadCertificate.bind(this, domain, certUrl);
|
||||
} else {
|
||||
debug('getCertificate: start acme flow for %s', domain);
|
||||
certificateGetter = acmeFlow.bind(null, domain);
|
||||
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
|
||||
certificateGetter = this.acmeFlow.bind(this, domain);
|
||||
}
|
||||
|
||||
certificateGetter(function (error) {
|
||||
@@ -411,4 +413,13 @@ function getCertificate(domain, callback) {
|
||||
|
||||
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
|
||||
});
|
||||
};
|
||||
|
||||
function getCertificate(domain, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme(options || { });
|
||||
acme.getCertificate(domain, callback);
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,8 +7,9 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/caas.js');
|
||||
|
||||
function getCertificate(domain, callback) {
|
||||
function getCertificate(domain, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', domain);
|
||||
|
||||
+8
-5
@@ -61,7 +61,10 @@ function getApi(callback) {
|
||||
|
||||
var api = tlsConfig.provider === 'caas' ? caas : acme;
|
||||
|
||||
callback(null, api);
|
||||
var options = { };
|
||||
options.prod = tlsConfig.provider.match(/.*-prod/ !== null);
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,14 +113,14 @@ function autoRenew(callback) {
|
||||
|
||||
debug('autoRenew: %j needs to be renewed', certs);
|
||||
|
||||
getApi(function (error, api) {
|
||||
getApi(function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(certs, function iterator(cert, iteratorCallback) {
|
||||
var domain = cert.match(/^(.*)\.cert$/)[1];
|
||||
if (domain === 'host') return iteratorCallback(); // cannot renew fallback cert
|
||||
|
||||
api.getCertificate(domain, function (error) {
|
||||
api.getCertificate(domain, apiOptions, function (error) {
|
||||
if (error) debug('autoRenew: could not renew cert for %s', domain, error);
|
||||
|
||||
iteratorCallback(); // move on to next cert
|
||||
@@ -224,12 +227,12 @@ function ensureCertificate(domain, callback) {
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
}
|
||||
|
||||
getApi(function (error, api) {
|
||||
getApi(function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s', domain);
|
||||
|
||||
api.getCertificate(domain, function (error, certFilePath, keyFilePath) {
|
||||
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs
|
||||
|
||||
+1
-1
@@ -179,7 +179,7 @@ function setTimeZone(ip, callback) {
|
||||
|
||||
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
|
||||
if ((error && !error.response) || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location', error);
|
||||
debug('Failed to get geo location: %s', error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -176,19 +176,19 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('delSubdomain: resource record set not found.', error);
|
||||
debug('del: resource record set not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('delSubdomain: hosted zone not found.', error);
|
||||
debug('del: hosted zone not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('delSubdomain: resource is still busy', error);
|
||||
debug('del: resource is still busy', error);
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('delSubdomain: invalid change batch. No such record to be deleted.');
|
||||
debug('del: invalid change batch. No such record to be deleted.');
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error) {
|
||||
debug('delSubdomain: error', error);
|
||||
debug('del: error', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user