| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var U = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var promisify = require('util').promisify; | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | //var resolveSoa = promisify(require('dns').resolveSoa);
 | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | var resolveMx = promisify(require('dns').resolveMx); | 
					
						
							|  |  |  | var punycode = require('punycode'); | 
					
						
							|  |  |  | var Keypairs = require('@root/keypairs'); | 
					
						
							|  |  |  | // TODO move to @root
 | 
					
						
							|  |  |  | var certParser = require('cert-info'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._parseDuration = function(str) { | 
					
						
							|  |  |  | 	if ('number' === typeof str) { | 
					
						
							|  |  |  | 		return str; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var pattern = /^(\-?\d+(\.\d+)?)([wdhms]|ms)$/; | 
					
						
							|  |  |  | 	var matches = str.match(pattern); | 
					
						
							|  |  |  | 	if (!matches || !matches[0]) { | 
					
						
							|  |  |  | 		throw new Error('invalid duration string: ' + str); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var n = parseInt(matches[1], 10); | 
					
						
							|  |  |  | 	var unit = matches[3]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch (unit) { | 
					
						
							|  |  |  | 		case 'w': | 
					
						
							|  |  |  | 			n *= 7; | 
					
						
							|  |  |  | 		/*falls through*/ | 
					
						
							|  |  |  | 		case 'd': | 
					
						
							|  |  |  | 			n *= 24; | 
					
						
							|  |  |  | 		/*falls through*/ | 
					
						
							|  |  |  | 		case 'h': | 
					
						
							|  |  |  | 			n *= 60; | 
					
						
							|  |  |  | 		/*falls through*/ | 
					
						
							|  |  |  | 		case 'm': | 
					
						
							|  |  |  | 			n *= 60; | 
					
						
							|  |  |  | 		/*falls through*/ | 
					
						
							|  |  |  | 		case 's': | 
					
						
							|  |  |  | 			n *= 1000; | 
					
						
							|  |  |  | 		/*falls through*/ | 
					
						
							|  |  |  | 		case 'ms': | 
					
						
							|  |  |  | 			n *= 1; // for completeness
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return n; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._encodeName = function(str) { | 
					
						
							|  |  |  | 	return punycode.toASCII(str.toLowerCase(str)); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._validName = function(str) { | 
					
						
							|  |  |  | 	// A quick check of the 38 and two ½ valid characters
 | 
					
						
							|  |  |  | 	// 253 char max full domain, including dots
 | 
					
						
							|  |  |  | 	// 63 char max each label segment
 | 
					
						
							|  |  |  | 	// Note: * is not allowed, but it's allowable here
 | 
					
						
							|  |  |  | 	// Note: _ (underscore) is only allowed for "domain names", not "hostnames"
 | 
					
						
							|  |  |  | 	// Note: - (hyphen) is not allowed as a first character (but a number is)
 | 
					
						
							|  |  |  | 	return ( | 
					
						
							|  |  |  | 		/^(\*\.)?[a-z0-9_\.\-]+$/.test(str) && | 
					
						
							|  |  |  | 		str.length < 254 && | 
					
						
							|  |  |  | 		str.split('.').every(function(label) { | 
					
						
							|  |  |  | 			return label.length > 0 && label.length < 64; | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._validMx = function(email) { | 
					
						
							|  |  |  | 	var host = email.split('@').slice(1)[0]; | 
					
						
							|  |  |  | 	// try twice, just because DNS hiccups sometimes
 | 
					
						
							|  |  |  | 	// Note: we don't care if the domain exists, just that it *can* exist
 | 
					
						
							|  |  |  | 	return resolveMx(host).catch(function() { | 
					
						
							|  |  |  | 		return U._timeout(1000).then(function() { | 
					
						
							|  |  |  | 			return resolveMx(host); | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // should be called after _validName
 | 
					
						
							|  |  |  | U._validDomain = function(str) { | 
					
						
							|  |  |  | 	// TODO use @root/dns (currently dns-suite)
 | 
					
						
							|  |  |  | 	// because node's dns can't read Authority records
 | 
					
						
							|  |  |  | 	return Promise.resolve(str); | 
					
						
							|  |  |  | 	/* | 
					
						
							|  |  |  | 	// try twice, just because DNS hiccups sometimes
 | 
					
						
							|  |  |  | 	// Note: we don't care if the domain exists, just that it *can* exist
 | 
					
						
							|  |  |  | 	return resolveSoa(str).catch(function() { | 
					
						
							|  |  |  | 		return U._timeout(1000).then(function() { | 
					
						
							|  |  |  | 			return resolveSoa(str); | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  |   */ | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // foo.example.com and *.example.com overlap
 | 
					
						
							|  |  |  | // should be called after _validName
 | 
					
						
							|  |  |  | // (which enforces *. or no *)
 | 
					
						
							|  |  |  | U._uniqueNames = function(altnames) { | 
					
						
							|  |  |  | 	var dups = {}; | 
					
						
							|  |  |  | 	var wilds = {}; | 
					
						
							|  |  |  | 	if ( | 
					
						
							|  |  |  | 		altnames.some(function(w) { | 
					
						
							|  |  |  | 			if ('*.' !== w.slice(0, 2)) { | 
					
						
							|  |  |  | 				return; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if (wilds[w]) { | 
					
						
							|  |  |  | 				return true; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			wilds[w] = true; | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return altnames.every(function(name) { | 
					
						
							|  |  |  | 		var w; | 
					
						
							|  |  |  | 		if ('*.' !== name.slice(0, 2)) { | 
					
						
							|  |  |  | 			w = | 
					
						
							|  |  |  | 				'*.' + | 
					
						
							|  |  |  | 				name | 
					
						
							|  |  |  | 					.split('.') | 
					
						
							|  |  |  | 					.slice(1) | 
					
						
							|  |  |  | 					.join('.'); | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			return true; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (!dups[name] && !dups[w]) { | 
					
						
							|  |  |  | 			dups[name] = true; | 
					
						
							|  |  |  | 			return true; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._timeout = function(d) { | 
					
						
							|  |  |  | 	return new Promise(function(resolve) { | 
					
						
							|  |  |  | 		setTimeout(resolve, d); | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._genKeypair = function(keyType) { | 
					
						
							|  |  |  | 	var keyopts; | 
					
						
							|  |  |  | 	var len = parseInt(keyType.replace(/.*?(\d)/, '$1') || 0, 10); | 
					
						
							|  |  |  | 	if (/RSA/.test(keyType)) { | 
					
						
							|  |  |  | 		keyopts = { | 
					
						
							|  |  |  | 			kty: 'RSA', | 
					
						
							|  |  |  | 			modulusLength: len || 2048 | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 	} else if (/^(EC|P\-?\d)/i.test(keyType)) { | 
					
						
							|  |  |  | 		keyopts = { | 
					
						
							|  |  |  | 			kty: 'EC', | 
					
						
							|  |  |  | 			namedCurve: 'P-' + (len || 256) | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		// TODO put in ./errors.js
 | 
					
						
							|  |  |  | 		throw new Error('invalid key type: ' + keyType); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return Keypairs.generate(keyopts).then(function(pair) { | 
					
						
							|  |  |  | 		return U._jwkToSet(pair.private); | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // TODO use ACME._importKeypair ??
 | 
					
						
							|  |  |  | U._importKeypair = function(keypair) { | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | 	// this should import all formats equally well:
 | 
					
						
							|  |  |  | 	// 'object' (JWK), 'string' (private key pem), kp.privateKeyPem, kp.privateKeyJwk
 | 
					
						
							|  |  |  | 	if (keypair.private || keypair.d) { | 
					
						
							|  |  |  | 		return U._jwkToSet(keypair.private || keypair); | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 	if (keypair.privateKeyJwk) { | 
					
						
							|  |  |  | 		return U._jwkToSet(keypair.privateKeyJwk); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | 	if ('string' !== typeof keypair && !keypair.privateKeyPem) { | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | 		// TODO put in errors
 | 
					
						
							|  |  |  | 		throw new Error('missing private key'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | 	return Keypairs.import({ pem: keypair.privateKeyPem || keypair }).then( | 
					
						
							|  |  |  | 		function(priv) { | 
					
						
							|  |  |  | 			if (!priv.d) { | 
					
						
							|  |  |  | 				throw new Error('missing private key'); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return U._jwkToSet(priv); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	); | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._jwkToSet = function(jwk) { | 
					
						
							|  |  |  | 	var keypair = { | 
					
						
							|  |  |  | 		privateKeyJwk: jwk | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | 	return Promise.all([ | 
					
						
							|  |  |  | 		Keypairs.export({ | 
					
						
							|  |  |  | 			jwk: jwk, | 
					
						
							|  |  |  | 			encoding: 'pem' | 
					
						
							|  |  |  | 		}).then(function(pem) { | 
					
						
							|  |  |  | 			keypair.privateKeyPem = pem; | 
					
						
							|  |  |  | 		}), | 
					
						
							|  |  |  | 		Keypairs.export({ | 
					
						
							|  |  |  | 			jwk: jwk, | 
					
						
							|  |  |  | 			encoding: 'pem', | 
					
						
							|  |  |  | 			public: true | 
					
						
							|  |  |  | 		}).then(function(pem) { | 
					
						
							|  |  |  | 			keypair.publicKeyPem = pem; | 
					
						
							|  |  |  | 		}), | 
					
						
							|  |  |  | 		Keypairs.publish({ | 
					
						
							|  |  |  | 			jwk: jwk | 
					
						
							|  |  |  | 		}).then(function(pub) { | 
					
						
							|  |  |  | 			keypair.publicKeyJwk = pub; | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	]).then(function() { | 
					
						
							|  |  |  | 		return keypair; | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._attachCertInfo = function(results) { | 
					
						
							|  |  |  | 	var certInfo = certParser.info(results.cert); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// subject, altnames, issuedAt, expiresAt
 | 
					
						
							|  |  |  | 	Object.keys(certInfo).forEach(function(key) { | 
					
						
							|  |  |  | 		results[key] = certInfo[key]; | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return results; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._certHasDomain = function(certInfo, _domain) { | 
					
						
							|  |  |  | 	var names = (certInfo.altnames || []).slice(0); | 
					
						
							|  |  |  | 	return names.some(function(name) { | 
					
						
							|  |  |  | 		var domain = _domain.toLowerCase(); | 
					
						
							|  |  |  | 		name = name.toLowerCase(); | 
					
						
							|  |  |  | 		if ('*.' === name.substr(0, 2)) { | 
					
						
							|  |  |  | 			name = name.substr(2); | 
					
						
							|  |  |  | 			domain = domain | 
					
						
							|  |  |  | 				.split('.') | 
					
						
							|  |  |  | 				.slice(1) | 
					
						
							|  |  |  | 				.join('.'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return name === domain; | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // a bit heavy to be labeled 'utils'... perhaps 'common' would be better?
 | 
					
						
							|  |  |  | U._getOrCreateKeypair = function(db, subject, query, keyType, mustExist) { | 
					
						
							|  |  |  | 	var exists = false; | 
					
						
							|  |  |  | 	return db | 
					
						
							|  |  |  | 		.checkKeypair(query) | 
					
						
							|  |  |  | 		.then(function(kp) { | 
					
						
							|  |  |  | 			if (kp) { | 
					
						
							|  |  |  | 				exists = true; | 
					
						
							|  |  |  | 				return U._importKeypair(kp); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (mustExist) { | 
					
						
							|  |  |  | 				// TODO put in errors
 | 
					
						
							|  |  |  | 				throw new Error( | 
					
						
							|  |  |  | 					'required keypair not found: ' + | 
					
						
							|  |  |  | 						(subject || '') + | 
					
						
							|  |  |  | 						' ' + | 
					
						
							|  |  |  | 						JSON.stringify(query) | 
					
						
							|  |  |  | 				); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return U._genKeypair(keyType); | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		.then(function(keypair) { | 
					
						
							|  |  |  | 			return { exists: exists, keypair: keypair }; | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | U._getKeypair = function(db, subject, query) { | 
					
						
							| 
									
										
										
										
											2019-10-27 04:38:05 -06:00
										 |  |  | 	return U._getOrCreateKeypair(db, subject, query, '', true).then(function( | 
					
						
							|  |  |  | 		result | 
					
						
							|  |  |  | 	) { | 
					
						
							|  |  |  | 		return result.keypair; | 
					
						
							|  |  |  | 	}); | 
					
						
							| 
									
										
										
										
											2019-10-20 02:51:19 -06:00
										 |  |  | }; |