mirror of
				https://github.com/therootcompany/acme.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	v1.7.0: better error checking and challenge type handling
This commit is contained in:
		
							parent
							
								
									401535a5ab
								
							
						
					
					
						commit
						b1d566d54e
					
				
							
								
								
									
										159
									
								
								node.js
									
									
									
									
									
								
							
							
						
						
									
										159
									
								
								node.js
									
									
									
									
									
								
							| @ -261,6 +261,36 @@ ACME._wait = function wait(ms) { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | ACME._testChallengeOptions = function () { | ||||||
|  |   var chToken = require('crypto').randomBytes(16).toString('hex'); | ||||||
|  |   return [ | ||||||
|  |     { | ||||||
|  |       "type": "http-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/0", | ||||||
|  |       "token": "test-" + chToken + "-0" | ||||||
|  |     } | ||||||
|  |   , { | ||||||
|  |       "type": "dns-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/1", | ||||||
|  |       "token": "test-" + chToken + "-1", | ||||||
|  |       "_wildcard": true | ||||||
|  |     } | ||||||
|  |   , { | ||||||
|  |       "type": "tls-sni-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/2", | ||||||
|  |       "token": "test-" + chToken + "-2" | ||||||
|  |     } | ||||||
|  |   , { | ||||||
|  |       "type": "tls-alpn-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/3", | ||||||
|  |       "token": "test-" + chToken + "-3" | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  | }; | ||||||
| ACME._testChallenges = function (me, options) { | ACME._testChallenges = function (me, options) { | ||||||
|   if (me.skipChallengeTest) { |   if (me.skipChallengeTest) { | ||||||
|     return Promise.resolve(); |     return Promise.resolve(); | ||||||
| @ -268,28 +298,58 @@ ACME._testChallenges = function (me, options) { | |||||||
| 
 | 
 | ||||||
|   return Promise.all(options.domains.map(function (identifierValue) { |   return Promise.all(options.domains.map(function (identifierValue) { | ||||||
|     // TODO we really only need one to pass, not all to pass
 |     // TODO we really only need one to pass, not all to pass
 | ||||||
|     return Promise.all(options.challengeTypes.map(function (chType) { |     var results = ACME._testChallengeOptions(); | ||||||
|       var chToken = require('crypto').randomBytes(16).toString('hex'); |     if (identifierValue.inludes("*")) { | ||||||
|  |       results = results.filter(function (ch) { return ch._wildcard; }); | ||||||
|  |     } | ||||||
|  |     var challenge = ACME._chooseChallenge(options, results); | ||||||
|  |     if (!challenge) { | ||||||
|  |       // For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||||
|  |       var enabled = options.challengeTypes.join(', ') || 'none'; | ||||||
|  |       var suitable = results.map(function (r) { return r.type; }).join(', ') || 'none'; | ||||||
|  |       return Promise.reject(new Error( | ||||||
|  |         "None of the challenge types that you've enabled ( " + enabled + " )" | ||||||
|  |           + " are suitable for validating the domain you've selected (" + identifierValue + ")." | ||||||
|  |           + " You must enable one of ( " + suitable + " )." | ||||||
|  |       )); | ||||||
|  |     } | ||||||
|  |     return Promise.resolve().then(function () { | ||||||
|       var thumbprint = me.RSA.thumbprint(options.accountKeypair); |       var thumbprint = me.RSA.thumbprint(options.accountKeypair); | ||||||
|       var keyAuthorization = chToken + '.' + thumbprint; |       var keyAuthorization = challenge.token + '.' + thumbprint; | ||||||
|       var auth = { |       var auth = { | ||||||
|         identifier: { type: "dns", value: identifierValue } |         identifier: { type: "dns", value: identifierValue } | ||||||
|       , hostname: identifierValue |       , hostname: identifierValue | ||||||
|       , type: chType |       , type: challenge.type | ||||||
|       , token: chToken |       , token: challenge.token | ||||||
|       , thumbprint: thumbprint |       , thumbprint: thumbprint | ||||||
|       , keyAuthorization: keyAuthorization |       , keyAuthorization: keyAuthorization | ||||||
|       , dnsAuthorization: me.RSA.utils.toWebsafeBase64( |       , dnsAuthorization: ACME._toWebsafeBase64( | ||||||
|           require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') |           require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') | ||||||
|         ) |         ) | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       return ACME._setChallenge(me, options, auth).then(function () { |       return ACME._setChallenge(me, options, auth).then(function () { | ||||||
|         return ACME.challengeTests[chType](me, auth); |         return ACME.challengeTests[challenge.type](me, auth); | ||||||
|       }); |       }); | ||||||
|     })); |     }); | ||||||
|   })); |   })); | ||||||
| }; | }; | ||||||
|  | ACME._chooseChallenge = function(options, results) { | ||||||
|  |   // For each of the challenge types that we support
 | ||||||
|  |   var challenge; | ||||||
|  |   options.challengesTypes.some(function (chType) { | ||||||
|  |     // And for each of the challenge types that are allowed
 | ||||||
|  |     return results.challenges.some(function (ch) { | ||||||
|  |       // Check to see if there are any matches
 | ||||||
|  |       if (ch.type === chType) { | ||||||
|  |         challenge = ch; | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return challenge; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
 | // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
 | ||||||
| ACME._postChallenge = function (me, options, identifier, ch) { | ACME._postChallenge = function (me, options, identifier, ch) { | ||||||
| @ -310,7 +370,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { | |||||||
|   , token: ch.token |   , token: ch.token | ||||||
|   , thumbprint: thumbprint |   , thumbprint: thumbprint | ||||||
|   , keyAuthorization: keyAuthorization |   , keyAuthorization: keyAuthorization | ||||||
|   , dnsAuthorization: me.RSA.utils.toWebsafeBase64( |   , dnsAuthorization: ACME._toWebsafeBase64( | ||||||
|       require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') |       require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') | ||||||
|     ) |     ) | ||||||
|   }; |   }; | ||||||
| @ -337,7 +397,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { | |||||||
|     var jws = me.RSA.signJws( |     var jws = me.RSA.signJws( | ||||||
|       options.accountKeypair |       options.accountKeypair | ||||||
|     , undefined |     , undefined | ||||||
|     , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } |     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: ch.url, kid: me._kid } | ||||||
|     , Buffer.from(JSON.stringify({ "status": "deactivated" })) |     , Buffer.from(JSON.stringify({ "status": "deactivated" })) | ||||||
|     ); |     ); | ||||||
|     me._nonce = null; |     me._nonce = null; | ||||||
| @ -562,24 +622,50 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { | |||||||
| ACME._getCertificate = function (me, options) { | ACME._getCertificate = function (me, options) { | ||||||
|   if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } |   if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } | ||||||
| 
 | 
 | ||||||
|   if (!options.challengeTypes) { |   // Lot's of error checking to inform the user of mistakes
 | ||||||
|     if (!options.challengeType) { |   if (!(options.challengeTypes||[]).length) { | ||||||
|       return Promise.reject(new Error("challenge type must be specified")); |     options.challengeTypes = Object.keys(options.challenges||{}); | ||||||
|     } |  | ||||||
|     options.challengeTypes = [ options.challengeType ]; |  | ||||||
|   } |   } | ||||||
|  |   if (!options.challengeTypes.length) { | ||||||
|  |     options.challengeTypes = [ options.challengeType ].filter(Boolean); | ||||||
|  |   } | ||||||
|  |   if (options.challengeType) { | ||||||
|  |     options.challengeTypes.sort(function (a, b) { | ||||||
|  |       if (a === options.challengeType) { return -1; } | ||||||
|  |       if (b === options.challengeType) { return 1; } | ||||||
|  |       return 0; | ||||||
|  |     }); | ||||||
|  |     if (options.challengeType !== options.challengeTypes[0]) { | ||||||
|  |       return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," | ||||||
|  |         + " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // TODO check that all challengeTypes are represented in challenges
 | ||||||
|  |   if (!options.challengeTypes.length) { | ||||||
|  |     return Promise.reject(new Error("options.challengesTypes (string array) must be specified" | ||||||
|  |       + " (and in order of preferential priority).")); | ||||||
|  |   } | ||||||
|  |   if (!(options.domains && options.domains.length)) { | ||||||
|  |     return Promise.reject(new Error("options.domains must be a list of string domain names," | ||||||
|  |     + " with the first being the subject of the domain (or options.subject must specified).")); | ||||||
|  |   } | ||||||
|  |   if (!options.subject) { options.subject = options.domains[0]; } | ||||||
| 
 | 
 | ||||||
|  |   // It's just fine if there's no account, we'll go get the key id we need via the public key
 | ||||||
|   if (!me._kid) { |   if (!me._kid) { | ||||||
|     if (options.accountKid) { |     if (options.accountKid || options.account.kid) { | ||||||
|       me._kid = options.accountKid; |       me._kid = options.accountKid || options.account.kid; | ||||||
|     } else { |     } else { | ||||||
|       //return Promise.reject(new Error("must include KeyID"));
 |       //return Promise.reject(new Error("must include KeyID"));
 | ||||||
|  |       // This is an idempotent request. It'll return the same account for the same public key.
 | ||||||
|       return ACME._registerAccount(me, options).then(function () { |       return ACME._registerAccount(me, options).then(function () { | ||||||
|  |         // start back from the top
 | ||||||
|         return ACME._getCertificate(me, options); |         return ACME._getCertificate(me, options); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // Do a little dry-run / self-test
 | ||||||
|   return ACME._testChallenges(me, options).then(function () { |   return ACME._testChallenges(me, options).then(function () { | ||||||
|     if (me.debug) { console.debug('[acme-v2] certificates.create'); } |     if (me.debug) { console.debug('[acme-v2] certificates.create'); } | ||||||
|     return ACME._getNonce(me).then(function () { |     return ACME._getNonce(me).then(function () { | ||||||
| @ -592,11 +678,14 @@ ACME._getCertificate = function (me, options) { | |||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       var payload = JSON.stringify(body); |       var payload = JSON.stringify(body); | ||||||
|  |       // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 | ||||||
|  |       me._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); | ||||||
|  |       me._alg = ('EC' === me._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
 | ||||||
|       var jws = me.RSA.signJws( |       var jws = me.RSA.signJws( | ||||||
|         options.accountKeypair |         options.accountKeypair | ||||||
|       , undefined |       , undefined | ||||||
|       , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } |       , { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } | ||||||
|       , Buffer.from(payload) |       , Buffer.from(payload, 'utf8') | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } |       if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } | ||||||
| @ -634,35 +723,20 @@ ACME._getCertificate = function (me, options) { | |||||||
| 
 | 
 | ||||||
|           return ACME._getChallenges(me, options, authUrl).then(function (results) { |           return ACME._getChallenges(me, options, authUrl).then(function (results) { | ||||||
|             // var domain = options.domains[i]; // results.identifier.value
 |             // var domain = options.domains[i]; // results.identifier.value
 | ||||||
|             var chType = options.challengeTypes.filter(function (chType) { |  | ||||||
|               return results.challenges.some(function (ch) { |  | ||||||
|                 return ch.type === chType; |  | ||||||
|               }); |  | ||||||
|             }).sort(function (aType, bType) { |  | ||||||
|               var a = results.challenges.filter(function (ch) { return ch.type === aType; })[0]; |  | ||||||
|               var b = results.challenges.filter(function (ch) { return ch.type === bType; })[0]; |  | ||||||
| 
 | 
 | ||||||
|               if ('valid' === a.status) { return 1; } |             // If it's already valid, we're golden it regardless
 | ||||||
|               if ('valid' === b.status) { return -1; } |             if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { | ||||||
|               return 0; |               return; | ||||||
|             })[0]; |             } | ||||||
| 
 |  | ||||||
|             var challenge = results.challenges.filter(function (ch) { |  | ||||||
|               if (chType === ch.type) { |  | ||||||
|                 return ch; |  | ||||||
|               } |  | ||||||
|             })[0]; |  | ||||||
| 
 | 
 | ||||||
|  |             var challenge = ACME._chooseChallenge(options, results); | ||||||
|             if (!challenge) { |             if (!challenge) { | ||||||
|  |               // For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||||
|               return Promise.reject(new Error( |               return Promise.reject(new Error( | ||||||
|                 "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." |                 "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." | ||||||
|               )); |               )); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if ("valid" === challenge.status) { |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return ACME._postChallenge(me, options, results.identifier, challenge); |             return ACME._postChallenge(me, options, results.identifier, challenge); | ||||||
|           }).then(function () { |           }).then(function () { | ||||||
|             return next(); |             return next(); | ||||||
| @ -705,6 +779,7 @@ ACME.create = function create(me) { | |||||||
|   // me.debug = true;
 |   // me.debug = true;
 | ||||||
|   me.challengePrefixes = ACME.challengePrefixes; |   me.challengePrefixes = ACME.challengePrefixes; | ||||||
|   me.RSA = me.RSA || require('rsa-compat').RSA; |   me.RSA = me.RSA || require('rsa-compat').RSA; | ||||||
|  |   //me.Keypairs = me.Keypairs || require('keypairs');
 | ||||||
|   me.request = me.request || require('@coolaj86/urequest'); |   me.request = me.request || require('@coolaj86/urequest'); | ||||||
|   me._dig = function (query) { |   me._dig = function (query) { | ||||||
|     // TODO use digd.js
 |     // TODO use digd.js
 | ||||||
| @ -767,3 +842,7 @@ ACME.create = function create(me) { | |||||||
|   }; |   }; | ||||||
|   return me; |   return me; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | ACME._toWebsafeBase64 = function (b64) { | ||||||
|  |   return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); | ||||||
|  | }; | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "acme-v2", |   "name": "acme-v2", | ||||||
|   "version": "1.6.0", |   "version": "1.7.0", | ||||||
|   "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", |   "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", | ||||||
|   "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", |   "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", | ||||||
|   "main": "node.js", |   "main": "node.js", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user