| 
									
										
										
										
											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'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  | module.exports.create = function (deps) { | 
					
						
							|  |  |  |   var NOOP=function () {}, log=NOOP; | 
					
						
							|  |  |  |   var request=require('request'); | 
					
						
							| 
									
										
										
										
											2015-12-16 01:18:40 +00:00
										 |  |  |   var toStandardB64 = deps.leUtils.toStandardB64; | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |   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) { | 
					
						
							|  |  |  |     var state={ | 
					
						
							|  |  |  |       validatedDomains:[] | 
					
						
							|  |  |  |     , validAuthorizationUrls:[] | 
					
						
							|  |  |  |     , newAuthorizationUrl: options.newAuthorizationUrl || options.newAuthz | 
					
						
							|  |  |  |     , newCertificateUrl: options.newCertificateUrl || options.newCert | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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']")); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |     state.domains = options.domains.slice(0); // copy array
 | 
					
						
							|  |  |  |     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'); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |     nextDomain(); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 00:13:07 +00:00
										 |  |  |       state.acme.post(state.newAuthorizationUrl, { | 
					
						
							|  |  |  |         resource:'new-authz', | 
					
						
							|  |  |  |         identifier:{ | 
					
						
							|  |  |  |           type:'dns', | 
					
						
							|  |  |  |           value:state.domain, | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }, getReadyToValidate); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |     function getReadyToValidate(err, res, body) { | 
					
						
							|  |  |  |       var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath; | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       if (err) { | 
					
						
							|  |  |  |         return handleErr(err); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       if (Math.floor(res.statusCode/100)!==2) { | 
					
						
							|  |  |  |         return handleErr(null, 'Authorization request failed ('+res.statusCode+')'); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       links=Acme.parseLink(res.headers.link); | 
					
						
							|  |  |  |       if (!links || !('next' in links)) { | 
					
						
							|  |  |  |         return handleErr(err, 'Server didn\'t provide information to proceed (2)'); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       state.authorizationUrl=res.headers.location; | 
					
						
							|  |  |  |       state.newCertificateUrl=links.next; | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       authz=JSON.parse(body); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       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]; | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       thumbprint=thumbprinter(state.accountKeyPair.publicKey); | 
					
						
							|  |  |  |       keyAuthorization=challenge.token+'.'+thumbprint; | 
					
						
							|  |  |  |       state.responseUrl=challenge.uri; | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +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 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
 | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |           }); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  |         }); | 
					
						
							| 
									
										
										
										
											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 ensureValidation(err, res, body, unlink) { | 
					
						
							|  |  |  |       var authz; | 
					
						
							| 
									
										
										
										
											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(); | 
					
						
							|  |  |  |         return handleErr(err, 'Authorization status request failed ('+res.statusCode+')'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       authz=JSON.parse(body); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       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); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |     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); | 
					
						
							| 
									
										
										
										
											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-16 00:13:07 +00:00
										 |  |  |       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'); | 
					
						
							| 
									
										
										
										
											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; | 
					
						
							|  |  |  |       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(); | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |     function done() { | 
					
						
							|  |  |  |       var cert; | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |       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 | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											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)); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |   function certBufferToPem(cert) { | 
					
						
							| 
									
										
										
										
											2015-12-16 01:18:40 +00:00
										 |  |  |     cert=toStandardB64(cert.toString('base64')); | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |     cert=cert.match(/.{1,64}/g).join('\n'); | 
					
						
							|  |  |  |     return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----'; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-12-15 14:33:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 00:13:07 +00:00
										 |  |  |   return getCert; | 
					
						
							|  |  |  | }; |