forked from root/acme.js
		
	update API and tests
This commit is contained in:
		
							parent
							
								
									f05e9db38e
								
							
						
					
					
						commit
						0efa94eeb0
					
				
							
								
								
									
										263
									
								
								acme.js
									
									
									
									
									
								
							
							
						
						
									
										263
									
								
								acme.js
									
									
									
									
									
								
							@ -179,17 +179,17 @@ ACME._testChallengeOptions = function() {
 | 
			
		||||
 | 
			
		||||
ACME._thumber = function(me, options, thumb) {
 | 
			
		||||
	var thumbPromise;
 | 
			
		||||
	return function() {
 | 
			
		||||
	return function(key) {
 | 
			
		||||
		if (thumb) {
 | 
			
		||||
			return Promise.resolve(thumb);
 | 
			
		||||
		}
 | 
			
		||||
		if (thumbPromise) {
 | 
			
		||||
			return thumbPromise;
 | 
			
		||||
		}
 | 
			
		||||
		thumbPromise = U._importKeypair(
 | 
			
		||||
			me,
 | 
			
		||||
			options.accountKey || options.accountKeypair
 | 
			
		||||
		).then(function(pair) {
 | 
			
		||||
		if (!key) {
 | 
			
		||||
			key = options.accountKey || options.accountKeypair;
 | 
			
		||||
		}
 | 
			
		||||
		thumbPromise = U._importKeypair(null, key).then(function(pair) {
 | 
			
		||||
			return Keypairs.thumbprint({
 | 
			
		||||
				jwk: pair.public
 | 
			
		||||
			});
 | 
			
		||||
@ -266,7 +266,14 @@ ACME._dryRun = function(me, realOptions) {
 | 
			
		||||
					type: ch.type
 | 
			
		||||
					//challenge: ch
 | 
			
		||||
				});
 | 
			
		||||
				noopts.challenges[ch.type].remove({ challenge: ch });
 | 
			
		||||
				noopts.challenges[ch.type]
 | 
			
		||||
					.remove({ challenge: ch })
 | 
			
		||||
					.catch(function(err) {
 | 
			
		||||
						err.action = 'challenge_remove';
 | 
			
		||||
						err.altname = ch.altname;
 | 
			
		||||
						err.type = ch.type;
 | 
			
		||||
						ACME._notify(me, noopts, 'error', err);
 | 
			
		||||
					});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -310,95 +317,117 @@ ACME._computeAuths = function(me, options, thumb, request, dryrun) {
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var getThumbprint = ACME._thumber(me, options, thumb);
 | 
			
		||||
	var getThumbprint = ACME._thumber(null, options, thumb);
 | 
			
		||||
 | 
			
		||||
	return getThumbprint().then(function(thumb) {
 | 
			
		||||
		return Promise.all(
 | 
			
		||||
			request.challenges.map(function(challenge) {
 | 
			
		||||
				// Don't do extra work for challenges that we can't satisfy
 | 
			
		||||
				if (!options._presenterTypes.includes(challenge.type)) {
 | 
			
		||||
					return null;
 | 
			
		||||
				}
 | 
			
		||||
	return Promise.all(
 | 
			
		||||
		request.challenges.map(function(challenge) {
 | 
			
		||||
			// Don't do extra work for challenges that we can't satisfy
 | 
			
		||||
			if (!options._presenterTypes.includes(challenge.type)) {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				var auth = {};
 | 
			
		||||
			var auth = {};
 | 
			
		||||
 | 
			
		||||
				// straight copy from the new order response
 | 
			
		||||
				// { identifier, status, expires, challenges, wildcard }
 | 
			
		||||
				Object.keys(request).forEach(function(key) {
 | 
			
		||||
					auth[key] = request[key];
 | 
			
		||||
			// straight copy from the new order response
 | 
			
		||||
			// { identifier, status, expires, challenges, wildcard }
 | 
			
		||||
			Object.keys(request).forEach(function(key) {
 | 
			
		||||
				auth[key] = request[key];
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// copy from the challenge we've chosen
 | 
			
		||||
			// { type, status, url, token }
 | 
			
		||||
			// (note the duplicate status overwrites the one above, but they should be the same)
 | 
			
		||||
			Object.keys(challenge).forEach(function(key) {
 | 
			
		||||
				// don't confused devs with the id url
 | 
			
		||||
				auth[key] = challenge[key];
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// batteries-included helpers
 | 
			
		||||
			auth.hostname = auth.identifier.value;
 | 
			
		||||
			// because I'm not 100% clear if the wildcard identifier does or doesn't
 | 
			
		||||
			// have the leading *. in all cases
 | 
			
		||||
			auth.altname = ACME._untame(auth.identifier.value, auth.wildcard);
 | 
			
		||||
 | 
			
		||||
			var zone = pluckZone(
 | 
			
		||||
				options.zonenames || [],
 | 
			
		||||
				auth.identifier.value
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			return ACME.computeChallenge({
 | 
			
		||||
				accountKey: options.accountKey,
 | 
			
		||||
				_getThumbprint: getThumbprint,
 | 
			
		||||
				challenge: auth,
 | 
			
		||||
				zone: zone,
 | 
			
		||||
				dnsPrefix: dnsPrefix
 | 
			
		||||
			}).then(function(resp) {
 | 
			
		||||
				Object.keys(resp).forEach(function(k) {
 | 
			
		||||
					auth[k] = resp[k];
 | 
			
		||||
				});
 | 
			
		||||
				return auth;
 | 
			
		||||
			});
 | 
			
		||||
		})
 | 
			
		||||
	).then(function(auths) {
 | 
			
		||||
		return auths.filter(Boolean);
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
				// copy from the challenge we've chosen
 | 
			
		||||
				// { type, status, url, token }
 | 
			
		||||
				// (note the duplicate status overwrites the one above, but they should be the same)
 | 
			
		||||
				Object.keys(challenge).forEach(function(key) {
 | 
			
		||||
					// don't confused devs with the id url
 | 
			
		||||
					auth[key] = challenge[key];
 | 
			
		||||
				});
 | 
			
		||||
ACME.computeChallenge = function(opts) {
 | 
			
		||||
	var auth = opts.challenge;
 | 
			
		||||
	var hostname = auth.hostname || opts.hostname;
 | 
			
		||||
	var zone = opts.zone;
 | 
			
		||||
	var thumb = opts.thumbprint || '';
 | 
			
		||||
	var accountKey = opts.accountKey;
 | 
			
		||||
	var getThumbprint = opts._getThumbprint || ACME._thumber(null, opts, thumb);
 | 
			
		||||
	var dnsPrefix = opts.dnsPrefix || ACME.challengePrefixes['dns-01'];
 | 
			
		||||
 | 
			
		||||
				// batteries-included helpers
 | 
			
		||||
				auth.hostname = auth.identifier.value;
 | 
			
		||||
				// because I'm not 100% clear if the wildcard identifier does or doesn't
 | 
			
		||||
				// have the leading *. in all cases
 | 
			
		||||
				auth.altname = ACME._untame(
 | 
			
		||||
					auth.identifier.value,
 | 
			
		||||
					auth.wildcard
 | 
			
		||||
				);
 | 
			
		||||
	return getThumbprint(accountKey).then(function(thumb) {
 | 
			
		||||
		var resp = {};
 | 
			
		||||
		resp.thumbprint = thumb;
 | 
			
		||||
		//   keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey))
 | 
			
		||||
		resp.keyAuthorization = auth.token + '.' + thumb;
 | 
			
		||||
 | 
			
		||||
				auth.thumbprint = thumb;
 | 
			
		||||
				//   keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey))
 | 
			
		||||
				auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
 | 
			
		||||
		if ('http-01' === auth.type) {
 | 
			
		||||
			// conflicts with ACME challenge id url is already in use,
 | 
			
		||||
			// so we call this challengeUrl instead
 | 
			
		||||
			// TODO auth.http01Url ?
 | 
			
		||||
			resp.challengeUrl =
 | 
			
		||||
				'http://' +
 | 
			
		||||
				// `hostname` is an alias of `auth.indentifier.value`
 | 
			
		||||
				hostname +
 | 
			
		||||
				ACME.challengePrefixes['http-01'] +
 | 
			
		||||
				'/' +
 | 
			
		||||
				auth.token;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				if ('http-01' === auth.type) {
 | 
			
		||||
					// conflicts with ACME challenge id url is already in use,
 | 
			
		||||
					// so we call this challengeUrl instead
 | 
			
		||||
					// TODO auth.http01Url ?
 | 
			
		||||
					auth.challengeUrl =
 | 
			
		||||
						'http://' +
 | 
			
		||||
						auth.identifier.value +
 | 
			
		||||
						ACME.challengePrefixes['http-01'] +
 | 
			
		||||
						'/' +
 | 
			
		||||
						auth.token;
 | 
			
		||||
					return auth;
 | 
			
		||||
				}
 | 
			
		||||
		if ('dns-01' !== auth.type) {
 | 
			
		||||
			return resp;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				if ('dns-01' !== auth.type) {
 | 
			
		||||
					return auth;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				var zone = pluckZone(
 | 
			
		||||
					options.zonenames || [],
 | 
			
		||||
					auth.identifier.value
 | 
			
		||||
				);
 | 
			
		||||
 | 
			
		||||
				// Always calculate dnsAuthorization because we
 | 
			
		||||
				// may need to present to the user for confirmation / instruction
 | 
			
		||||
				// _as part of_ the decision making process
 | 
			
		||||
				return sha2
 | 
			
		||||
					.sum(256, auth.keyAuthorization)
 | 
			
		||||
					.then(function(hash) {
 | 
			
		||||
						return Enc.bufToUrlBase64(new Uint8Array(hash));
 | 
			
		||||
					})
 | 
			
		||||
					.then(function(hash64) {
 | 
			
		||||
						auth.dnsHost =
 | 
			
		||||
							dnsPrefix + '.' + auth.hostname.replace('*.', '');
 | 
			
		||||
 | 
			
		||||
						auth.dnsAuthorization = hash64;
 | 
			
		||||
						auth.keyAuthorizationDigest = hash64;
 | 
			
		||||
 | 
			
		||||
						if (zone) {
 | 
			
		||||
							auth.dnsZone = zone;
 | 
			
		||||
							auth.dnsPrefix = auth.dnsHost
 | 
			
		||||
								.replace(newZoneRegExp(zone), '')
 | 
			
		||||
								.replace(/\.$/, '');
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						return auth;
 | 
			
		||||
					});
 | 
			
		||||
		// Always calculate dnsAuthorization because we
 | 
			
		||||
		// may need to present to the user for confirmation / instruction
 | 
			
		||||
		// _as part of_ the decision making process
 | 
			
		||||
		return sha2
 | 
			
		||||
			.sum(256, resp.keyAuthorization)
 | 
			
		||||
			.then(function(hash) {
 | 
			
		||||
				return Enc.bufToUrlBase64(Uint8Array.from(hash));
 | 
			
		||||
			})
 | 
			
		||||
		).then(function(auths) {
 | 
			
		||||
			return auths.filter(Boolean);
 | 
			
		||||
		});
 | 
			
		||||
			.then(function(hash64) {
 | 
			
		||||
				resp.dnsHost = dnsPrefix + '.' + hostname; // .replace('*.', '');
 | 
			
		||||
 | 
			
		||||
				// deprecated
 | 
			
		||||
				resp.dnsAuthorization = hash64;
 | 
			
		||||
				// should use this instead
 | 
			
		||||
				resp.keyAuthorizationDigest = hash64;
 | 
			
		||||
 | 
			
		||||
				if (zone) {
 | 
			
		||||
					resp.dnsZone = zone;
 | 
			
		||||
					resp.dnsPrefix = resp.dnsHost
 | 
			
		||||
						.replace(newZoneRegExp(zone), '')
 | 
			
		||||
						.replace(/\.$/, '');
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return resp;
 | 
			
		||||
			});
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -583,6 +612,7 @@ ACME._setChallenges = function(me, options, order) {
 | 
			
		||||
	var claims = order._claims.slice(0);
 | 
			
		||||
	var valids = [];
 | 
			
		||||
	var auths = [];
 | 
			
		||||
	var placed = [];
 | 
			
		||||
	var USE_DNS = false;
 | 
			
		||||
	var DNS_DELAY = 0;
 | 
			
		||||
 | 
			
		||||
@ -618,6 +648,7 @@ ACME._setChallenges = function(me, options, order) {
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
				auths.push(selected);
 | 
			
		||||
				placed.push(selected);
 | 
			
		||||
				ACME._notify(me, options, 'challenge_select', {
 | 
			
		||||
					// API-locked
 | 
			
		||||
					altname: ACME._untame(
 | 
			
		||||
@ -651,10 +682,13 @@ ACME._setChallenges = function(me, options, order) {
 | 
			
		||||
	function waitAll() {
 | 
			
		||||
		//#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
 | 
			
		||||
		if (!DNS_DELAY || DNS_DELAY <= 0) {
 | 
			
		||||
			console.warn(
 | 
			
		||||
				'the given dns-01 challenge did not specify `propagationDelay`'
 | 
			
		||||
			);
 | 
			
		||||
			console.warn('the default of 5000ms will be used');
 | 
			
		||||
			if (!ACME._propagationDelayWarning) {
 | 
			
		||||
				console.warn(
 | 
			
		||||
					'warn: the given dns-01 challenge did not specify `propagationDelay`'
 | 
			
		||||
				);
 | 
			
		||||
				console.warn('warn: the default of 5000ms will be used');
 | 
			
		||||
				ACME._propagationDelayWarning = true;
 | 
			
		||||
			}
 | 
			
		||||
			DNS_DELAY = 5000;
 | 
			
		||||
		}
 | 
			
		||||
		return ACME._wait(DNS_DELAY);
 | 
			
		||||
@ -683,7 +717,22 @@ ACME._setChallenges = function(me, options, order) {
 | 
			
		||||
	// is so that we don't poison our own DNS cache with misses.
 | 
			
		||||
	return setNext()
 | 
			
		||||
		.then(waitAll)
 | 
			
		||||
		.then(checkNext);
 | 
			
		||||
		.then(checkNext)
 | 
			
		||||
		.catch(function(err) {
 | 
			
		||||
			if (!options.debug) {
 | 
			
		||||
				placed.forEach(function(ch) {
 | 
			
		||||
					options.challenges[ch.type]
 | 
			
		||||
						.remove({ challenge: ch })
 | 
			
		||||
						.catch(function(err) {
 | 
			
		||||
							err.action = 'challenge_remove';
 | 
			
		||||
							err.altname = ch.altname;
 | 
			
		||||
							err.type = ch.type;
 | 
			
		||||
							ACME._notify(me, options, 'error', err);
 | 
			
		||||
						});
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			throw err;
 | 
			
		||||
		});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ACME._normalizePresenters = function(me, options, presenters) {
 | 
			
		||||
@ -1283,40 +1332,6 @@ ACME._prnd = function(n) {
 | 
			
		||||
ACME._toHex = function(pair) {
 | 
			
		||||
	return parseInt(pair, 10).toString(16);
 | 
			
		||||
};
 | 
			
		||||
ACME._removeChallenge = function(me, options, auth) {
 | 
			
		||||
	var challengers = options.challenges || {};
 | 
			
		||||
	var ch = auth.challenge;
 | 
			
		||||
	var removeChallenge = challengers[ch.type] && challengers[ch.type].remove;
 | 
			
		||||
	if (!removeChallenge) {
 | 
			
		||||
		throw new Error('challenge plugin is missing remove()');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO normalize, warn, and just use promises
 | 
			
		||||
	if (1 === removeChallenge.length) {
 | 
			
		||||
		return Promise.resolve(removeChallenge(auth)).then(
 | 
			
		||||
			function() {},
 | 
			
		||||
			function(e) {
 | 
			
		||||
				console.error('Error during remove challenge:');
 | 
			
		||||
				console.error(e);
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
	} else if (2 === removeChallenge.length) {
 | 
			
		||||
		return new Promise(function(resolve) {
 | 
			
		||||
			removeChallenge(auth, function(err) {
 | 
			
		||||
				resolve();
 | 
			
		||||
				if (err) {
 | 
			
		||||
					console.error('Error during remove challenge:');
 | 
			
		||||
					console.error(err);
 | 
			
		||||
				}
 | 
			
		||||
				return err;
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		throw new Error(
 | 
			
		||||
			"Bad function signature for '" + auth.type + "' challenge.remove()"
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ACME._depInit = function(me, presenter) {
 | 
			
		||||
	if ('function' !== typeof presenter.init) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										111
									
								
								tests/compute-authorization-response.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								tests/compute-authorization-response.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,111 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var ACME = require('../');
 | 
			
		||||
var accountKey = require('../fixtures/account.jwk.json').private;
 | 
			
		||||
 | 
			
		||||
var authorization = {
 | 
			
		||||
	identifier: {
 | 
			
		||||
		type: 'dns',
 | 
			
		||||
		value: 'example.com'
 | 
			
		||||
	},
 | 
			
		||||
	status: 'pending',
 | 
			
		||||
	expires: '2018-04-25T00:23:57Z',
 | 
			
		||||
	challenges: [
 | 
			
		||||
		{
 | 
			
		||||
			type: 'dns-01',
 | 
			
		||||
			status: 'pending',
 | 
			
		||||
			url:
 | 
			
		||||
				'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755342',
 | 
			
		||||
			token: 'LZdlUiZ-kWPs6q5WTmQFYQHZKpz9szn2vxEUu0XhyyM'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			type: 'http-01',
 | 
			
		||||
			status: 'pending',
 | 
			
		||||
			url:
 | 
			
		||||
				'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755343',
 | 
			
		||||
			token: '1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU'
 | 
			
		||||
		}
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
var expectedChallengeUrl =
 | 
			
		||||
	'http://example.com/.well-known/acme-challenge/1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU';
 | 
			
		||||
var expectedKeyAuth =
 | 
			
		||||
	'1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU.UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs';
 | 
			
		||||
var expectedKeyAuthDigest = 'iQiMcQUDiAeD0TJV1RHJuGnI5D2-PuSpxKz9JqUaZ2M';
 | 
			
		||||
var expectedDnsHost = '_test-challenge.example.com';
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	console.info('\n[Test] computing challenge authorizatin responses');
 | 
			
		||||
	var challenges = authorization.challenges.slice(0);
 | 
			
		||||
 | 
			
		||||
	function next() {
 | 
			
		||||
		var ch = challenges.shift();
 | 
			
		||||
		if (!ch) {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var hostname = authorization.identifier.value;
 | 
			
		||||
		return ACME.computeChallenge({
 | 
			
		||||
			accountKey: accountKey,
 | 
			
		||||
			hostname: hostname,
 | 
			
		||||
			challenge: ch,
 | 
			
		||||
			dnsPrefix: '_test-challenge'
 | 
			
		||||
		})
 | 
			
		||||
			.then(function(auth) {
 | 
			
		||||
				if ('dns-01' === ch.type) {
 | 
			
		||||
					if (auth.keyAuthorizationDigest !== expectedKeyAuthDigest) {
 | 
			
		||||
						console.error('[keyAuthorizationDigest]');
 | 
			
		||||
						console.error(auth.keyAuthorizationDigest);
 | 
			
		||||
						console.error(expectedKeyAuthDigest);
 | 
			
		||||
						throw new Error('bad keyAuthDigest');
 | 
			
		||||
					}
 | 
			
		||||
					if (auth.dnsHost !== expectedDnsHost) {
 | 
			
		||||
						console.error('[dnsHost]');
 | 
			
		||||
						console.error(auth.dnsHost);
 | 
			
		||||
						console.error(expectedDnsHost);
 | 
			
		||||
						throw new Error('bad dnsHost');
 | 
			
		||||
					}
 | 
			
		||||
				} else if ('http-01' === ch.type) {
 | 
			
		||||
					if (auth.challengeUrl !== expectedChallengeUrl) {
 | 
			
		||||
						console.error('[challengeUrl]');
 | 
			
		||||
						console.error(auth.challengeUrl);
 | 
			
		||||
						console.error(expectedChallengeUrl);
 | 
			
		||||
						throw new Error('bad challengeUrl');
 | 
			
		||||
					}
 | 
			
		||||
					if (auth.challengeUrl !== expectedChallengeUrl) {
 | 
			
		||||
						console.error('[keyAuthorization]');
 | 
			
		||||
						console.error(auth.keyAuthorization);
 | 
			
		||||
						console.error(expectedKeyAuth);
 | 
			
		||||
						throw new Error('bad keyAuth');
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					throw new Error('bad authorization inputs');
 | 
			
		||||
				}
 | 
			
		||||
				console.info('PASS', hostname, ch.type);
 | 
			
		||||
				return next();
 | 
			
		||||
			})
 | 
			
		||||
			.catch(function(err) {
 | 
			
		||||
				err.message =
 | 
			
		||||
					'Error computing ' +
 | 
			
		||||
					ch.type +
 | 
			
		||||
					' for ' +
 | 
			
		||||
					hostname +
 | 
			
		||||
					':' +
 | 
			
		||||
					err.message;
 | 
			
		||||
				throw err;
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return next();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = function() {
 | 
			
		||||
	return main(authorization)
 | 
			
		||||
		.then(function() {
 | 
			
		||||
			console.info('PASS');
 | 
			
		||||
		})
 | 
			
		||||
		.catch(function(err) {
 | 
			
		||||
			console.error(err.stack);
 | 
			
		||||
			process.exit(1);
 | 
			
		||||
		});
 | 
			
		||||
};
 | 
			
		||||
@ -35,55 +35,46 @@ var tests = [
 | 
			
		||||
	'----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n'
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function formatPemChain(str) {
 | 
			
		||||
	return (
 | 
			
		||||
		str
 | 
			
		||||
			.trim()
 | 
			
		||||
			.replace(/[\r\n]+/g, '\n')
 | 
			
		||||
			.replace(/\-\n\-/g, '-\n\n-') + '\n'
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
function splitPemChain(str) {
 | 
			
		||||
	return str
 | 
			
		||||
		.trim()
 | 
			
		||||
		.split(/[\r\n]{2,}/g)
 | 
			
		||||
		.map(function(str) {
 | 
			
		||||
			return str + '\n';
 | 
			
		||||
		});
 | 
			
		||||
}
 | 
			
		||||
var ACME = require('../');
 | 
			
		||||
 | 
			
		||||
tests.forEach(function(str) {
 | 
			
		||||
	var actual = formatPemChain(str);
 | 
			
		||||
	if (expected !== actual) {
 | 
			
		||||
		console.error('input:   ', JSON.stringify(str));
 | 
			
		||||
		console.error('expected:', JSON.stringify(expected));
 | 
			
		||||
		console.error('actual:  ', JSON.stringify(actual));
 | 
			
		||||
		throw new Error('did not pass');
 | 
			
		||||
module.exports = function() {
 | 
			
		||||
	console.info('\n[Test] can split and format PEM chain properly');
 | 
			
		||||
 | 
			
		||||
	tests.forEach(function(str) {
 | 
			
		||||
		var actual = ACME.formatPemChain(str);
 | 
			
		||||
		if (expected !== actual) {
 | 
			
		||||
			console.error('input:   ', JSON.stringify(str));
 | 
			
		||||
			console.error('expected:', JSON.stringify(expected));
 | 
			
		||||
			console.error('actual:  ', JSON.stringify(actual));
 | 
			
		||||
			throw new Error('did not pass');
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (
 | 
			
		||||
		'----\nxxxx\nyyyy\n----\n' !==
 | 
			
		||||
		ACME.formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n')
 | 
			
		||||
	) {
 | 
			
		||||
		throw new Error('Not proper for single cert in chain');
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (
 | 
			
		||||
	'----\nxxxx\nyyyy\n----\n' !==
 | 
			
		||||
	formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n')
 | 
			
		||||
) {
 | 
			
		||||
	throw new Error('Not proper for single cert in chain');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (
 | 
			
		||||
	'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !==
 | 
			
		||||
	formatPemChain(
 | 
			
		||||
		'\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n'
 | 
			
		||||
	)
 | 
			
		||||
) {
 | 
			
		||||
	throw new Error('Not proper for three certs in chain');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
splitPemChain(
 | 
			
		||||
	'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n'
 | 
			
		||||
).forEach(function(str) {
 | 
			
		||||
	if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) {
 | 
			
		||||
		throw new Error('bad thingy');
 | 
			
		||||
	if (
 | 
			
		||||
		'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !==
 | 
			
		||||
		ACME.formatPemChain(
 | 
			
		||||
			'\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n'
 | 
			
		||||
		)
 | 
			
		||||
	) {
 | 
			
		||||
		throw new Error('Not proper for three certs in chain');
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
console.info('PASS');
 | 
			
		||||
	ACME.splitPemChain(
 | 
			
		||||
		'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n'
 | 
			
		||||
	).forEach(function(str) {
 | 
			
		||||
		if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) {
 | 
			
		||||
			throw new Error('bad thingy');
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	console.info('PASS');
 | 
			
		||||
 | 
			
		||||
  return Promise.resolve();
 | 
			
		||||
};
 | 
			
		||||
@ -1,15 +1,27 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
async function run() {
 | 
			
		||||
module.exports = async function() {
 | 
			
		||||
	console.log('[Test] can generate, export, and import key');
 | 
			
		||||
	var Keypairs = require('@root/keypairs');
 | 
			
		||||
 | 
			
		||||
	var certKeypair = await Keypairs.generate({ kty: 'RSA' });
 | 
			
		||||
	console.log(certKeypair);
 | 
			
		||||
	//console.log(certKeypair);
 | 
			
		||||
	var pem = await Keypairs.export({
 | 
			
		||||
		jwk: certKeypair.private,
 | 
			
		||||
		encoding: 'pem'
 | 
			
		||||
	});
 | 
			
		||||
	console.log(pem);
 | 
			
		||||
}
 | 
			
		||||
	var jwk = await Keypairs.import({
 | 
			
		||||
		pem: pem
 | 
			
		||||
	});
 | 
			
		||||
	['kty', 'd', 'n', 'e'].forEach(function(k) {
 | 
			
		||||
		if (!jwk[k] || jwk[k] !== certKeypair.private[k]) {
 | 
			
		||||
			throw new Error('bad export/import');
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	//console.log(pem);
 | 
			
		||||
	console.log('PASS');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
run();
 | 
			
		||||
if (require.main === module) {
 | 
			
		||||
	module.exports();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										245
									
								
								tests/index.js
									
									
									
									
									
								
							
							
						
						
									
										245
									
								
								tests/index.js
									
									
									
									
									
								
							@ -1,245 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
require('dotenv').config();
 | 
			
		||||
 | 
			
		||||
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');
 | 
			
		||||
 | 
			
		||||
// 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;
 | 
			
		||||
 | 
			
		||||
var acme = ACME.create({
 | 
			
		||||
	// debug: true
 | 
			
		||||
	maintainerEmail: config.email,
 | 
			
		||||
	notify: function(ev, params) {
 | 
			
		||||
		console.info(
 | 
			
		||||
			ev,
 | 
			
		||||
			params.subject || params.altname || params.domain,
 | 
			
		||||
			params.status
 | 
			
		||||
		);
 | 
			
		||||
		if ('error' === ev) {
 | 
			
		||||
			console.error(params);
 | 
			
		||||
			console.error(params.error);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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 });
 | 
			
		||||
	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;
 | 
			
		||||
		return Promise.resolve(tos);
 | 
			
		||||
	}
 | 
			
		||||
	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() {
 | 
			
		||||
		// Now try RSA + EC
 | 
			
		||||
		rnd = random();
 | 
			
		||||
		return happyPath('RSA', 'EC', rnd).then(function() {
 | 
			
		||||
			console.info('success');
 | 
			
		||||
		});
 | 
			
		||||
	})
 | 
			
		||||
	.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) + '例'
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										253
									
								
								tests/issue-certificates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								tests/issue-certificates.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,253 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
require('dotenv').config();
 | 
			
		||||
 | 
			
		||||
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');
 | 
			
		||||
 | 
			
		||||
// 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,
 | 
			
		||||
		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 });
 | 
			
		||||
		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;
 | 
			
		||||
			return Promise.resolve(tos);
 | 
			
		||||
		}
 | 
			
		||||
		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) + '例'
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										27
									
								
								utils.js
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								utils.js
									
									
									
									
									
								
							@ -122,29 +122,26 @@ U._setNonce = function(me, nonce) {
 | 
			
		||||
	me._nonces.unshift({ nonce: nonce, createdAt: Date.now() });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
U._importKeypair = function(me, kp) {
 | 
			
		||||
	var jwk = kp.privateKeyJwk;
 | 
			
		||||
	if (kp.kty) {
 | 
			
		||||
		jwk = kp;
 | 
			
		||||
		kp = {};
 | 
			
		||||
	}
 | 
			
		||||
	var pub;
 | 
			
		||||
U._importKeypair = function(me, key) {
 | 
			
		||||
	var p;
 | 
			
		||||
	if (jwk) {
 | 
			
		||||
	var pub;
 | 
			
		||||
 | 
			
		||||
	if (key && key.kty) {
 | 
			
		||||
		// nix the browser jwk extras
 | 
			
		||||
		jwk.key_ops = undefined;
 | 
			
		||||
		jwk.ext = undefined;
 | 
			
		||||
		pub = Keypairs.neuter({ jwk: jwk });
 | 
			
		||||
		key.key_ops = undefined;
 | 
			
		||||
		key.ext = undefined;
 | 
			
		||||
		pub = Keypairs.neuter({ jwk: key });
 | 
			
		||||
		p = Promise.resolve({
 | 
			
		||||
			private: jwk,
 | 
			
		||||
			private: key,
 | 
			
		||||
			public: pub
 | 
			
		||||
		});
 | 
			
		||||
	} else if ('string' === typeof key) {
 | 
			
		||||
		p = Keypairs.import({ pem: key });
 | 
			
		||||
	} else {
 | 
			
		||||
		p = Keypairs.import({ pem: kp.privateKeyPem });
 | 
			
		||||
		throw new Error('no private key given');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return p.then(function(pair) {
 | 
			
		||||
		kp.privateKeyJwk = pair.private;
 | 
			
		||||
		kp.publicKeyJwk = pair.public;
 | 
			
		||||
		if (pair.public.kid) {
 | 
			
		||||
			pair = JSON.parse(JSON.stringify(pair));
 | 
			
		||||
			delete pair.public.kid;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user