le-acme-core.js/lib/get-certificate.js

413 lines
12 KiB
JavaScript
Raw Normal View History

2015-12-15 14:33:53 +00:00
/*!
* letiny
* Copyright(c) 2015 Anatol Sommer <anatol@anatol.at>
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
* MPL 2.0
*/
'use strict';
2016-08-01 18:10:37 +00:00
function _toStandardBase64(str) {
var b64 = str.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
switch (b64.length % 4) {
case 2: b64 += "=="; break;
case 3: b64 += "="; break;
}
return b64;
}
2016-08-09 19:58:11 +00:00
function certBufferToPem(cert) {
cert = _toStandardBase64(cert.toString('base64'));
cert = cert.match(/.{1,64}/g).join('\r\n');
return '-----BEGIN CERTIFICATE-----\r\n'+cert+'\r\n-----END CERTIFICATE-----\r\n';
}
2015-12-16 00:13:07 +00:00
module.exports.create = function (deps) {
2016-08-09 19:58:11 +00:00
var request = deps.request;
2015-12-16 00:13:07 +00:00
var Acme = deps.Acme;
2016-08-01 09:53:50 +00:00
var RSA = deps.RSA;
2015-12-16 00:13:07 +00:00
2016-08-02 01:47:48 +00:00
// getCertificate // returns "pems", meaning "certs"
2015-12-16 00:13:07 +00:00
function getCert(options, cb) {
2015-12-15 14:33:53 +00:00
2015-12-19 22:25:11 +00:00
function bodyToError(res, body) {
var err;
if (!body) {
err = new Error("[Error] letiny-core: no request body");
err.code = "E_NO_RESPONSE_BODY";
throw err;
}
if ('{' === body[0] || '{' === String.fromCharCode(body[0])) {
try {
body = JSON.parse(body.toString('utf8'));
2015-12-19 22:25:11 +00:00
} catch(e) {
err = new Error("[Error] letiny-core: body could not be parsed");
err.code = "E_BODY_PARSE";
err.description = body;
throw err;
}
}
if (Math.floor(res.statusCode / 100) !== 2) {
err = new Error("[Error] letiny-core: not 200 ok");
err.code = "E_STATUS_CODE";
2016-08-01 09:53:50 +00:00
err.type = body.type;
2015-12-19 22:25:11 +00:00
err.description = body;
err.detail = body.detail;
2016-02-16 17:08:12 +00:00
console.error("TODO: modules which depend on this module should expose this error properly but since some of them don't, I expose it here directly:");
console.error(err.stack);
console.error(body);
2015-12-19 22:25:11 +00:00
throw err;
}
if (body.type && body.detail) {
err = new Error("[Error] letiny-core: " + body.detail);
err.code = body.type;
err.type = body.type;
err.description = body.detail;
err.detail = body.detail;
throw err;
}
return body;
}
2015-12-16 00:13:07 +00:00
function nextDomain() {
if (state.domains.length > 0) {
getChallenges(state.domains.shift());
return;
} else {
getCertificate();
2015-12-15 14:33:53 +00:00
}
2015-12-15 15:05:20 +00:00
}
2015-12-16 00:13:07 +00:00
function getChallenges(domain) {
state.domain = domain;
2015-12-15 14:33:53 +00:00
2015-12-16 03:23:34 +00:00
state.acme.post(state.newAuthzUrl, {
resource: 'new-authz',
identifier: {
type: 'dns',
value: state.domain,
2015-12-16 00:13:07 +00:00
}
}, function (err, res, body) {
if (!err && res.body) {
try {
body = bodyToError(res, body);
} catch(e) {
err = e;
}
}
2016-08-01 09:53:50 +00:00
getReadyToValidate(err, res, body);
});
2015-12-15 14:33:53 +00:00
}
2015-12-16 00:13:07 +00:00
function getReadyToValidate(err, res, body) {
var links;
var authz;
var httpChallenges;
var challenge;
var thumbprint;
var keyAuthorization;
2015-12-15 14:33:53 +00:00
2016-02-11 20:22:41 +00:00
function challengeDone(err) {
if (err) {
console.error('[letiny-core] setChallenge Error:');
console.error(err && err.stack || err);
ensureValidation(err, null, null, function () {
options.removeChallenge(state.domain, challenge.token, function () {
// ignore
});
});
return;
}
2015-12-16 00:13:07 +00:00
state.acme.post(state.responseUrl, {
resource: 'challenge',
keyAuthorization: keyAuthorization
2015-12-16 00:13:07 +00:00
}, function(err, res, body) {
if (!err && res.body) {
try {
body = bodyToError(res, body);
} catch(e) {
err = e;
}
}
2015-12-16 00:13:07 +00:00
ensureValidation(err, res, body, function unlink() {
options.removeChallenge(state.domain, challenge.token, function () {
// ignore
});
});
2015-12-15 14:33:53 +00:00
});
2015-12-16 00:13:07 +00:00
}
2016-08-01 09:53:50 +00:00
if (err) {
return handleErr(err);
}
if (Math.floor(res.statusCode/100)!==2) {
return handleErr(null, 'Authorization request failed ('+res.statusCode+')');
}
links = Acme.parseLink(res.headers.link);
2016-08-01 09:53:50 +00:00
if (!links || !('next' in links)) {
return handleErr(err, 'Server didn\'t provide information to proceed (2)');
}
state.authorizationUrl = res.headers.location;
state.newCertUrl = links.next;
2016-08-01 09:53:50 +00:00
authz = body;
2016-08-01 09:53:50 +00:00
httpChallenges = authz.challenges.filter(function(x) {
return x.type === options.challengeType;
2016-08-01 09:53:50 +00:00
});
if (httpChallenges.length === 0) {
2016-08-01 09:53:50 +00:00
return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
}
challenge = httpChallenges[0];
2016-08-01 09:53:50 +00:00
thumbprint = RSA.thumbprint(state.accountKeypair);
keyAuthorization = challenge.token + '.' + thumbprint;
2016-08-01 09:53:50 +00:00
state.responseUrl = challenge.uri;
2016-08-01 09:53:50 +00:00
options.setChallenge(state.domain, challenge.token, keyAuthorization, challengeDone);
2015-12-15 14:33:53 +00:00
}
2015-12-16 00:13:07 +00:00
function ensureValidation(err, res, body, unlink) {
2016-02-15 14:06:41 +00:00
var authz, challengesState;
2015-12-15 14:33:53 +00:00
2015-12-16 00:13:07 +00:00
if (err || Math.floor(res.statusCode/100)!==2) {
unlink();
2016-02-11 20:22:41 +00:00
return handleErr(err, 'Authorization status request failed ('
+ (res && res.statusCode || err.code || err.message || err) + ')');
2015-12-16 00:13:07 +00:00
}
2015-12-19 22:25:11 +00:00
authz=body;
2015-12-15 14:33:53 +00:00
2015-12-16 00:13:07 +00:00
if (authz.status==='pending') {
setTimeout(function() {
2015-12-19 22:25:11 +00:00
request({
method: 'GET'
, url: state.authorizationUrl
}, function(err, res, body) {
if (!err && res.body) {
try {
body = bodyToError(res, body);
} catch(e) {
err = e;
}
}
2015-12-16 00:13:07 +00:00
ensureValidation(err, res, body, unlink);
});
}, 1000);
} else if (authz.status==='valid') {
log('Validating domain ... done');
state.validatedDomains.push(state.domain);
state.validAuthorizationUrls.push(state.authorizationUrl);
unlink();
nextDomain();
} else if (authz.status==='invalid') {
unlink();
2016-02-15 16:41:02 +00:00
challengesState = (authz.challenges || []).map(function (challenge) {
2016-02-15 14:06:41 +00:00
var result = ' - ' + challenge.uri + ' [' + challenge.status + ']';
2016-02-15 16:30:05 +00:00
if (challenge.error) {
result += '\n ' + challenge.error.detail;
2016-02-15 14:06:41 +00:00
}
return result;
}).join('\n');
return handleErr(null,
'The CA was unable to validate the file you provisioned. '
+ (authz.detail ? 'Details: ' + authz.detail : '')
+ (challengesState ? '\n' + challengesState : ''), body);
2015-12-16 00:13:07 +00:00
} else {
unlink();
return handleErr(null, 'CA returned an authorization in an unexpected state' + authz.detail, authz);
2015-12-15 14:33:53 +00:00
}
}
2015-12-16 00:13:07 +00:00
function getCertificate() {
var csr=RSA.generateCsrWeb64(state.certKeypair, state.validatedDomains);
2015-12-16 00:13:07 +00:00
log('Requesting certificate...');
2015-12-16 03:23:34 +00:00
state.acme.post(state.newCertUrl, {
2015-12-16 00:13:07 +00:00
resource:'new-cert',
csr:csr,
authorizations:state.validAuthorizationUrls
}, function (err, res, body ) {
if (!err && res.body) {
try {
body = bodyToError(res, body);
} catch(e) {
err = e;
}
}
downloadCertificate(err, res, body);
});
2015-12-15 14:33:53 +00:00
}
2015-12-16 00:13:07 +00:00
function downloadCertificate(err, res, body) {
var links, certUrl;
2015-12-15 14:33:53 +00:00
2015-12-19 22:25:11 +00:00
if (err) {
handleErr(err, 'Certificate request failed');
return;
}
if (Math.floor(res.statusCode/100)!==2) {
err = new Error("invalid status code: " + res.statusCode);
err.code = "E_STATUS_CODE";
err.description = body;
handleErr(err);
return;
2015-12-15 14:33:53 +00:00
}
2015-12-16 00:13:07 +00:00
links=Acme.parseLink(res.headers.link);
if (!links || !('up' in links)) {
return handleErr(err, 'Failed to fetch issuer certificate');
2015-12-15 14:33:53 +00:00
}
2015-12-16 00:13:07 +00:00
log('Requesting certificate: done');
2015-12-15 14:33:53 +00:00
2015-12-16 00:13:07 +00:00
state.certificate=body;
certUrl=res.headers.location;
2015-12-19 22:25:11 +00:00
request({
method: 'GET'
, url: certUrl
, encoding: null
2015-12-16 00:13:07 +00:00
}, function(err, res, body) {
2015-12-19 22:25:11 +00:00
if (!err) {
try {
body = bodyToError(res, body);
} catch(e) {
err = e;
}
}
2015-12-16 00:13:07 +00:00
if (err) {
return handleErr(err, 'Failed to fetch cert from '+certUrl);
}
2015-12-19 22:25:11 +00:00
2015-12-16 00:13:07 +00:00
if (res.statusCode!==200) {
return handleErr(err, 'Failed to fetch cert from '+certUrl, res.body.toString());
}
2015-12-19 22:25:11 +00:00
2015-12-16 00:13:07 +00:00
if (body.toString()!==state.certificate.toString()) {
2015-12-19 22:25:11 +00:00
return handleErr(null, 'Cert at '+certUrl+' did not match returned cert');
2015-12-16 00:13:07 +00:00
}
2015-12-19 22:25:11 +00:00
log('Successfully verified cert at '+certUrl);
downloadIssuerCert(links);
});
}
2015-12-19 22:25:11 +00:00
function downloadIssuerCert(links) {
log('Requesting issuer certificate...');
request({
method: 'GET'
, url: links.up
, encoding: null
}, function(err, res, body) {
if (!err) {
try {
body = bodyToError(res, body);
} catch(e) {
err = e;
2015-12-19 22:25:11 +00:00
}
}
2015-12-19 22:25:11 +00:00
if (err || res.statusCode!==200) {
return handleErr(err, 'Failed to fetch issuer certificate');
}
2016-08-09 19:58:11 +00:00
state.chainPem = certBufferToPem(body);
log('Requesting issuer certificate: done');
done();
2015-12-16 00:13:07 +00:00
});
2015-12-15 14:33:53 +00:00
}
2015-12-16 00:13:07 +00:00
function done() {
var privkeyPem = RSA.exportPrivatePem(state.certKeypair);
2015-12-16 00:13:07 +00:00
cb(null, {
cert: certBufferToPem(state.certificate)
2016-08-02 01:16:25 +00:00
, privkey: privkeyPem
, chain: state.chainPem
// TODO nix backwards compat
2016-08-02 01:19:17 +00:00
, key: privkeyPem
, ca: state.chainPem
2015-12-16 00:13:07 +00:00
});
}
2015-12-15 14:33:53 +00:00
2015-12-16 00:13:07 +00:00
function handleErr(err, text, info) {
log(text, err, info);
cb(err || new Error(text));
}
2016-08-01 09:53:50 +00:00
var NOOP = function () {};
var log = options.debug ? console.log : NOOP;
var state={
validatedDomains:[]
, validAuthorizationUrls:[]
, newAuthzUrl: options.newAuthzUrl
, newCertUrl: options.newCertUrl
};
if (!options.challengeType) {
options.challengeType = 'http-01';
}
if (-1 === [ 'http-01', 'tls-sni-01', 'dns-01' ].indexOf(options.challengeType)) {
return handleErr(new Error("options.challengeType '" + options.challengeType + "' is not yet supported"));
}
2016-08-01 09:53:50 +00:00
if (!options.newAuthzUrl) {
return handleErr(new Error("options.newAuthzUrl must be the authorization url"));
}
if (!options.newCertUrl) {
return handleErr(new Error("options.newCertUrl must be the new certificate url"));
}
if (!options.accountKeypair) {
2016-08-02 00:07:54 +00:00
if (!options.accountPrivateKeyPem) {
return handleErr(new Error("options.accountKeypair must be an object with `privateKeyPem` and/or `privateKeyJwk`"));
}
console.warn("'accountPrivateKeyPem' is deprecated. Use options.accountKeypair.privateKeyPem instead.");
options.accountKeypair = RSA.import({ privateKeyPem: options.accountPrivateKeyPem });
2016-08-01 09:53:50 +00:00
}
if (!options.domainKeypair) {
2016-08-02 00:07:54 +00:00
if (!options.domainPrivateKeyPem) {
return handleErr(new Error("options.domainKeypair must be an object with `privateKeyPem` and/or `privateKeyJwk`"));
}
console.warn("'domainPrivateKeyPem' is deprecated. Use options.domainKeypair.privateKeyPem instead.");
options.domainKeypair = RSA.import({ privateKeyPem: options.domainPrivateKeyPem });
2016-08-01 09:53:50 +00:00
}
if (!options.setChallenge) {
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
}
if (!options.removeChallenge) {
return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}"));
}
if (!(options.domains && options.domains.length)) {
return handleErr(new Error("options.domains must be an array of domains such as ['example.com', 'www.example.com']"));
}
state.domains = options.domains.slice(0); // copy array
try {
state.accountKeypair = options.accountKeypair;
state.certKeypair = options.domainKeypair;
state.acme = new Acme(state.accountKeypair);
2016-08-01 09:53:50 +00:00
} catch(err) {
return handleErr(err, 'Failed to parse privateKey');
}
nextDomain();
2015-12-16 00:13:07 +00:00
}
2015-12-15 14:33:53 +00:00
2015-12-16 00:13:07 +00:00
return getCert;
};