forked from root/acme.js
		
	backports: POST-as-GET, error handling, etc
This commit is contained in:
		
							parent
							
								
									d25fa6756c
								
							
						
					
					
						commit
						b1046222dc
					
				@ -152,11 +152,11 @@ ACME Accounts are key and device based, with an email address as a backup identi
 | 
			
		||||
A public account key must be registered before an SSL certificate can be requested.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
var accountPrivateKey;
 | 
			
		||||
var accountPrivateJwk;
 | 
			
		||||
var account;
 | 
			
		||||
 | 
			
		||||
Keypairs.generate({ kty: 'EC' }).then(function(pair) {
 | 
			
		||||
	accountPrivateKey = pair.private;
 | 
			
		||||
	accountPrivateJwk = pair.private;
 | 
			
		||||
 | 
			
		||||
	return acme.accounts
 | 
			
		||||
		.create({
 | 
			
		||||
@ -169,7 +169,7 @@ Keypairs.generate({ kty: 'EC' }).then(function(pair) {
 | 
			
		||||
					return Promise.resolve(tos);
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			accountKeypair: { privateKeyJwk: pair.private },
 | 
			
		||||
			accountKey: pair.private,
 | 
			
		||||
			subscriberEmail: $('.js-email-input').value
 | 
			
		||||
		})
 | 
			
		||||
		.then(function(_account) {
 | 
			
		||||
@ -224,7 +224,7 @@ var certinfo = await acme.certificates.create({
 | 
			
		||||
		return tos;
 | 
			
		||||
	},
 | 
			
		||||
	account: account,
 | 
			
		||||
	accountKeypair: { privateKeyJwk: accountPrivateKey },
 | 
			
		||||
	accountKey: accountPrivateJwk,
 | 
			
		||||
	csr: csr,
 | 
			
		||||
	domains: sortedDomains,
 | 
			
		||||
	challenges: challenges, // must be implemented
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										596
									
								
								acme.js
									
									
									
									
									
								
							
							
						
						
									
										596
									
								
								acme.js
									
									
									
									
									
								
							@ -13,7 +13,7 @@ var CSR = require('@root/csr');
 | 
			
		||||
var sha2 = require('@root/keypairs/lib/node/sha2.js');
 | 
			
		||||
var http = require('./lib/node/http.js');
 | 
			
		||||
 | 
			
		||||
var native = require('./native.js');
 | 
			
		||||
var native = require('./lib/native.js');
 | 
			
		||||
 | 
			
		||||
ACME.formatPemChain = function formatPemChain(str) {
 | 
			
		||||
	return (
 | 
			
		||||
@ -60,7 +60,7 @@ ACME.challengeTests = {
 | 
			
		||||
					"Got: '" +
 | 
			
		||||
					keyAuth +
 | 
			
		||||
					"'\n" +
 | 
			
		||||
					'See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4'
 | 
			
		||||
					'See https://git.rootprojects.org/root/acme.js/issues/4'
 | 
			
		||||
			);
 | 
			
		||||
			err.code = 'E_FAIL_DRY_CHALLENGE';
 | 
			
		||||
			throw err;
 | 
			
		||||
@ -87,7 +87,7 @@ ACME.challengeTests = {
 | 
			
		||||
					"' does not return '" +
 | 
			
		||||
					ch.dnsAuthorization +
 | 
			
		||||
					"'\n" +
 | 
			
		||||
					'See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4'
 | 
			
		||||
					'See https://git.rootprojects.org/root/acme.js/issues/4'
 | 
			
		||||
			);
 | 
			
		||||
			err.code = 'E_FAIL_DRY_CHALLENGE';
 | 
			
		||||
			throw err;
 | 
			
		||||
@ -96,12 +96,12 @@ ACME.challengeTests = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ACME._directory = function(me) {
 | 
			
		||||
	// GET-as-GET ok
 | 
			
		||||
	// TODO cache the directory URL
 | 
			
		||||
 | 
			
		||||
	// GET-as-GET ok
 | 
			
		||||
	return me.request({ method: 'GET', url: me.directoryUrl, json: true });
 | 
			
		||||
};
 | 
			
		||||
ACME._getNonce = function(me) {
 | 
			
		||||
	// GET-as-GET, HEAD-as-HEAD ok
 | 
			
		||||
	var nonce;
 | 
			
		||||
	while (true) {
 | 
			
		||||
		nonce = me._nonces.shift();
 | 
			
		||||
@ -117,11 +117,14 @@ ACME._getNonce = function(me) {
 | 
			
		||||
	if (nonce) {
 | 
			
		||||
		return Promise.resolve(nonce.nonce);
 | 
			
		||||
	}
 | 
			
		||||
	return me
 | 
			
		||||
		.request({ method: 'HEAD', url: me._directoryUrls.newNonce })
 | 
			
		||||
		.then(function(resp) {
 | 
			
		||||
			return resp.headers['replay-nonce'];
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	// HEAD-as-HEAD ok
 | 
			
		||||
	return ACME._request(me, {
 | 
			
		||||
		method: 'HEAD',
 | 
			
		||||
		url: me._directoryUrls.newNonce
 | 
			
		||||
	}).then(function(resp) {
 | 
			
		||||
		return resp.headers['replay-nonce'];
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
ACME._setNonce = function(me, nonce) {
 | 
			
		||||
	me._nonces.unshift({ nonce: nonce, createdAt: Date.now() });
 | 
			
		||||
@ -147,149 +150,115 @@ ACME._setNonce = function(me, nonce) {
 | 
			
		||||
 }
 | 
			
		||||
*/
 | 
			
		||||
ACME._registerAccount = function(me, options) {
 | 
			
		||||
	if (me.debug) {
 | 
			
		||||
		console.debug('[acme-v2] accounts.create');
 | 
			
		||||
	}
 | 
			
		||||
	//#console.debug('[ACME.js] accounts.create');
 | 
			
		||||
 | 
			
		||||
	return new Promise(function(resolve, reject) {
 | 
			
		||||
		function agree(tosUrl) {
 | 
			
		||||
			var err;
 | 
			
		||||
			if (me._tos !== tosUrl) {
 | 
			
		||||
				err = new Error(
 | 
			
		||||
					"You must agree to the ToS at '" + me._tos + "'"
 | 
			
		||||
				);
 | 
			
		||||
				err.code = 'E_AGREE_TOS';
 | 
			
		||||
				reject(err);
 | 
			
		||||
				return;
 | 
			
		||||
	function agree(tosUrl) {
 | 
			
		||||
		var err;
 | 
			
		||||
		if (me._tos !== tosUrl) {
 | 
			
		||||
			err = new Error("You must agree to the ToS at '" + me._tos + "'");
 | 
			
		||||
			err.code = 'E_AGREE_TOS';
 | 
			
		||||
			throw err;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return ACME._importKeypair(
 | 
			
		||||
			me,
 | 
			
		||||
			options.accountKey || options.accountKeypair
 | 
			
		||||
		).then(function(pair) {
 | 
			
		||||
			var contact;
 | 
			
		||||
			if (options.contact) {
 | 
			
		||||
				contact = options.contact.slice(0);
 | 
			
		||||
			} else if (options.subscriberEmail || options.email) {
 | 
			
		||||
				contact = [
 | 
			
		||||
					'mailto:' + (options.subscriberEmail || options.email)
 | 
			
		||||
				];
 | 
			
		||||
			}
 | 
			
		||||
			var accountRequest = {
 | 
			
		||||
				termsOfServiceAgreed: tosUrl === me._tos,
 | 
			
		||||
				onlyReturnExisting: false,
 | 
			
		||||
				contact: contact
 | 
			
		||||
			};
 | 
			
		||||
			var pExt;
 | 
			
		||||
			if (options.externalAccount) {
 | 
			
		||||
				pExt = me.Keypairs.signJws({
 | 
			
		||||
					// TODO is HMAC the standard, or is this arbitrary?
 | 
			
		||||
					secret: options.externalAccount.secret,
 | 
			
		||||
					protected: {
 | 
			
		||||
						alg: options.externalAccount.alg || 'HS256',
 | 
			
		||||
						kid: options.externalAccount.id,
 | 
			
		||||
						url: me._directoryUrls.newAccount
 | 
			
		||||
					},
 | 
			
		||||
					payload: Enc.strToBuf(JSON.stringify(pair.public))
 | 
			
		||||
				}).then(function(jws) {
 | 
			
		||||
					accountRequest.externalAccountBinding = jws;
 | 
			
		||||
					return accountRequest;
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				pExt = Promise.resolve(accountRequest);
 | 
			
		||||
			}
 | 
			
		||||
			return pExt.then(function(accountRequest) {
 | 
			
		||||
				var payload = JSON.stringify(accountRequest);
 | 
			
		||||
				return ACME._jwsRequest(me, {
 | 
			
		||||
					options: options,
 | 
			
		||||
					url: me._directoryUrls.newAccount,
 | 
			
		||||
					protected: { kid: false, jwk: pair.public },
 | 
			
		||||
					payload: Enc.strToBuf(payload)
 | 
			
		||||
				}).then(function(resp) {
 | 
			
		||||
					var account = resp.body;
 | 
			
		||||
 | 
			
		||||
			return ACME._importKeypair(me, options.accountKeypair).then(
 | 
			
		||||
				function(pair) {
 | 
			
		||||
					var contact;
 | 
			
		||||
					if (options.contact) {
 | 
			
		||||
						contact = options.contact.slice(0);
 | 
			
		||||
					} else if (options.subscriberEmail || options.email) {
 | 
			
		||||
						contact = [
 | 
			
		||||
							'mailto:' +
 | 
			
		||||
								(options.subscriberEmail || options.email)
 | 
			
		||||
						];
 | 
			
		||||
					if (resp.statusCode < 200 || resp.statusCode >= 300) {
 | 
			
		||||
						if ('string' !== typeof account) {
 | 
			
		||||
							account = JSON.stringify(account);
 | 
			
		||||
						}
 | 
			
		||||
						throw new Error(
 | 
			
		||||
							'account error: ' +
 | 
			
		||||
								resp.statusCode +
 | 
			
		||||
								' ' +
 | 
			
		||||
								account +
 | 
			
		||||
								'\n' +
 | 
			
		||||
								payload
 | 
			
		||||
						);
 | 
			
		||||
					}
 | 
			
		||||
					var accountRequest = {
 | 
			
		||||
						termsOfServiceAgreed: tosUrl === me._tos,
 | 
			
		||||
						onlyReturnExisting: false,
 | 
			
		||||
						contact: contact
 | 
			
		||||
					};
 | 
			
		||||
					var pExt;
 | 
			
		||||
					if (options.externalAccount) {
 | 
			
		||||
						pExt = me.Keypairs.signJws({
 | 
			
		||||
							// TODO is HMAC the standard, or is this arbitrary?
 | 
			
		||||
							secret: options.externalAccount.secret,
 | 
			
		||||
							protected: {
 | 
			
		||||
								alg: options.externalAccount.alg || 'HS256',
 | 
			
		||||
								kid: options.externalAccount.id,
 | 
			
		||||
								url: me._directoryUrls.newAccount
 | 
			
		||||
							},
 | 
			
		||||
							payload: Enc.strToBuf(JSON.stringify(pair.public))
 | 
			
		||||
						}).then(function(jws) {
 | 
			
		||||
							accountRequest.externalAccountBinding = jws;
 | 
			
		||||
							return accountRequest;
 | 
			
		||||
						});
 | 
			
		||||
					} else {
 | 
			
		||||
						pExt = Promise.resolve(accountRequest);
 | 
			
		||||
					}
 | 
			
		||||
					return pExt.then(function(accountRequest) {
 | 
			
		||||
						var payload = JSON.stringify(accountRequest);
 | 
			
		||||
						return ACME._jwsRequest(me, {
 | 
			
		||||
							options: options,
 | 
			
		||||
							url: me._directoryUrls.newAccount,
 | 
			
		||||
							protected: { kid: false, jwk: pair.public },
 | 
			
		||||
							payload: Enc.strToBuf(payload)
 | 
			
		||||
						})
 | 
			
		||||
							.then(function(resp) {
 | 
			
		||||
								var account = resp.body;
 | 
			
		||||
 | 
			
		||||
								if (
 | 
			
		||||
									resp.statusCode < 200 ||
 | 
			
		||||
									resp.statusCode >= 300
 | 
			
		||||
								) {
 | 
			
		||||
									if ('string' !== typeof account) {
 | 
			
		||||
										account = JSON.stringify(account);
 | 
			
		||||
									}
 | 
			
		||||
									throw new Error(
 | 
			
		||||
										'account error: ' +
 | 
			
		||||
											resp.statusCode +
 | 
			
		||||
											' ' +
 | 
			
		||||
											account +
 | 
			
		||||
											'\n' +
 | 
			
		||||
											JSON.stringify(accountRequest)
 | 
			
		||||
									);
 | 
			
		||||
								}
 | 
			
		||||
					var location = resp.headers.location;
 | 
			
		||||
					// the account id url
 | 
			
		||||
					options._kid = location;
 | 
			
		||||
					//#console.debug('[DEBUG] new account location:');
 | 
			
		||||
					//#console.debug(location);
 | 
			
		||||
					//#console.debug(resp);
 | 
			
		||||
 | 
			
		||||
								var location = resp.headers.location;
 | 
			
		||||
								// the account id url
 | 
			
		||||
								options._kid = location;
 | 
			
		||||
								if (me.debug) {
 | 
			
		||||
									console.debug(
 | 
			
		||||
										'[DEBUG] new account location:'
 | 
			
		||||
									);
 | 
			
		||||
								}
 | 
			
		||||
								if (me.debug) {
 | 
			
		||||
									console.debug(location);
 | 
			
		||||
								}
 | 
			
		||||
								if (me.debug) {
 | 
			
		||||
									console.debug(resp);
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								/*
 | 
			
		||||
					/*
 | 
			
		||||
            {
 | 
			
		||||
              contact: ["mailto:jon@example.com"],
 | 
			
		||||
              orders: "https://some-url",
 | 
			
		||||
              status: 'valid'
 | 
			
		||||
            }
 | 
			
		||||
            */
 | 
			
		||||
								if (!account) {
 | 
			
		||||
									account = { _emptyResponse: true };
 | 
			
		||||
								}
 | 
			
		||||
								// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
 | 
			
		||||
								if (!account.key) {
 | 
			
		||||
									account.key = {};
 | 
			
		||||
								}
 | 
			
		||||
								account.key.kid = options._kid;
 | 
			
		||||
								return account;
 | 
			
		||||
							})
 | 
			
		||||
							.then(resolve, reject);
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (me.debug) {
 | 
			
		||||
			console.debug('[acme-v2] agreeToTerms');
 | 
			
		||||
		}
 | 
			
		||||
		if (1 === options.agreeToTerms.length) {
 | 
			
		||||
			// newer promise API
 | 
			
		||||
			return Promise.resolve(options.agreeToTerms(me._tos)).then(
 | 
			
		||||
				agree,
 | 
			
		||||
				reject
 | 
			
		||||
			);
 | 
			
		||||
		} else if (2 === options.agreeToTerms.length) {
 | 
			
		||||
			// backwards compat cb API
 | 
			
		||||
			return options.agreeToTerms(me._tos, function(err, tosUrl) {
 | 
			
		||||
				if (!err) {
 | 
			
		||||
					agree(tosUrl);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				reject(err);
 | 
			
		||||
					if (!account) {
 | 
			
		||||
						account = { _emptyResponse: true };
 | 
			
		||||
					}
 | 
			
		||||
					// https://git.rootprojects.org/root/acme.js/issues/8
 | 
			
		||||
					if (!account.key) {
 | 
			
		||||
						account.key = {};
 | 
			
		||||
					}
 | 
			
		||||
					account.key.kid = options._kid;
 | 
			
		||||
					return account;
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			reject(
 | 
			
		||||
				new Error(
 | 
			
		||||
					'agreeToTerms has incorrect function signature.' +
 | 
			
		||||
						' Should be fn(tos) { return Promise<tos>; }'
 | 
			
		||||
				)
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return Promise.resolve()
 | 
			
		||||
		.then(function() {
 | 
			
		||||
			//#console.debug('[ACME.js] agreeToTerms');
 | 
			
		||||
			var agreeToTerms = options.agreeToTerms;
 | 
			
		||||
			if (true === agreeToTerms) {
 | 
			
		||||
				agreeToTerms = function(tos) {
 | 
			
		||||
					return tos;
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
			return agreeToTerms(me._tos);
 | 
			
		||||
		})
 | 
			
		||||
		.then(agree);
 | 
			
		||||
};
 | 
			
		||||
/*
 | 
			
		||||
 POST /acme/new-order HTTP/1.1
 | 
			
		||||
@ -312,14 +281,11 @@ ACME._registerAccount = function(me, options) {
 | 
			
		||||
 }
 | 
			
		||||
*/
 | 
			
		||||
ACME._getChallenges = function(me, options, authUrl) {
 | 
			
		||||
	if (me.debug) {
 | 
			
		||||
		console.debug('\n[DEBUG] getChallenges\n');
 | 
			
		||||
	}
 | 
			
		||||
	// TODO POST-as-GET
 | 
			
		||||
	//#console.debug('\n[DEBUG] getChallenges\n');
 | 
			
		||||
 | 
			
		||||
	return ACME._jwsRequest(me, {
 | 
			
		||||
		options: options,
 | 
			
		||||
		protected: {},
 | 
			
		||||
		protected: { kid: options._kid },
 | 
			
		||||
		payload: '',
 | 
			
		||||
		url: authUrl
 | 
			
		||||
	}).then(function(resp) {
 | 
			
		||||
@ -361,13 +327,14 @@ ACME._testChallenges = function(me, options) {
 | 
			
		||||
 | 
			
		||||
	// memoized so that it doesn't run until it's first called
 | 
			
		||||
	var getThumbnail = function() {
 | 
			
		||||
		var thumbPromise = ACME._importKeypair(me, options.accountKeypair).then(
 | 
			
		||||
			function(pair) {
 | 
			
		||||
				return me.Keypairs.thumbprint({
 | 
			
		||||
					jwk: pair.public
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
		var thumbPromise = ACME._importKeypair(
 | 
			
		||||
			me,
 | 
			
		||||
			options.accountKey || options.accountKeypair
 | 
			
		||||
		).then(function(pair) {
 | 
			
		||||
			return me.Keypairs.thumbprint({
 | 
			
		||||
				jwk: pair.public
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
		getThumbnail = function() {
 | 
			
		||||
			return thumbPromise;
 | 
			
		||||
		};
 | 
			
		||||
@ -624,9 +591,7 @@ ACME._postChallenge = function(me, options, auth) {
 | 
			
		||||
   }
 | 
			
		||||
   */
 | 
			
		||||
	function deactivate() {
 | 
			
		||||
		if (me.debug) {
 | 
			
		||||
			console.debug('[acme-v2.js] deactivate:');
 | 
			
		||||
		}
 | 
			
		||||
		//#console.debug('[ACME.js] deactivate:');
 | 
			
		||||
		return ACME._jwsRequest(me, {
 | 
			
		||||
			options: options,
 | 
			
		||||
			url: ch.url,
 | 
			
		||||
@ -647,7 +612,7 @@ ACME._postChallenge = function(me, options, auth) {
 | 
			
		||||
		if (count >= MAX_POLL) {
 | 
			
		||||
			return Promise.reject(
 | 
			
		||||
				new Error(
 | 
			
		||||
					"[acme-v2] stuck in bad pending/processing state for '" +
 | 
			
		||||
					"[ACME.js] stuck in bad pending/processing state for '" +
 | 
			
		||||
						altname +
 | 
			
		||||
						"'"
 | 
			
		||||
				)
 | 
			
		||||
@ -656,77 +621,77 @@ ACME._postChallenge = function(me, options, auth) {
 | 
			
		||||
 | 
			
		||||
		count += 1;
 | 
			
		||||
 | 
			
		||||
		if (me.debug) {
 | 
			
		||||
			console.debug('\n[DEBUG] statusChallenge\n');
 | 
			
		||||
		}
 | 
			
		||||
		// TODO POST-as-GET
 | 
			
		||||
		return me
 | 
			
		||||
			.request({ method: 'GET', url: ch.url, json: true })
 | 
			
		||||
			.then(function(resp) {
 | 
			
		||||
				if ('processing' === resp.body.status) {
 | 
			
		||||
					if (me.debug) {
 | 
			
		||||
						console.debug('poll: again', ch.url);
 | 
			
		||||
					}
 | 
			
		||||
					return ACME._wait(RETRY_INTERVAL).then(pollStatus);
 | 
			
		||||
		//#console.debug('\n[DEBUG] statusChallenge\n');
 | 
			
		||||
		// POST-as-GET
 | 
			
		||||
		return ACME._jwsRequest(me, {
 | 
			
		||||
			options: options,
 | 
			
		||||
			url: ch.url,
 | 
			
		||||
			protected: { kid: options._kid },
 | 
			
		||||
			payload: Enc.binToBuf('')
 | 
			
		||||
		}).then(function(resp) {
 | 
			
		||||
			if ('processing' === resp.body.status) {
 | 
			
		||||
				if (me.debug) {
 | 
			
		||||
					console.debug('poll: again', ch.url);
 | 
			
		||||
				}
 | 
			
		||||
				return ACME._wait(RETRY_INTERVAL).then(pollStatus);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// This state should never occur
 | 
			
		||||
			if ('pending' === resp.body.status) {
 | 
			
		||||
				if (count >= MAX_PEND) {
 | 
			
		||||
					return ACME._wait(RETRY_INTERVAL)
 | 
			
		||||
						.then(deactivate)
 | 
			
		||||
						.then(respondToChallenge);
 | 
			
		||||
				}
 | 
			
		||||
				if (me.debug) {
 | 
			
		||||
					console.debug('poll: again', ch.url);
 | 
			
		||||
				}
 | 
			
		||||
				return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// REMOVE DNS records as soon as the state is non-processing
 | 
			
		||||
			try {
 | 
			
		||||
				ACME._removeChallenge(me, options, auth);
 | 
			
		||||
			} catch (e) {}
 | 
			
		||||
 | 
			
		||||
			if ('valid' === resp.body.status) {
 | 
			
		||||
				if (me.debug) {
 | 
			
		||||
					console.debug('poll: valid');
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// This state should never occur
 | 
			
		||||
				if ('pending' === resp.body.status) {
 | 
			
		||||
					if (count >= MAX_PEND) {
 | 
			
		||||
						return ACME._wait(RETRY_INTERVAL)
 | 
			
		||||
							.then(deactivate)
 | 
			
		||||
							.then(respondToChallenge);
 | 
			
		||||
					}
 | 
			
		||||
					if (me.debug) {
 | 
			
		||||
						console.debug('poll: again', ch.url);
 | 
			
		||||
					}
 | 
			
		||||
					return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
 | 
			
		||||
				}
 | 
			
		||||
				return resp.body;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				// REMOVE DNS records as soon as the state is non-processing
 | 
			
		||||
				try {
 | 
			
		||||
					ACME._removeChallenge(me, options, auth);
 | 
			
		||||
				} catch (e) {}
 | 
			
		||||
			var errmsg;
 | 
			
		||||
			if (!resp.body.status) {
 | 
			
		||||
				errmsg =
 | 
			
		||||
					"[ACME.js] (E_STATE_EMPTY) empty challenge state for '" +
 | 
			
		||||
					altname +
 | 
			
		||||
					"':" +
 | 
			
		||||
					JSON.stringify(resp.body);
 | 
			
		||||
			} else if ('invalid' === resp.body.status) {
 | 
			
		||||
				errmsg =
 | 
			
		||||
					"[ACME.js] (E_STATE_INVALID) challenge state for '" +
 | 
			
		||||
					altname +
 | 
			
		||||
					"': '" +
 | 
			
		||||
					//resp.body.status +
 | 
			
		||||
					JSON.stringify(resp.body) +
 | 
			
		||||
					"'";
 | 
			
		||||
			} else {
 | 
			
		||||
				errmsg =
 | 
			
		||||
					"[ACME.js] (E_STATE_UKN) challenge state for '" +
 | 
			
		||||
					altname +
 | 
			
		||||
					"': '" +
 | 
			
		||||
					resp.body.status +
 | 
			
		||||
					"'";
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				if ('valid' === resp.body.status) {
 | 
			
		||||
					if (me.debug) {
 | 
			
		||||
						console.debug('poll: valid');
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					return resp.body;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				var errmsg;
 | 
			
		||||
				if (!resp.body.status) {
 | 
			
		||||
					errmsg =
 | 
			
		||||
						"[acme-v2] (E_STATE_EMPTY) empty challenge state for '" +
 | 
			
		||||
						altname +
 | 
			
		||||
						"':";
 | 
			
		||||
				} else if ('invalid' === resp.body.status) {
 | 
			
		||||
					errmsg =
 | 
			
		||||
						"[acme-v2] (E_STATE_INVALID) challenge state for '" +
 | 
			
		||||
						altname +
 | 
			
		||||
						"': '" +
 | 
			
		||||
						//resp.body.status +
 | 
			
		||||
						JSON.stringify(resp.body) +
 | 
			
		||||
						"'";
 | 
			
		||||
				} else {
 | 
			
		||||
					errmsg =
 | 
			
		||||
						"[acme-v2] (E_STATE_UKN) challenge state for '" +
 | 
			
		||||
						altname +
 | 
			
		||||
						"': '" +
 | 
			
		||||
						resp.body.status +
 | 
			
		||||
						"'";
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return Promise.reject(new Error(errmsg));
 | 
			
		||||
			});
 | 
			
		||||
			return Promise.reject(new Error(errmsg));
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function respondToChallenge() {
 | 
			
		||||
		if (me.debug) {
 | 
			
		||||
			console.debug('[acme-v2.js] responding to accept challenge:');
 | 
			
		||||
		}
 | 
			
		||||
		//#console.debug('[ACME.js] responding to accept challenge:');
 | 
			
		||||
		return ACME._jwsRequest(me, {
 | 
			
		||||
			options: options,
 | 
			
		||||
			url: ch.url,
 | 
			
		||||
@ -787,9 +752,7 @@ ACME._finalizeOrder = function(me, options, validatedDomains) {
 | 
			
		||||
		var payload = JSON.stringify(body);
 | 
			
		||||
 | 
			
		||||
		function pollCert() {
 | 
			
		||||
			if (me.debug) {
 | 
			
		||||
				console.debug('[acme-v2.js] pollCert:');
 | 
			
		||||
			}
 | 
			
		||||
			//#console.debug('[ACME.js] pollCert:');
 | 
			
		||||
			return ACME._jwsRequest(me, {
 | 
			
		||||
				options: options,
 | 
			
		||||
				url: options._finalize,
 | 
			
		||||
@ -870,7 +833,7 @@ ACME._finalizeOrder = function(me, options, validatedDomains) {
 | 
			
		||||
								"'\n" +
 | 
			
		||||
								JSON.stringify(resp.body, null, 2) +
 | 
			
		||||
								'\n\n' +
 | 
			
		||||
								'Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js'
 | 
			
		||||
								'Please open an issue at https://git.rootprojects.org/root/acme.js'
 | 
			
		||||
						)
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
@ -889,7 +852,7 @@ ACME._finalizeOrder = function(me, options, validatedDomains) {
 | 
			
		||||
							"'\n" +
 | 
			
		||||
							JSON.stringify(resp.body, null, 2) +
 | 
			
		||||
							'\n\n' +
 | 
			
		||||
							'Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js'
 | 
			
		||||
							'Please open an issue at https://git.rootprojects.org/root/acme.js'
 | 
			
		||||
					)
 | 
			
		||||
				);
 | 
			
		||||
			});
 | 
			
		||||
@ -904,9 +867,7 @@ ACME._finalizeOrder = function(me, options, validatedDomains) {
 | 
			
		||||
// finalizeOrder
 | 
			
		||||
// getCertificate
 | 
			
		||||
ACME._getCertificate = function(me, options) {
 | 
			
		||||
	if (me.debug) {
 | 
			
		||||
		console.debug('[acme-v2] DEBUG get cert 1');
 | 
			
		||||
	}
 | 
			
		||||
	//#console.debug('[ACME.js] DEBUG get cert 1');
 | 
			
		||||
 | 
			
		||||
	// Prefer this order for efficiency:
 | 
			
		||||
	// * http-01 is the fasest
 | 
			
		||||
@ -1027,9 +988,8 @@ ACME._getCertificate = function(me, options) {
 | 
			
		||||
			return ACME._testChallenges(me, options);
 | 
			
		||||
		})
 | 
			
		||||
		.then(function() {
 | 
			
		||||
			if (me.debug) {
 | 
			
		||||
				console.debug('[acme-v2] certificates.create');
 | 
			
		||||
			}
 | 
			
		||||
			//#console.debug('[ACME.js] certificates.create');
 | 
			
		||||
 | 
			
		||||
			var certOrder = {
 | 
			
		||||
				// raw wildcard syntax MUST be used here
 | 
			
		||||
				identifiers: options.domains
 | 
			
		||||
@ -1084,7 +1044,7 @@ ACME._getCertificate = function(me, options) {
 | 
			
		||||
				if (!options._authorizations) {
 | 
			
		||||
					return Promise.reject(
 | 
			
		||||
						new Error(
 | 
			
		||||
							"[acme-v2.js] authorizations were not fetched for '" +
 | 
			
		||||
							"[ACME.js] authorizations were not fetched for '" +
 | 
			
		||||
								options.domains.join() +
 | 
			
		||||
								"':\n" +
 | 
			
		||||
								JSON.stringify(resp.body)
 | 
			
		||||
@ -1092,21 +1052,22 @@ ACME._getCertificate = function(me, options) {
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
				if (me.debug) {
 | 
			
		||||
					console.debug('[acme-v2] POST newOrder has authorizations');
 | 
			
		||||
					console.debug('[ACME.js] POST newOrder has authorizations');
 | 
			
		||||
				}
 | 
			
		||||
				setAuths = options._authorizations.slice(0);
 | 
			
		||||
 | 
			
		||||
				var accountKeyThumb;
 | 
			
		||||
				function setThumbnail() {
 | 
			
		||||
					return ACME._importKeypair(me, options.accountKeypair).then(
 | 
			
		||||
						function(pair) {
 | 
			
		||||
							return me.Keypairs.thumbprint({
 | 
			
		||||
								jwk: pair.public
 | 
			
		||||
							}).then(function(_thumb) {
 | 
			
		||||
								accountKeyThumb = _thumb;
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					);
 | 
			
		||||
					return ACME._importKeypair(
 | 
			
		||||
						me,
 | 
			
		||||
						options.accountKey || options.accountKeypair
 | 
			
		||||
					).then(function(pair) {
 | 
			
		||||
						return me.Keypairs.thumbprint({
 | 
			
		||||
							jwk: pair.public
 | 
			
		||||
						}).then(function(_thumb) {
 | 
			
		||||
							accountKeyThumb = _thumb;
 | 
			
		||||
						});
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				function setNext() {
 | 
			
		||||
@ -1219,40 +1180,34 @@ ACME._getCertificate = function(me, options) {
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				function retrieveCerts(order) {
 | 
			
		||||
					if (me.debug) {
 | 
			
		||||
						console.debug('acme-v2: order was finalized');
 | 
			
		||||
					}
 | 
			
		||||
					// TODO POST-as-GET
 | 
			
		||||
					return me
 | 
			
		||||
						.request({
 | 
			
		||||
							method: 'GET',
 | 
			
		||||
							url: options._certificate,
 | 
			
		||||
							json: true
 | 
			
		||||
						})
 | 
			
		||||
						.then(function(resp) {
 | 
			
		||||
							if (me.debug) {
 | 
			
		||||
								console.debug(
 | 
			
		||||
									'acme-v2: csr submitted and cert received:'
 | 
			
		||||
								);
 | 
			
		||||
							}
 | 
			
		||||
							// https://github.com/certbot/certbot/issues/5721
 | 
			
		||||
							var certsarr = ACME.splitPemChain(
 | 
			
		||||
								ACME.formatPemChain(resp.body || '')
 | 
			
		||||
							);
 | 
			
		||||
							//  cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
 | 
			
		||||
							var certs = {
 | 
			
		||||
								expires: order.expires,
 | 
			
		||||
								identifiers: order.identifiers,
 | 
			
		||||
								//, authorizations: order.authorizations
 | 
			
		||||
								cert: certsarr.shift(),
 | 
			
		||||
								//, privkey: privkeyPem
 | 
			
		||||
								chain: certsarr.join('\n')
 | 
			
		||||
							};
 | 
			
		||||
							if (me.debug) {
 | 
			
		||||
								console.debug(certs);
 | 
			
		||||
							}
 | 
			
		||||
							return certs;
 | 
			
		||||
						});
 | 
			
		||||
					//#console.debug('ACME.js: order was finalized');
 | 
			
		||||
 | 
			
		||||
					// POST-as-GET
 | 
			
		||||
					return ACME._jwsRequest(me, {
 | 
			
		||||
						options: options,
 | 
			
		||||
						url: options._certificate,
 | 
			
		||||
						protected: { kid: options._kid },
 | 
			
		||||
						payload: Enc.binToBuf(''),
 | 
			
		||||
						json: true
 | 
			
		||||
					}).then(function(resp) {
 | 
			
		||||
						//#console.debug('ACME.js: csr submitted and cert received:');
 | 
			
		||||
 | 
			
		||||
						// https://github.com/certbot/certbot/issues/5721
 | 
			
		||||
						var certsarr = ACME.splitPemChain(
 | 
			
		||||
							ACME.formatPemChain(resp.body || '')
 | 
			
		||||
						);
 | 
			
		||||
						//  cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
 | 
			
		||||
						var certs = {
 | 
			
		||||
							expires: order.expires,
 | 
			
		||||
							identifiers: order.identifiers,
 | 
			
		||||
							//, authorizations: order.authorizations
 | 
			
		||||
							cert: certsarr.shift(),
 | 
			
		||||
							//, privkey: privkeyPem
 | 
			
		||||
							chain: certsarr.join('\n')
 | 
			
		||||
						};
 | 
			
		||||
						//#console.debug(certs);
 | 
			
		||||
						return certs;
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// First we set each and every challenge
 | 
			
		||||
@ -1360,6 +1315,24 @@ ACME.create = function create(me) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
	me.orders = {
 | 
			
		||||
		// create + get challlenges
 | 
			
		||||
		request: function(options) {
 | 
			
		||||
			try {
 | 
			
		||||
				return ACME._createOrder(me, options);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				return Promise.reject(e);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		// set challenges, check challenges, finalize order, return order
 | 
			
		||||
		complete: function(options) {
 | 
			
		||||
			try {
 | 
			
		||||
				return ACME._finalizeOrder(me, options);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				return Promise.reject(e);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
	me.certificates = {
 | 
			
		||||
		create: function(options) {
 | 
			
		||||
			try {
 | 
			
		||||
@ -1384,20 +1357,31 @@ ACME._jwsRequest = function(me, bigopts) {
 | 
			
		||||
				bigopts.protected.kid = bigopts.options._kid;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// this will shasum the thumbnail the 2nd time
 | 
			
		||||
		return me.Keypairs.signJws({
 | 
			
		||||
			jwk: bigopts.options.accountKeypair.privateKeyJwk,
 | 
			
		||||
			jwk:
 | 
			
		||||
				bigopts.options.accountKey ||
 | 
			
		||||
				bigopts.options.accountKeypair.privateKeyJwk,
 | 
			
		||||
			protected: bigopts.protected,
 | 
			
		||||
			payload: bigopts.payload
 | 
			
		||||
		}).then(function(jws) {
 | 
			
		||||
			if (me.debug) {
 | 
			
		||||
				console.debug('[acme-v2] ' + bigopts.url + ':');
 | 
			
		||||
			}
 | 
			
		||||
			if (me.debug) {
 | 
			
		||||
				console.debug(jws);
 | 
			
		||||
			}
 | 
			
		||||
			return ACME._request(me, { url: bigopts.url, json: jws });
 | 
			
		||||
		});
 | 
			
		||||
		})
 | 
			
		||||
			.then(function(jws) {
 | 
			
		||||
				//#console.debug('[ACME.js] url: ' + bigopts.url + ':');
 | 
			
		||||
				//#console.debug(jws);
 | 
			
		||||
				return ACME._request(me, { url: bigopts.url, json: jws });
 | 
			
		||||
			})
 | 
			
		||||
			.catch(function(e) {
 | 
			
		||||
				if (/badNonce$/.test(e.urn)) {
 | 
			
		||||
					// retry badNonces
 | 
			
		||||
					var retryable = bigopts._retries >= 2;
 | 
			
		||||
					if (!retryable) {
 | 
			
		||||
						bigopts._retries = (bigopts._retries || 0) + 1;
 | 
			
		||||
						return ACME._jwsRequest(me, bigopts);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				throw e;
 | 
			
		||||
			});
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1414,13 +1398,41 @@ ACME._request = function(me, opts) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return me.request(opts).then(function(resp) {
 | 
			
		||||
		resp = resp.toJSON();
 | 
			
		||||
		if (resp.toJSON) {
 | 
			
		||||
			resp = resp.toJSON();
 | 
			
		||||
		}
 | 
			
		||||
		if (resp.headers['replay-nonce']) {
 | 
			
		||||
			ACME._setNonce(me, resp.headers['replay-nonce']);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var e;
 | 
			
		||||
		var err;
 | 
			
		||||
		if (resp.body) {
 | 
			
		||||
			err = resp.body.error;
 | 
			
		||||
			e = new Error('');
 | 
			
		||||
			if (400 === resp.body.status) {
 | 
			
		||||
				err = { type: resp.body.type, detail: resp.body.detail };
 | 
			
		||||
			}
 | 
			
		||||
			if (err) {
 | 
			
		||||
				e.status = resp.body.status;
 | 
			
		||||
				e.code = 'E_ACME';
 | 
			
		||||
				if (e.status) {
 | 
			
		||||
					e.message = '[' + e.status + '] ';
 | 
			
		||||
				}
 | 
			
		||||
				e.detail = err.detail;
 | 
			
		||||
				e.message += err.detail || JSON.stringify(err);
 | 
			
		||||
				e.urn = err.type;
 | 
			
		||||
				e.uri = resp.body.url;
 | 
			
		||||
				e._rawError = err;
 | 
			
		||||
				e._rawBody = resp.body;
 | 
			
		||||
				throw e;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return resp;
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// A very generic, swappable request lib
 | 
			
		||||
ACME._defaultRequest = function(opts) {
 | 
			
		||||
	// Note: normally we'd have to supply a User-Agent string, but not here in a browser
 | 
			
		||||
@ -1446,14 +1458,20 @@ ACME._defaultRequest = function(opts) {
 | 
			
		||||
 | 
			
		||||
ACME._importKeypair = function(me, kp) {
 | 
			
		||||
	var jwk = kp.privateKeyJwk;
 | 
			
		||||
	if (kp.kty) {
 | 
			
		||||
		jwk = kp;
 | 
			
		||||
		kp = {};
 | 
			
		||||
	}
 | 
			
		||||
	var pub;
 | 
			
		||||
	var p;
 | 
			
		||||
	if (jwk) {
 | 
			
		||||
		// nix the browser jwk extras
 | 
			
		||||
		jwk.key_ops = undefined;
 | 
			
		||||
		jwk.ext = undefined;
 | 
			
		||||
		pub = me.Keypairs.neuter({ jwk: jwk });
 | 
			
		||||
		p = Promise.resolve({
 | 
			
		||||
			private: jwk,
 | 
			
		||||
			public: me.Keypairs.neuter({ jwk: jwk })
 | 
			
		||||
			public: pub
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		p = me.Keypairs.import({ pem: kp.privateKeyPem });
 | 
			
		||||
@ -1617,3 +1635,5 @@ function pluckZone(zonenames, dnsHost) {
 | 
			
		||||
			return b.length - a.length;
 | 
			
		||||
		})[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO accountKey vs accountKeypair
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
	"homepage": "https://rootprojects.org/acme/",
 | 
			
		||||
	"main": "acme.js",
 | 
			
		||||
	"browser": {
 | 
			
		||||
		"./native.js": "./browser.js",
 | 
			
		||||
		"./lib/native.js": "./lib/browser.js",
 | 
			
		||||
		"./lib/node/sha2.js": "./lib/browser/sha2.js",
 | 
			
		||||
		"./lib/node/http.js": "./lib/browser/http.js"
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@ -88,9 +88,10 @@ async function happyPath(accKty, srvKty, rnd) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var accountKeypair = await Keypairs.generate({ kty: accKty });
 | 
			
		||||
  var accountKey = accountKeypair.private;
 | 
			
		||||
	if (config.debug) {
 | 
			
		||||
		console.info('Account Key Created');
 | 
			
		||||
		console.info(JSON.stringify(accountKeypair, null, 2));
 | 
			
		||||
		console.info(JSON.stringify(accountKey, null, 2));
 | 
			
		||||
		console.info();
 | 
			
		||||
		console.info();
 | 
			
		||||
	}
 | 
			
		||||
@ -98,7 +99,7 @@ async function happyPath(accKty, srvKty, rnd) {
 | 
			
		||||
	var account = await acme.accounts.create({
 | 
			
		||||
		agreeToTerms: agree,
 | 
			
		||||
		// TODO detect jwk/pem/der?
 | 
			
		||||
		accountKeypair: { privateKeyJwk: accountKeypair.private },
 | 
			
		||||
		accountKey: accountKey,
 | 
			
		||||
		subscriberEmail: config.email
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@ -176,7 +177,7 @@ async function happyPath(accKty, srvKty, rnd) {
 | 
			
		||||
 | 
			
		||||
	var results = await acme.certificates.create({
 | 
			
		||||
		account: account,
 | 
			
		||||
		accountKeypair: { privateKeyJwk: accountKeypair.private },
 | 
			
		||||
		accountKey: accountKey,
 | 
			
		||||
		csr: csr,
 | 
			
		||||
		domains: domains,
 | 
			
		||||
		challenges: challenges, // must be implemented
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user