/*! * letiny * Copyright(c) 2015 Anatol Sommer * Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js * MPL 2.0 */ 'use strict'; var NOOP=function () {}, log=NOOP; var request=require('request'); var util=require('./acme-util'); var cryptoUtil=require('./crypto-util'); var Acme = require('./acme-client'); function getCert(options, cb) { var state={ validatedDomains:[], validAuthorizationURLs:[] }; if (!options.accountPrivateKeyPem) { return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem")); } if (!options.domainPrivateKeyPem) { return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem")); } 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.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.domain=domain; state.acme.post(state.newAuthorizationURL, { resource:'new-authz', identifier:{ type:'dns', value:state.domain, } }, getReadyToValidate); } function getReadyToValidate(err, res, body) { var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath; if (err || Math.floor(res.statusCode/100)!==2) { return handleErr(err, 'Authorization request failed ('+res.statusCode+')'); } links=Acme.parseLink(res.headers.link); if (!links || !('next' in links)) { return handleErr(err, 'Server didn\'t provide information to proceed (2)'); } state.authorizationURL=res.headers.location; state.newCertificateURL=links.next; authz=JSON.parse(body); httpChallenges=authz.challenges.filter(function(x) { return x.type==='http-01'; }); 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); keyAuthorization=challenge.token+'.'+thumbprint; challengePath='.well-known/acme-challenge/'+challenge.token; state.responseURL=challenge.uri; state.path=challengePath; options.setChallenge(state.domain, '/'+challengePath, 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, '/'+challengePath, 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=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'); } 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 done() { var cert; try { 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) { 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;