This commit is contained in:
AJ ONeal 2015-12-16 00:13:07 +00:00
parent aec8958ca8
commit 4015e792dd
3 changed files with 222 additions and 218 deletions

View File

@ -23,19 +23,21 @@ leCore.
## API ## API
``` ```javascript
LeCore.registerNewAccount(); LeCore.registerNewAccount(options, cb);
LeCore.getCertificate(); LeCore.getCertificate(options, cb);
LeCore.Acme // Signs requests with JWK LeCore.Acme // Signs requests with JWK
acme = new Acme(lePrivateKey) // privateKey format is abstract acme = new Acme(lePrivateKey) // privateKey format is abstract
acme.post(url, body, cb) // POST with signature acme.post(url, body, cb) // POST with signature
acme.parseLinks(link) // (internal) parses 'link' header acme.parseLinks(link) // (internal) parses 'link' header
acme.getNonce(url, cb) // (internal) HEAD request to get 'replay-nonce' strings acme.getNonce(url, cb) // (internal) HEAD request to get 'replay-nonce' strings
LeCore.leCrypto LeCore.leCrypto
generateSignature(lePrivateKey, nodeBufferBody, nonceString) thumbprint(lePubKey) // generates thumbprint
generateSignature(lePrivKey, bodyBuf, nonce) // generates a signature
importPemPrivateKey(privateKeyPem); // returns abstract private key
``` ```
For testing and development, you can also inject the dependencies you want to use: For testing and development, you can also inject the dependencies you want to use:

View File

