| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var C = module.exports; | 
					
						
							|  |  |  | var U = require('./utils.js'); | 
					
						
							|  |  |  | var CSR = require('@root/csr'); | 
					
						
							|  |  |  | var Enc = require('@root/encoding'); | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | var Keypairs = require('@root/keypairs'); | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | var pending = {}; | 
					
						
							|  |  |  | var rawPending = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | // What the abbreviations mean
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // gnlkc => greenlock
 | 
					
						
							|  |  |  | // mconf => manager config
 | 
					
						
							|  |  |  | // db => greenlock store instance
 | 
					
						
							|  |  |  | // acme => instance of ACME.js
 | 
					
						
							|  |  |  | // chs => instances of challenges
 | 
					
						
							|  |  |  | // acc => account
 | 
					
						
							|  |  |  | // args => site / extra options
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | // Certificates
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | C._getOrOrder = function(gnlck, mconf, db, acme, chs, acc, args) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     var email = args.subscriberEmail || mconf.subscriberEmail; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     var id = args.altnames | 
					
						
							|  |  |  |         .slice(0) | 
					
						
							|  |  |  |         .sort() | 
					
						
							|  |  |  |         .join(' '); | 
					
						
							|  |  |  |     if (pending[id]) { | 
					
						
							|  |  |  |         return pending[id]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     pending[id] = C._rawGetOrOrder( | 
					
						
							|  |  |  |         gnlck, | 
					
						
							|  |  |  |         mconf, | 
					
						
							|  |  |  |         db, | 
					
						
							|  |  |  |         acme, | 
					
						
							|  |  |  |         chs, | 
					
						
							|  |  |  |         acc, | 
					
						
							|  |  |  |         email, | 
					
						
							|  |  |  |         args | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |         .then(function(pems) { | 
					
						
							|  |  |  |             delete pending[id]; | 
					
						
							|  |  |  |             return pems; | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .catch(function(err) { | 
					
						
							|  |  |  |             delete pending[id]; | 
					
						
							|  |  |  |             throw err; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return pending[id]; | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Certificates
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | C._rawGetOrOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     return C._check(gnlck, mconf, db, args).then(function(pems) { | 
					
						
							|  |  |  |         // Nice and fresh? We're done!
 | 
					
						
							|  |  |  |         if (pems) { | 
					
						
							|  |  |  |             if (!C._isStale(gnlck, mconf, args, pems)) { | 
					
						
							|  |  |  |                 // return existing unexpired (although potentially stale) certificates when available
 | 
					
						
							|  |  |  |                 // there will be an additional .renewing property if the certs are being asynchronously renewed
 | 
					
						
							|  |  |  |                 //pems._type = 'current';
 | 
					
						
							|  |  |  |                 return pems; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // We're either starting fresh or freshening up...
 | 
					
						
							|  |  |  |         var p = C._rawOrder(gnlck, mconf, db, acme, chs, acc, email, args); | 
					
						
							|  |  |  |         var evname = pems ? 'cert_renewal' : 'cert_issue'; | 
					
						
							|  |  |  |         p.then(function(newPems) { | 
					
						
							|  |  |  |             // notify in the background
 | 
					
						
							|  |  |  |             var renewAt = C._renewWithStagger(gnlck, mconf, args, newPems); | 
					
						
							|  |  |  |             gnlck._notify(evname, { | 
					
						
							|  |  |  |                 renewAt: renewAt, | 
					
						
							|  |  |  |                 subject: args.subject, | 
					
						
							|  |  |  |                 altnames: args.altnames | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |             gnlck._notify('_cert_issue', { | 
					
						
							|  |  |  |                 renewAt: renewAt, | 
					
						
							|  |  |  |                 subject: args.subject, | 
					
						
							|  |  |  |                 altnames: args.altnames, | 
					
						
							|  |  |  |                 pems: newPems | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |         }).catch(function(err) { | 
					
						
							|  |  |  |             if (!err.context) { | 
					
						
							|  |  |  |                 err.context = evname; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             err.subject = args.subject; | 
					
						
							|  |  |  |             err.altnames = args.altnames; | 
					
						
							|  |  |  |             gnlck._notify('error', err); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // No choice but to hang tight and wait for it
 | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |             !pems || | 
					
						
							|  |  |  |             pems.renewAt < Date.now() - 24 * 60 * 60 * 1000 || | 
					
						
							|  |  |  |             pems.expiresAt <= Date.now() + 24 * 60 * 60 * 1000 | 
					
						
							|  |  |  |         ) { | 
					
						
							|  |  |  |             return p; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Wait it out
 | 
					
						
							|  |  |  |         // TODO should we call this waitForRenewal?
 | 
					
						
							|  |  |  |         if (args.waitForRenewal) { | 
					
						
							|  |  |  |             return p; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Let the certs renew in the background
 | 
					
						
							|  |  |  |         return pems; | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // we have another promise here because it the optional renewal
 | 
					
						
							|  |  |  | // may resolve in a different stack than the returned pems
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | C._rawOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     var id = args.altnames | 
					
						
							|  |  |  |         .slice(0) | 
					
						
							|  |  |  |         .sort() | 
					
						
							|  |  |  |         .join(' '); | 
					
						
							|  |  |  |     if (rawPending[id]) { | 
					
						
							|  |  |  |         return rawPending[id]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     var keyType = args.serverKeyType || mconf.serverKeyType; | 
					
						
							|  |  |  |     var query = { | 
					
						
							|  |  |  |         subject: args.subject, | 
					
						
							|  |  |  |         certificate: args.certificate || {}, | 
					
						
							|  |  |  |         directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType) | 
					
						
							|  |  |  |         .then(function(kresult) { | 
					
						
							|  |  |  |             var serverKeypair = kresult.keypair; | 
					
						
							|  |  |  |             var domains = args.altnames.slice(0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return CSR.csr({ | 
					
						
							|  |  |  |                 jwk: serverKeypair.privateKeyJwk || serverKeypair.private, | 
					
						
							|  |  |  |                 domains: domains, | 
					
						
							|  |  |  |                 encoding: 'der' | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |                 .then(function(csrDer) { | 
					
						
							|  |  |  |                     // TODO let CSR support 'urlBase64' ?
 | 
					
						
							|  |  |  |                     return Enc.bufToUrlBase64(csrDer); | 
					
						
							|  |  |  |                 }) | 
					
						
							|  |  |  |                 .then(function(csr) { | 
					
						
							|  |  |  |                     function notify(ev, opts) { | 
					
						
							|  |  |  |                         gnlck._notify(ev, opts); | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     var certReq = { | 
					
						
							|  |  |  |                         debug: args.debug || gnlck._defaults.debug, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                         challenges: chs, | 
					
						
							|  |  |  |                         account: acc, // only used if accounts.key.kid exists
 | 
					
						
							|  |  |  |                         accountKey: | 
					
						
							|  |  |  |                             acc.keypair.privateKeyJwk || acc.keypair.private, | 
					
						
							|  |  |  |                         keypair: acc.keypair, // TODO
 | 
					
						
							|  |  |  |                         csr: csr, | 
					
						
							|  |  |  |                         domains: domains, // because ACME.js v3 uses `domains` still, actually
 | 
					
						
							|  |  |  |                         onChallengeStatus: notify, | 
					
						
							|  |  |  |                         notify: notify // TODO
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                         // TODO handle this in acme-v2
 | 
					
						
							|  |  |  |                         //subject: args.subject,
 | 
					
						
							|  |  |  |                         //altnames: args.altnames.slice(0),
 | 
					
						
							|  |  |  |                     }; | 
					
						
							|  |  |  |                     return acme.certificates | 
					
						
							|  |  |  |                         .create(certReq) | 
					
						
							|  |  |  |                         .then(U._attachCertInfo); | 
					
						
							|  |  |  |                 }) | 
					
						
							|  |  |  |                 .then(function(pems) { | 
					
						
							|  |  |  |                     if (kresult.exists) { | 
					
						
							|  |  |  |                         return pems; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     query.keypair = serverKeypair; | 
					
						
							|  |  |  |                     return db.setKeypair(query, serverKeypair).then(function() { | 
					
						
							|  |  |  |                         return pems; | 
					
						
							|  |  |  |                     }); | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .then(function(pems) { | 
					
						
							|  |  |  |             // TODO put this in the docs
 | 
					
						
							|  |  |  |             // { cert, chain, privkey, subject, altnames, issuedAt, expiresAt }
 | 
					
						
							|  |  |  |             // Note: the query has been updated
 | 
					
						
							|  |  |  |             query.pems = pems; | 
					
						
							|  |  |  |             return db.set(query); | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .then(function() { | 
					
						
							|  |  |  |             return C._check(gnlck, mconf, db, args); | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .then(function(bundle) { | 
					
						
							|  |  |  |             // TODO notify Manager
 | 
					
						
							|  |  |  |             delete rawPending[id]; | 
					
						
							|  |  |  |             return bundle; | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .catch(function(err) { | 
					
						
							|  |  |  |             // Todo notify manager
 | 
					
						
							|  |  |  |             delete rawPending[id]; | 
					
						
							|  |  |  |             throw err; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return rawPending[id]; | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // returns pems, if they exist
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | C._check = function(gnlck, mconf, db, args) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     var query = { | 
					
						
							|  |  |  |         subject: args.subject, | 
					
						
							|  |  |  |         // may contain certificate.id
 | 
					
						
							|  |  |  |         certificate: args.certificate, | 
					
						
							|  |  |  |         directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     return db.check(query).then(function(pems) { | 
					
						
							|  |  |  |         if (!pems) { | 
					
						
							|  |  |  |             return null; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         pems = U._attachCertInfo(pems); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // For eager management
 | 
					
						
							|  |  |  |         if (args.subject && !U._certHasDomain(pems, args.subject)) { | 
					
						
							|  |  |  |             // TODO report error, but continue the process as with no cert
 | 
					
						
							|  |  |  |             return null; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // For lazy SNI requests
 | 
					
						
							|  |  |  |         if (args.domain && !U._certHasDomain(pems, args.domain)) { | 
					
						
							|  |  |  |             // TODO report error, but continue the process as with no cert
 | 
					
						
							|  |  |  |             return null; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return U._getKeypair(db, args.subject, query) | 
					
						
							|  |  |  |             .then(function(keypair) { | 
					
						
							|  |  |  |                 return Keypairs.export({ | 
					
						
							|  |  |  |                     jwk: keypair.privateKeyJwk || keypair.private, | 
					
						
							|  |  |  |                     encoding: 'pem' | 
					
						
							|  |  |  |                 }).then(function(pem) { | 
					
						
							|  |  |  |                     pems.privkey = pem; | 
					
						
							|  |  |  |                     return pems; | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |             .catch(function() { | 
					
						
							|  |  |  |                 // TODO report error, but continue the process as with no cert
 | 
					
						
							|  |  |  |                 return null; | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Certificates
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | C._isStale = function(gnlck, mconf, args, pems) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     if (args.duplicate) { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     var renewAt = C._renewableAt(gnlck, mconf, args, pems); | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     if (Date.now() >= renewAt) { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     return false; | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-29 06:19:26 +00:00
										 |  |  | C._renewWithStagger = function(gnlck, mconf, args, pems) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     var renewOffset = C._renewOffset(gnlck, mconf, args, pems); | 
					
						
							|  |  |  |     var renewStagger; | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |         renewStagger = U._parseDuration( | 
					
						
							|  |  |  |             args.renewStagger || mconf.renewStagger || 0 | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |     } catch (e) { | 
					
						
							|  |  |  |         renewStagger = U._parseDuration( | 
					
						
							|  |  |  |             args.renewStagger || mconf.renewStagger | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // TODO check this beforehand
 | 
					
						
							|  |  |  |     if (!args.force && renewStagger / renewOffset >= 0.5) { | 
					
						
							|  |  |  |         renewStagger = renewOffset * 0.1; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (renewOffset > 0) { | 
					
						
							|  |  |  |         // stagger forward, away from issued at
 | 
					
						
							|  |  |  |         return Math.round( | 
					
						
							|  |  |  |             pems.issuedAt + renewOffset + Math.random() * renewStagger | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // stagger backward, toward issued at
 | 
					
						
							|  |  |  |     return Math.round( | 
					
						
							|  |  |  |         pems.expiresAt + renewOffset - Math.random() * renewStagger | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2019-10-29 06:19:26 +00:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2019-10-31 05:49:37 -06:00
										 |  |  | C._renewOffset = function(gnlck, mconf, args /*, pems*/) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     var renewOffset = U._parseDuration( | 
					
						
							|  |  |  |         args.renewOffset || mconf.renewOffset || 0 | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     var week = 1000 * 60 * 60 * 24 * 6; | 
					
						
							|  |  |  |     if (!args.force && Math.abs(renewOffset) < week) { | 
					
						
							|  |  |  |         throw new Error( | 
					
						
							|  |  |  |             'developer error: `renewOffset` should always be at least a week, use `force` to not safety-check renewOffset' | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return renewOffset; | 
					
						
							| 
									
										
										
										
											2019-10-29 06:19:26 +00:00
										 |  |  | }; | 
					
						
							|  |  |  | C._renewableAt = function(gnlck, mconf, args, pems) { | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     if (args.renewAt) { | 
					
						
							|  |  |  |         return args.renewAt; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-10-29 06:19:26 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     var renewOffset = C._renewOffset(gnlck, mconf, args, pems); | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     if (renewOffset > 0) { | 
					
						
							|  |  |  |         return pems.issuedAt + renewOffset; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-31 16:26:18 -06:00
										 |  |  |     return pems.expiresAt + renewOffset; | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; |