| 
									
										
										
										
											2019-10-24 11:39:25 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require('dotenv').config(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-25 04:54:40 -06:00
										 |  |  | var pkg = require('../package.json'); | 
					
						
							| 
									
										
										
										
											2019-10-24 11:39:25 -06:00
										 |  |  | var CSR = require('@root/csr'); | 
					
						
							|  |  |  | var Enc = require('@root/encoding/base64'); | 
					
						
							|  |  |  | var PEM = require('@root/pem'); | 
					
						
							|  |  |  | var punycode = require('punycode'); | 
					
						
							|  |  |  | var ACME = require('../acme.js'); | 
					
						
							|  |  |  | var Keypairs = require('@root/keypairs'); | 
					
						
							| 
									
										
										
										
											2019-10-25 04:54:40 -06:00
										 |  |  | var ecJwk = require('../fixtures/account.jwk.json'); | 
					
						
							| 
									
										
										
										
											2019-10-24 11:39:25 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | // TODO exec npm install --save-dev CHALLENGE_MODULE
 | 
					
						
							|  |  |  | if (!process.env.CHALLENGE_OPTIONS) { | 
					
						
							|  |  |  | 	console.error( | 
					
						
							|  |  |  | 		'Please create a .env in the format of examples/example.env to run the tests' | 
					
						
							|  |  |  | 	); | 
					
						
							|  |  |  | 	process.exit(1); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var config = { | 
					
						
							|  |  |  | 	env: process.env.ENV, | 
					
						
							|  |  |  | 	email: process.env.SUBSCRIBER_EMAIL, | 
					
						
							|  |  |  | 	domain: process.env.BASE_DOMAIN, | 
					
						
							|  |  |  | 	challengeType: process.env.CHALLENGE_TYPE, | 
					
						
							|  |  |  | 	challengeModule: process.env.CHALLENGE_PLUGIN, | 
					
						
							|  |  |  | 	challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS) | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | //config.debug = !/^PROD/i.test(config.env);
 | 
					
						
							|  |  |  | var pluginPrefix = 'acme-' + config.challengeType + '-'; | 
					
						
							|  |  |  | var pluginName = config.challengeModule; | 
					
						
							|  |  |  | var plugin; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = function() { | 
					
						
							|  |  |  | 	console.info('\n[Test] end-to-end issue certificates'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var acme = ACME.create({ | 
					
						
							|  |  |  | 		// debug: true
 | 
					
						
							|  |  |  | 		maintainerEmail: config.email, | 
					
						
							| 
									
										
										
										
											2019-10-25 04:54:40 -06:00
										 |  |  | 		packageAgent: 'test-' + pkg.name + '/' + pkg.version, | 
					
						
							| 
									
										
										
										
											2019-10-24 11:39:25 -06:00
										 |  |  | 		notify: function(ev, params) { | 
					
						
							|  |  |  | 			console.info( | 
					
						
							|  |  |  | 				'\t' + ev, | 
					
						
							|  |  |  | 				params.subject || params.altname || params.domain || '', | 
					
						
							|  |  |  | 				params.status || '' | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 			if ('error' === ev) { | 
					
						
							|  |  |  | 				console.error(params.action || params.type || ''); | 
					
						
							|  |  |  | 				console.error(params); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	function badPlugin(err) { | 
					
						
							|  |  |  | 		if ('MODULE_NOT_FOUND' !== err.code) { | 
					
						
							|  |  |  | 			console.error(err); | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		console.error("Couldn't find '" + pluginName + "'. Is it installed?"); | 
					
						
							|  |  |  | 		console.error("\tnpm install --save-dev '" + pluginName + "'"); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		plugin = require(pluginName); | 
					
						
							|  |  |  | 	} catch (err) { | 
					
						
							|  |  |  | 		if ( | 
					
						
							|  |  |  | 			'MODULE_NOT_FOUND' !== err.code || | 
					
						
							|  |  |  | 			0 === pluginName.indexOf(pluginPrefix) | 
					
						
							|  |  |  | 		) { | 
					
						
							|  |  |  | 			badPlugin(err); | 
					
						
							|  |  |  | 			process.exit(1); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		try { | 
					
						
							|  |  |  | 			pluginName = pluginPrefix + pluginName; | 
					
						
							|  |  |  | 			plugin = require(pluginName); | 
					
						
							|  |  |  | 		} catch (e) { | 
					
						
							|  |  |  | 			badPlugin(e); | 
					
						
							|  |  |  | 			process.exit(1); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	config.challenger = plugin.create(config.challengeOptions); | 
					
						
							|  |  |  | 	if (!config.challengeType || !config.domain) { | 
					
						
							|  |  |  | 		console.error( | 
					
						
							|  |  |  | 			new Error('Missing config variables. Check you .env and the docs') | 
					
						
							|  |  |  | 				.message | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 		console.error(config); | 
					
						
							|  |  |  | 		process.exit(1); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var challenges = {}; | 
					
						
							|  |  |  | 	challenges[config.challengeType] = config.challenger; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	async function happyPath(accKty, srvKty, rnd) { | 
					
						
							|  |  |  | 		var agreed = false; | 
					
						
							|  |  |  | 		var metadata = await acme.init( | 
					
						
							|  |  |  | 			'https://acme-staging-v02.api.letsencrypt.org/directory' | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Ready to use, show page
 | 
					
						
							|  |  |  | 		if (config.debug) { | 
					
						
							|  |  |  | 			console.info('ACME.js initialized'); | 
					
						
							|  |  |  | 			console.info(metadata); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var accountKeypair = await Keypairs.generate({ kty: accKty }); | 
					
						
							| 
									
										
										
										
											2019-10-25 04:54:40 -06:00
										 |  |  | 		if (/EC/i.test(accKty)) { | 
					
						
							|  |  |  | 			// to test that an existing account gets back data
 | 
					
						
							|  |  |  | 			accountKeypair = ecJwk; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2019-10-24 11:39:25 -06:00
										 |  |  | 		var accountKey = accountKeypair.private; | 
					
						
							|  |  |  | 		if (config.debug) { | 
					
						
							|  |  |  | 			console.info('Account Key Created'); | 
					
						
							|  |  |  | 			console.info(JSON.stringify(accountKey, null, 2)); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var account = await acme.accounts.create({ | 
					
						
							|  |  |  | 			agreeToTerms: agree, | 
					
						
							|  |  |  | 			// TODO detect jwk/pem/der?
 | 
					
						
							|  |  |  | 			accountKey: accountKey, | 
					
						
							|  |  |  | 			subscriberEmail: config.email | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// TODO top-level agree
 | 
					
						
							|  |  |  | 		function agree(tos) { | 
					
						
							|  |  |  | 			if (config.debug) { | 
					
						
							|  |  |  | 				console.info('Agreeing to Terms of Service:'); | 
					
						
							|  |  |  | 				console.info(tos); | 
					
						
							|  |  |  | 				console.info(); | 
					
						
							|  |  |  | 				console.info(); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			agreed = true; | 
					
						
							| 
									
										
										
										
											2019-10-26 00:03:43 -06:00
										 |  |  | 			return Promise.resolve(agreed); | 
					
						
							| 
									
										
										
										
											2019-10-24 11:39:25 -06:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		if (config.debug) { | 
					
						
							|  |  |  | 			console.info('New Subscriber Account'); | 
					
						
							|  |  |  | 			console.info(JSON.stringify(account, null, 2)); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if (!agreed) { | 
					
						
							|  |  |  | 			throw new Error('Failed to ask the user to agree to terms'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var certKeypair = await Keypairs.generate({ kty: srvKty }); | 
					
						
							|  |  |  | 		var pem = await Keypairs.export({ | 
					
						
							|  |  |  | 			jwk: certKeypair.private, | 
					
						
							|  |  |  | 			encoding: 'pem' | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 		if (config.debug) { | 
					
						
							|  |  |  | 			console.info('Server Key Created'); | 
					
						
							|  |  |  | 			console.info('privkey.jwk.json'); | 
					
						
							|  |  |  | 			console.info(JSON.stringify(certKeypair, null, 2)); | 
					
						
							|  |  |  | 			// This should be saved as `privkey.pem`
 | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 			console.info('privkey.' + srvKty.toLowerCase() + '.pem:'); | 
					
						
							|  |  |  | 			console.info(pem); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// 'subject' should be first in list
 | 
					
						
							|  |  |  | 		var domains = randomDomains(rnd); | 
					
						
							|  |  |  | 		if (config.debug) { | 
					
						
							|  |  |  | 			console.info('Get certificates for random domains:'); | 
					
						
							|  |  |  | 			console.info( | 
					
						
							|  |  |  | 				domains | 
					
						
							|  |  |  | 					.map(function(puny) { | 
					
						
							|  |  |  | 						var uni = punycode.toUnicode(puny); | 
					
						
							|  |  |  | 						if (puny !== uni) { | 
					
						
							|  |  |  | 							return puny + ' (' + uni + ')'; | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 						return puny; | 
					
						
							|  |  |  | 					}) | 
					
						
							|  |  |  | 					.join('\n') | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Create CSR
 | 
					
						
							|  |  |  | 		var csrDer = await CSR.csr({ | 
					
						
							|  |  |  | 			jwk: certKeypair.private, | 
					
						
							|  |  |  | 			domains: domains, | 
					
						
							|  |  |  | 			encoding: 'der' | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 		var csr = Enc.bufToUrlBase64(csrDer); | 
					
						
							|  |  |  | 		var csrPem = PEM.packBlock({ | 
					
						
							|  |  |  | 			type: 'CERTIFICATE REQUEST', | 
					
						
							|  |  |  | 			bytes: csrDer /* { jwk: jwk, domains: opts.domains } */ | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 		if (config.debug) { | 
					
						
							|  |  |  | 			console.info('Certificate Signing Request'); | 
					
						
							|  |  |  | 			console.info(csrPem); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var results = await acme.certificates.create({ | 
					
						
							|  |  |  | 			account: account, | 
					
						
							|  |  |  | 			accountKey: accountKey, | 
					
						
							|  |  |  | 			csr: csr, | 
					
						
							|  |  |  | 			domains: domains, | 
					
						
							|  |  |  | 			challenges: challenges, // must be implemented
 | 
					
						
							|  |  |  | 			customerEmail: null | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (config.debug) { | 
					
						
							|  |  |  | 			console.info('Got SSL Certificate:'); | 
					
						
							|  |  |  | 			console.info(Object.keys(results)); | 
					
						
							|  |  |  | 			console.info(results.expires); | 
					
						
							|  |  |  | 			console.info(results.cert); | 
					
						
							|  |  |  | 			console.info(results.chain); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 			console.info(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Try EC + RSA
 | 
					
						
							|  |  |  | 	var rnd = random(); | 
					
						
							|  |  |  | 	happyPath('EC', 'RSA', rnd) | 
					
						
							|  |  |  | 		.then(function() { | 
					
						
							|  |  |  | 			console.info('PASS: ECDSA account key with RSA server key'); | 
					
						
							|  |  |  | 			// Now try RSA + EC
 | 
					
						
							|  |  |  | 			rnd = random(); | 
					
						
							|  |  |  | 			return happyPath('RSA', 'EC', rnd).then(function() { | 
					
						
							|  |  |  | 				console.info('PASS: RSA account key with ECDSA server key'); | 
					
						
							|  |  |  | 			}); | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		.then(function() { | 
					
						
							|  |  |  | 			console.info('PASS'); | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		.catch(function(err) { | 
					
						
							|  |  |  | 			console.error('Error:'); | 
					
						
							|  |  |  | 			console.error(err.stack); | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	function randomDomains(rnd) { | 
					
						
							|  |  |  | 		return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map( | 
					
						
							|  |  |  | 			function(pre) { | 
					
						
							|  |  |  | 				return punycode.toASCII(pre + '-' + rnd + '.' + config.domain); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	function random() { | 
					
						
							|  |  |  | 		return ( | 
					
						
							|  |  |  | 			parseInt( | 
					
						
							|  |  |  | 				Math.random() | 
					
						
							|  |  |  | 					.toString() | 
					
						
							|  |  |  | 					.slice(2, 99), | 
					
						
							|  |  |  | 				10 | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 				.toString(16) | 
					
						
							|  |  |  | 				.slice(0, 4) + '例' | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; |