@ -4,243 +4,245 @@
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js * Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
* MPL 2.0 * MPL 2.0
*/ */
'use strict'; 'use strict';
var NOOP=function () {}, log=NOOP; module.exports.create = function (deps) {
var request=require('request'); var NOOP=function () {}, log=NOOP;
var util=require('./acme-util'); var request=require('request');
var cryptoUtil=require('./crypto-util'); var util=require('./acme-util');
var Acme = require('./acme-client'); var importPemPrivateKey = deps.leCrypto.importPemPrivateKey;
var thumbprinter = deps.leCrypto.thumbprint;
var generateCsr = deps.leCrypto.generateCsr || deps.leCrypto.generateCSR;
var Acme = deps.Acme;
function getCert(options, cb) { function getCert(options, cb) {
var state={ var state={
validatedDomains:[] validatedDomains:[]
, validAuthorizationUrls:[] , validAuthorizationUrls:[]
, newAuthorizationUrl: options.newAuthorizationUrl || options.newAuthz , newAuthorizationUrl: options.newAuthorizationUrl || options.newAuthz
, newCertificateUrl: options.newCertificateUrl || options.newCert , newCertificateUrl: options.newCertificateUrl || options.newCert
}; };
if (!options.accountPrivateKeyPem) { if (!options.accountPrivateKeyPem) {
return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem")); return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem"));
} }
if (!options.domainPrivateKeyPem) { if (!options.domainPrivateKeyPem) {
return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem")); return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem"));
} }
if (!options.setChallenge) { if (!options.setChallenge) {
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}")); return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
} }
if (!options.removeChallenge) { if (!options.removeChallenge) {
return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}")); return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}"));
} }
if (!(options.domains && options.domains.length)) { 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']")); 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.accountKeyPem=options.accountPrivateKeyPem;
state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPem);
state.acme=new Acme(state.accountKeyPair);
state.certPrivateKeyPem=options.domainPrivateKeyPem;
state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPem);
} catch(err) {
return handleErr(err, 'Failed to parse privateKey');
}
nextDomain();
function nextDomain() {
if (state.domains.length > 0) {
getChallenges(state.domains.shift());
return;
} else {
getCertificate();
} }
}
function getChallenges(domain) { state.domains = options.domains.slice(0); // copy array
state.domain=domain; try {
state.accountKeyPem=options.accountPrivateKeyPem;
state.accountKeyPair=importPemPrivateKey(state.accountKeyPem);
state.acme=new Acme(state.accountKeyPair);
state.certPrivateKeyPem=options.domainPrivateKeyPem;
state.certPrivateKey=importPemPrivateKey(state.certPrivateKeyPem);
} catch(err) {
return handleErr(err, 'Failed to parse privateKey');
}
state.acme.post(state.newAuthorizationUrl, { nextDomain();
resource:'new-authz',
identifier:{ function nextDomain() {
type:'dns', if (state.domains.length > 0) {
value:state.domain, getChallenges(state.domains.shift());
return;
} else {
getCertificate();
} }
}, getReadyToValidate);
}
function getReadyToValidate(err, res, body) {
var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath;
if (err) {
return handleErr(err);
} }
if (Math.floor(res.statusCode/100)!==2) { function getChallenges(domain) {
return handleErr(null, 'Authorization request failed ('+res.statusCode+')'); state.domain=domain;
state.acme.post(state.newAuthorizationUrl, {
resource:'new-authz',
identifier:{
type:'dns',
value:state.domain,
}
}, getReadyToValidate);
} }
links=Acme.parseLink(res.headers.link); function getReadyToValidate(err, res, body) {
if (!links || !('next' in links)) { var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath;
return handleErr(err, 'Server didn\'t provide information to proceed (2)');
}
state.authorizationUrl=res.headers.location; if (err) {
state.newCertificateUrl=links.next; return handleErr(err);
}
authz=JSON.parse(body); if (Math.floor(res.statusCode/100)!==2) {
return handleErr(null, 'Authorization request failed ('+res.statusCode+')');
}
httpChallenges=authz.challenges.filter(function(x) { links=Acme.parseLink(res.headers.link);
return x.type==='http-01'; if (!links || !('next' in links)) {
}); return handleErr(err, 'Server didn\'t provide information to proceed (2)');
if (httpChallenges.length===0) { }
return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
}
challenge=httpChallenges[0];
thumbprint=cryptoUtil.thumbprint(state.accountKeyPair.publicKey); state.authorizationUrl=res.headers.location;
keyAuthorization=challenge.token+'.'+thumbprint; state.newCertificateUrl=links.next;
state.responseUrl=challenge.uri;
options.setChallenge(state.domain, challenge.token, keyAuthorization, challengeDone); authz=JSON.parse(body);
function challengeDone() { httpChallenges=authz.challenges.filter(function(x) {
state.acme.post(state.responseUrl, { return x.type==='http-01';
resource:'challenge', });
keyAuthorization:keyAuthorization if (httpChallenges.length===0) {
}, function(err, res, body) { return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
ensureValidation(err, res, body, function unlink() { }
options.removeChallenge(state.domain, challenge.token, function () { challenge=httpChallenges[0];
// ignore
thumbprint=thumbprinter(state.accountKeyPair.publicKey);
keyAuthorization=challenge.token+'.'+thumbprint;
state.responseUrl=challenge.uri;
options.setChallenge(state.domain, challenge.token, keyAuthorization, challengeDone);
function challengeDone() {
state.acme.post(state.responseUrl, {
resource:'challenge',
keyAuthorization:keyAuthorization
}, function(err, res, body) {
ensureValidation(err, res, body, function unlink() {
options.removeChallenge(state.domain, challenge.token, function () {
// ignore
});
}); });
}); });
}
}
function ensureValidation(err, res, body, unlink) {
var authz;
if (err || Math.floor(res.statusCode/100)!==2) {
unlink();
return handleErr(err, 'Authorization status request failed ('+res.statusCode+')');
}
authz=JSON.parse(body);
if (authz.status==='pending') {
setTimeout(function() {
request.get(state.authorizationUrl, {}, function(err, res, body) {
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();
return handleErr(null, 'The CA was unable to validate the file you provisioned', body);
} else {
unlink();
return handleErr(null, 'CA returned an authorization in an unexpected state', authz);
}
}
function getCertificate() {
var csr=generateCsr(state.certPrivateKey, state.validatedDomains);
log('Requesting certificate...');
state.acme.post(state.newCertificateUrl, {
resource:'new-cert',
csr:csr,
authorizations:state.validAuthorizationUrls
}, downloadCertificate);
}
function downloadCertificate(err, res, body) {
var links, certUrl;
if (err || Math.floor(res.statusCode/100)!==2) {
log('Certificate request failed with error ', err);
if (body) {
log(body.toString());
}
return handleErr(err, 'Certificate request failed');
}
links=Acme.parseLink(res.headers.link);
if (!links || !('up' in links)) {
return handleErr(err, 'Failed to fetch issuer certificate');
}
log('Requesting certificate: done');
state.certificate=body;
certUrl=res.headers.location;
request.get({
url:certUrl,
encoding:null
}, function(err, res, body) {
if (err) {
return handleErr(err, 'Failed to fetch cert from '+certUrl);
}
if (res.statusCode!==200) {
return handleErr(err, 'Failed to fetch cert from '+certUrl, res.body.toString());
}
if (body.toString()!==state.certificate.toString()) {
handleErr(null, 'Cert at '+certUrl+' did not match returned cert');
} else {
log('Successfully verified cert at '+certUrl);
log('Requesting issuer certificate...');
request.get({
url:links.up,
encoding:null
}, function(err, res, body) {
if (err || res.statusCode!==200) {
return handleErr(err, 'Failed to fetch issuer certificate');
}
state.caCert=certBufferToPem(body);
log('Requesting issuer certificate: done');
done();
});
}
}); });
} }
}
function ensureValidation(err, res, body, unlink) { function done() {
var authz; var cert;
if (err || Math.floor(res.statusCode/100)!==2) { try {
unlink(); cert=certBufferToPem(state.certificate);
return handleErr(err, 'Authorization status request failed ('+res.statusCode+')'); } catch(e) {
} console.error(e.stack);
//cb(new Error("Could not write output files. Please check permissions!"));
authz=JSON.parse(body); handleErr(e, 'Could not write output files. Please check permissions!');
return;
if (authz.status==='pending') {
setTimeout(function() {
request.get(state.authorizationUrl, {}, function(err, res, body) {
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();
return handleErr(null, 'The CA was unable to validate the file you provisioned', body);
} else {
unlink();
return handleErr(null, 'CA returned an authorization in an unexpected state', authz);
}
}
function getCertificate() {
var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains);
log('Requesting certificate...');
state.acme.post(state.newCertificateUrl, {
resource:'new-cert',
csr:csr,
authorizations:state.validAuthorizationUrls
}, downloadCertificate);
}
function downloadCertificate(err, res, body) {
var links, certUrl;
if (err || Math.floor(res.statusCode/100)!==2) {
log('Certificate request failed with error ', err);
if (body) {
log(body.toString());
} }
return handleErr(err, 'Certificate request failed');
cb(null, {
cert: cert
, key: state.certPrivateKeyPem
, ca: state.caCert
});
} }
links=Acme.parseLink(res.headers.link); function handleErr(err, text, info) {
if (!links || !('up' in links)) { log(text, err, info);
return handleErr(err, 'Failed to fetch issuer certificate'); cb(err || new Error(text));
} }
log('Requesting certificate: done');
state.certificate=body;
certUrl=res.headers.location;
request.get({
url:certUrl,
encoding:null
}, function(err, res, body) {
if (err) {
return handleErr(err, 'Failed to fetch cert from '+certUrl);
}
if (res.statusCode!==200) {
return handleErr(err, 'Failed to fetch cert from '+certUrl, res.body.toString());
}
if (body.toString()!==state.certificate.toString()) {
handleErr(null, 'Cert at '+certUrl+' did not match returned cert');
} else {
log('Successfully verified cert at '+certUrl);
log('Requesting issuer certificate...');
request.get({
url:links.up,
encoding:null
}, function(err, res, body) {
if (err || res.statusCode!==200) {
return handleErr(err, 'Failed to fetch issuer certificate');
}
state.caCert=certBufferToPem(body);
log('Requesting issuer certificate: done');
done();
});
}
});
} }
function done() { function certBufferToPem(cert) {
var cert; cert=util.toStandardB64(cert.toString('base64'));
cert=cert.match(/.{1,64}/g).join('\n');
try { return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----';
cert=certBufferToPem(state.certificate);
} catch(e) {
console.error(e.stack);
//cb(new Error("Could not write output files. Please check permissions!"));
handleErr(e, 'Could not write output files. Please check permissions!');
return;
}
cb(null, {
cert: cert
, key: state.certPrivateKeyPem
, ca: state.caCert
});
} }
function handleErr(err, text, info) { return getCert;
log(text, err, info); };
cb(err || new Error(text));
}
}
function certBufferToPem(cert) {
cert=util.toStandardB64(cert.toString('base64'));
cert=cert.match(/.{1,64}/g).join('\n');
return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----';
}
module.exports = getCert;

View File

@ -9,7 +9,7 @@ function create(deps) {
var LeCore = {}; var LeCore = {};
LeCore.leCrypto = deps.leCrypto; LeCore.leCrypto = deps.leCrypto;
LeCore.Acme = require('./lib/acme-client').create(deps); deps.Acme = LeCore.Acme = require('./lib/acme-client').create(deps);
LeCore.registerNewAccount = require('./lib/register-new-account').create(deps); LeCore.registerNewAccount = require('./lib/register-new-account').create(deps);
LeCore.getCertificate = require('./lib/get-certificate').create(deps); LeCore.getCertificate = require('./lib/get-certificate').create(deps);