WIP get challenges
This commit is contained in:
		
							parent
							
								
									488067ec20
								
							
						
					
					
						commit
						7385dd8580
					
				
							
								
								
									
										62
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								app.js
									
									
									
									
									
								
							| @ -1,3 +1,4 @@ | ||||
| /*global Promise*/ | ||||
| (function () { | ||||
|   'use strict'; | ||||
| 
 | ||||
| @ -47,8 +48,8 @@ | ||||
|       $$('button').map(function ($el) { $el.disabled = true; }); | ||||
|       var opts = { | ||||
|         kty: $('input[name="kty"]:checked').value | ||||
|         , namedCurve: $('input[name="ec-crv"]:checked').value | ||||
|         , modulusLength: $('input[name="rsa-len"]:checked').value | ||||
|       , namedCurve: $('input[name="ec-crv"]:checked').value | ||||
|       , modulusLength: $('input[name="rsa-len"]:checked').value | ||||
|       }; | ||||
|       console.log('opts', opts); | ||||
|       Keypairs.generate(opts).then(function (results) { | ||||
| @ -112,15 +113,56 @@ | ||||
|       }); | ||||
|       acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) { | ||||
|         console.log('acme result', result); | ||||
|         var privJwk = JSON.parse($('.js-jwk').innerText).private; | ||||
|         var email = $('.js-email').innerText; | ||||
|         function checkTos(tos) { | ||||
|           console.log("TODO checkbox for agree to terms"); | ||||
|           return tos; | ||||
|         } | ||||
|         return acme.accounts.create({ | ||||
|           email: $('.js-email').innerText | ||||
|         , agreeToTerms: function (tos) { | ||||
|             console.log("TODO checkbox for agree to terms"); | ||||
|             return tos; | ||||
|           } | ||||
|         , accountKeypair: { | ||||
|             privateKeyJwk: JSON.parse($('.js-jwk').innerText).private | ||||
|           } | ||||
|           email: email | ||||
|         , agreeToTerms: checkTos | ||||
|         , accountKeypair: { privateKeyJwk: privJwk } | ||||
|         }).then(function (account) { | ||||
|           console.log("account created result:", account); | ||||
|           return Keypairs.generate({ | ||||
|             kty: 'RSA' | ||||
|           , modulusLength: 2048 | ||||
|           }).then(function (pair) { | ||||
|             console.log('domain keypair:', pair); | ||||
|             var domains = ($('.js-domains').innerText||'example.com').split(/[, ]+/g); | ||||
|             return acme.certificates.create({ | ||||
|               accountKeypair: { privateKeyJwk: privJwk } | ||||
|             , account: account | ||||
|             , domainKeypair: { privateKeyJwk: pair.private } | ||||
|             , email: email | ||||
|             , domains: domains | ||||
|             , agreeToTerms: checkTos | ||||
|             , challenges: { | ||||
|                 'dns-01': { | ||||
|                   set: function (opts) { | ||||
|                     console.log('dns-01 set challenge:'); | ||||
|                     console.log(JSON.stringify(opts, null, 2)); | ||||
|                     return new Promise(function (resolve) { | ||||
|                       while (!window.confirm("Did you set the challenge?")) {} | ||||
|                       resolve(); | ||||
|                     }); | ||||
|                   } | ||||
|                 , remove: function (opts) { | ||||
|                     console.log('dns-01 remove challenge:'); | ||||
|                     console.log(JSON.stringify(opts, null, 2)); | ||||
|                     return new Promise(function (resolve) { | ||||
|                       while (!window.confirm("Did you delete the challenge?")) {} | ||||
|                       resolve(); | ||||
|                     }); | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|             }); | ||||
|           }); | ||||
|         }).catch(function (err) { | ||||
|           console.error("A bad thing happened:"); | ||||
|           console.error(err); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
| @ -63,6 +63,10 @@ | ||||
|     <form class="js-acme-account"> | ||||
|       <label for="-acmeEmail">Email:</label> | ||||
|       <input class="js-email" type="email" id="-acmeEmail"> | ||||
|       <br> | ||||
|       <label for="-acmeDomains">Domains:</label> | ||||
|       <input class="js-domains" type="text" id="-acmeDomains"> | ||||
|       <br> | ||||
|       <button class="js-create-account" hidden>Create Account</button> | ||||
|     </form> | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										244
									
								
								lib/acme.js
									
									
									
									
									
								
							
							
						
						
									
										244
									
								
								lib/acme.js
									
									
									
									
									
								
							| @ -7,7 +7,7 @@ | ||||
| /* globals Promise */ | ||||
| 
 | ||||
| var ACME = exports.ACME = {}; | ||||
| var Keypairs = exports.Keypairs || {}; | ||||
| //var Keypairs = exports.Keypairs || {};
 | ||||
| var Enc = exports.Enc || {}; | ||||
| var Crypto = exports.Crypto || {}; | ||||
| 
 | ||||
| @ -90,7 +90,7 @@ ACME._getNonce = function (me) { | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (nonce) { return Promise.resolve(nonce); } | ||||
|   if (nonce) { return Promise.resolve(nonce.nonce); } | ||||
|   return me.request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { | ||||
|     return resp.headers['replay-nonce']; | ||||
|   }); | ||||
| @ -132,26 +132,7 @@ ACME._registerAccount = function (me, options) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       var jwk = options.accountKeypair.privateKeyJwk; | ||||
|       var p; | ||||
|       if (jwk) { | ||||
|         // nix the browser jwk extras
 | ||||
|         jwk.key_ops = undefined; | ||||
|         jwk.ext = undefined; | ||||
|         p = Promise.resolve({ private: jwk, public: Keypairs.neuter({ jwk: jwk }) }); | ||||
|       } else { | ||||
|         p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); | ||||
|       } | ||||
|       return p.then(function (pair) { | ||||
|         options.accountKeypair.privateKeyJwk = pair.private; | ||||
|         options.accountKeypair.publicKeyJwk = pair.public; | ||||
|         if (pair.public.kid) { | ||||
|           pair = JSON.parse(JSON.stringify(pair)); | ||||
|           delete pair.public.kid; | ||||
|           delete pair.private.kid; | ||||
|         } | ||||
|         return pair; | ||||
|       }).then(function (pair) { | ||||
|       return ACME._importKeypair(me, options.accountKeypair).then(function (pair) { | ||||
|         var contact; | ||||
|         if (options.contact) { | ||||
|           contact = options.contact.slice(0); | ||||
| @ -209,7 +190,7 @@ ACME._registerAccount = function (me, options) { | ||||
|               status: 'valid' | ||||
|             } | ||||
|             */ | ||||
|             if (!account) { account = { _emptyResponse: true, key: {} }; } | ||||
|             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; | ||||
| @ -346,9 +327,10 @@ ACME._testChallenges = function (me, options) { | ||||
|       , wildcard: identifierValue.includes('*.') || undefined | ||||
|       }; | ||||
|       var dryrun = true; | ||||
|       var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); | ||||
|       return ACME._setChallenge(me, options, auth).then(function () { | ||||
|         return auth; | ||||
|       return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) { | ||||
|         return ACME._setChallenge(me, options, auth).then(function () { | ||||
|           return auth; | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   })).then(function (auths) { | ||||
| @ -402,17 +384,19 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { | ||||
|   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 me.Keypairs.thumbprint({ jwk: options.accountKeypair.publicKeyJwk }).then(function (thumb) { | ||||
|     auth.thumbprint = thumb; | ||||
|     //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||
|     auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||||
|     // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 | ||||
|     auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||
|     auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); | ||||
|   return ACME._importKeypair(me, options.accountKeypair).then(function (pair) { | ||||
|     return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { | ||||
|       auth.thumbprint = thumb; | ||||
|       //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||
|       auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||||
|       // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 | ||||
|       auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||
|       auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); | ||||
| 
 | ||||
|     return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { | ||||
|       auth.dnsAuthorization = hash; | ||||
|       return auth; | ||||
|       return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { | ||||
|         auth.dnsAuthorization = hash; | ||||
|         return auth; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| @ -542,15 +526,20 @@ ACME._postChallenge = function (me, options, auth) { | ||||
|   return respondToChallenge(); | ||||
| }; | ||||
| ACME._setChallenge = function (me, options, auth) { | ||||
|   console.log('challenge auth:', auth); | ||||
|   console.log('challenges:', options.challenges); | ||||
|   return new Promise(function (resolve, reject) { | ||||
|     var challengers = options.challenges || {}; | ||||
|     var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge; | ||||
|     try { | ||||
|       if (1 === options.setChallenge.length) { | ||||
|         options.setChallenge(auth).then(resolve).catch(reject); | ||||
|       } else if (2 === options.setChallenge.length) { | ||||
|         options.setChallenge(auth, function (err) { | ||||
|       if (1 === challenger.length) { | ||||
|         challenger(auth).then(resolve).catch(reject); | ||||
|       } else if (2 === challenger.length) { | ||||
|         challenger(auth, function (err) { | ||||
|           if(err) { reject(err); } else { resolve(); } | ||||
|         }); | ||||
|       } else { | ||||
|         // TODO remove this old backwards-compat
 | ||||
|         var challengeCb = function(err) { | ||||
|           if(err) { reject(err); } else { resolve(); } | ||||
|         }; | ||||
| @ -563,7 +552,7 @@ ACME._setChallenge = function (me, options, auth) { | ||||
|           console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); | ||||
|           ACME._setChallengeWarn = true; | ||||
|         } | ||||
|         options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); | ||||
|         challenger(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); | ||||
|       } | ||||
|     } catch(e) { | ||||
|       reject(e); | ||||
| @ -577,81 +566,82 @@ ACME._setChallenge = function (me, options, auth) { | ||||
| }; | ||||
| ACME._finalizeOrder = function (me, options, validatedDomains) { | ||||
|   if (me.debug) { console.debug('finalizeOrder:'); } | ||||
|   var csr = me.Keypairs.generateCsrWeb64(options.domainKeypair, validatedDomains); | ||||
|   var body = { csr: csr }; | ||||
|   var payload = JSON.stringify(body); | ||||
|   return ACME._generateCsrWeb64(me, options, validatedDomains).then(function (csr) { | ||||
|     var body = { csr: csr }; | ||||
|     var payload = JSON.stringify(body); | ||||
| 
 | ||||
|   function pollCert() { | ||||
|     if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } | ||||
|     return ACME._jwsRequest({ | ||||
|       options: options | ||||
|     , url: options._finalize | ||||
|     , protected: { kid: options._kid } | ||||
|     , payload: Enc.strToBuf(payload) | ||||
|     }).then(function (resp) { | ||||
|       if (me.debug) { console.debug('order finalized: resp.body:'); } | ||||
|       if (me.debug) { console.debug(resp.body); } | ||||
|     function pollCert() { | ||||
|       if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } | ||||
|       return ACME._jwsRequest({ | ||||
|         options: options | ||||
|       , url: options._finalize | ||||
|       , protected: { kid: options._kid } | ||||
|       , payload: Enc.strToBuf(payload) | ||||
|       }).then(function (resp) { | ||||
|         if (me.debug) { console.debug('order finalized: resp.body:'); } | ||||
|         if (me.debug) { console.debug(resp.body); } | ||||
| 
 | ||||
|       // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
 | ||||
|       // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
 | ||||
|       if ('valid' === resp.body.status) { | ||||
|         options._expires = resp.body.expires; | ||||
|         options._certificate = resp.body.certificate; | ||||
|         // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
 | ||||
|         // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
 | ||||
|         if ('valid' === resp.body.status) { | ||||
|           options._expires = resp.body.expires; | ||||
|           options._certificate = resp.body.certificate; | ||||
| 
 | ||||
|         return resp.body; // return order
 | ||||
|       } | ||||
|           return resp.body; // return order
 | ||||
|         } | ||||
| 
 | ||||
|       if ('processing' === resp.body.status) { | ||||
|         return ACME._wait().then(pollCert); | ||||
|       } | ||||
|         if ('processing' === resp.body.status) { | ||||
|           return ACME._wait().then(pollCert); | ||||
|         } | ||||
| 
 | ||||
|       if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } | ||||
|         if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } | ||||
| 
 | ||||
|         if ('pending' === resp.body.status) { | ||||
|           return Promise.reject(new Error( | ||||
|             "Did not finalize order: status 'pending'." | ||||
|           + " Best guess: You have not accepted at least one challenge for each domain:\n" | ||||
|           + "Requested: '" + options.domains.join(', ') + "'\n" | ||||
|           + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||
|           + JSON.stringify(resp.body, null, 2) | ||||
|           )); | ||||
|         } | ||||
| 
 | ||||
|         if ('invalid' === resp.body.status) { | ||||
|           return Promise.reject(new Error( | ||||
|             "Did not finalize order: status 'invalid'." | ||||
|           + " Best guess: One or more of the domain challenges could not be verified" | ||||
|           + " (or the order was canceled).\n" | ||||
|           + "Requested: '" + options.domains.join(', ') + "'\n" | ||||
|           + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||
|           + JSON.stringify(resp.body, null, 2) | ||||
|           )); | ||||
|         } | ||||
| 
 | ||||
|         if ('ready' === resp.body.status) { | ||||
|           return Promise.reject(new Error( | ||||
|             "Did not finalize order: status 'ready'." | ||||
|           + " Hmmm... this state shouldn't be possible here. That was the last state." | ||||
|           + " This one should at least be 'processing'.\n" | ||||
|           + "Requested: '" + options.domains.join(', ') + "'\n" | ||||
|           + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||
|           + JSON.stringify(resp.body, null, 2) + "\n\n" | ||||
|           + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" | ||||
|           )); | ||||
|         } | ||||
| 
 | ||||
|       if ('pending' === resp.body.status) { | ||||
|         return Promise.reject(new Error( | ||||
|           "Did not finalize order: status 'pending'." | ||||
|         + " Best guess: You have not accepted at least one challenge for each domain:\n" | ||||
|         + "Requested: '" + options.domains.join(', ') + "'\n" | ||||
|         + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||
|         + JSON.stringify(resp.body, null, 2) | ||||
|         )); | ||||
|       } | ||||
| 
 | ||||
|       if ('invalid' === resp.body.status) { | ||||
|         return Promise.reject(new Error( | ||||
|           "Did not finalize order: status 'invalid'." | ||||
|         + " Best guess: One or more of the domain challenges could not be verified" | ||||
|         + " (or the order was canceled).\n" | ||||
|         + "Requested: '" + options.domains.join(', ') + "'\n" | ||||
|         + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||
|         + JSON.stringify(resp.body, null, 2) | ||||
|         )); | ||||
|       } | ||||
| 
 | ||||
|       if ('ready' === resp.body.status) { | ||||
|         return Promise.reject(new Error( | ||||
|           "Did not finalize order: status 'ready'." | ||||
|         + " Hmmm... this state shouldn't be possible here. That was the last state." | ||||
|         + " This one should at least be 'processing'.\n" | ||||
|           "Didn't finalize order: Unhandled status '" + resp.body.status + "'." | ||||
|         + " This is not one of the known statuses...\n" | ||||
|         + "Requested: '" + options.domains.join(', ') + "'\n" | ||||
|         + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||
|         + JSON.stringify(resp.body, null, 2) + "\n\n" | ||||
|         + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" | ||||
|         )); | ||||
|       } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|       return Promise.reject(new Error( | ||||
|         "Didn't finalize order: Unhandled status '" + resp.body.status + "'." | ||||
|       + " This is not one of the known statuses...\n" | ||||
|       + "Requested: '" + options.domains.join(', ') + "'\n" | ||||
|       + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||
|       + JSON.stringify(resp.body, null, 2) + "\n\n" | ||||
|       + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   return pollCert(); | ||||
|     return pollCert(); | ||||
|   }); | ||||
| }; | ||||
| // _kid
 | ||||
| // registerAccount
 | ||||
| @ -686,16 +676,18 @@ ACME._getCertificate = function (me, options) { | ||||
|   } | ||||
|   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).")); | ||||
|     + " with the first being the subject of the certificate (or options.subject must specified).")); | ||||
|   } | ||||
| 
 | ||||
|   // It's just fine if there's no account, we'll go get the key id we need via the public key
 | ||||
|   if (options.accountKid || options.account && options.account.kid) { | ||||
|     options._kid = options.accountKid || options.account.kid; | ||||
|   } else { | ||||
|   // It's just fine if there's no account, we'll go get the key id we need via the existing key
 | ||||
|   options._kid = options._kid || options.accountKid | ||||
|     || (options.account && (options.account.kid | ||||
|       || (options.account.key && options.account.key.kid))); | ||||
|   if (!options._kid) { | ||||
|     //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 (account) { | ||||
|       options._kid = account.key.kid; | ||||
|       // start back from the top
 | ||||
|       return ACME._getCertificate(me, options); | ||||
|     }); | ||||
| @ -720,9 +712,6 @@ ACME._getCertificate = function (me, options) { | ||||
|     }; | ||||
| 
 | ||||
|     var payload = JSON.stringify(body); | ||||
|     // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 | ||||
|     options._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); | ||||
|     options._alg = ('EC' === options._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
 | ||||
|     if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } | ||||
|     return ACME._jwsRequest({ | ||||
|       options: options | ||||
| @ -815,6 +804,13 @@ ACME._getCertificate = function (me, options) { | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| ACME._generateCsrWeb64 = function (me, options, validatedDomains) { | ||||
|   return ACME._importKeypair(me, options.domainKeypair).then(function (/*pair*/) { | ||||
|     return me.Keypairs.generateCsr(options.domainKeypair, validatedDomains).then(function (der) { | ||||
|       return Enc.bufToUrlBase64(der); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| ACME.create = function create(me) { | ||||
|   if (!me) { me = {}; } | ||||
| @ -942,6 +938,30 @@ ACME._defaultRequest = function (opts) { | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| ACME._importKeypair = function (me, kp) { | ||||
|   var jwk = kp.privateKeyJwk; | ||||
|   var p; | ||||
|   if (jwk) { | ||||
|     // nix the browser jwk extras
 | ||||
|     jwk.key_ops = undefined; | ||||
|     jwk.ext = undefined; | ||||
|     p = Promise.resolve({ private: jwk, public: me.Keypairs.neuter({ jwk: jwk }) }); | ||||
|   } else { | ||||
|     p = me.Keypairs.import({ pem: kp.privateKeyPem }); | ||||
|   } | ||||
|   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; | ||||
|       delete pair.private.kid; | ||||
|     } | ||||
|     return pair; | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
| TODO | ||||
| Per-Order State Params | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user