using new acme
This commit is contained in:
		
							parent
							
								
									49d5346615
								
							
						
					
					
						commit
						0405a7db90
					
				| @ -153,9 +153,10 @@ | |||||||
|           --> |           --> | ||||||
|           <button class="button-next" type="submit">Next</button> |           <button class="button-next" type="submit">Next</button> | ||||||
|           <div class="email-usage"> |           <div class="email-usage"> | ||||||
|             Why do we need your email? We link your SSL certificates to the |             Why do we need your email? | ||||||
|             email you use so you can manage your certificates in the future, |             We link your SSL certificates to the email you use so that you'll | ||||||
|             and get important email updates about them. |             be notified before the certificate expires and so you can manage | ||||||
|  |             your certificates in the future. | ||||||
|           </div> |           </div> | ||||||
|         </form> |         </form> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										670
									
								
								app/js/app.js
									
									
									
									
									
								
							
							
						
						
									
										670
									
								
								app/js/app.js
									
									
									
									
									
								
							| @ -1,670 +0,0 @@ | |||||||
| (function () { |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
|   /*global URLSearchParams,Headers*/ |  | ||||||
|   var BROWSER_SUPPORTS_ECDSA; |  | ||||||
|   var $qs = function (s) { return window.document.querySelector(s); }; |  | ||||||
|   var $qsa = function (s) { return window.document.querySelectorAll(s); }; |  | ||||||
|   var info = {}; |  | ||||||
|   var steps = {}; |  | ||||||
|   var nonce; |  | ||||||
|   var kid; |  | ||||||
|   var i = 1; |  | ||||||
|   var BACME = window.BACME; |  | ||||||
|   var PromiseA = window.Promise; |  | ||||||
|   var crypto = window.crypto; |  | ||||||
| 
 |  | ||||||
|   function testEcdsaSupport() { |  | ||||||
|     var opts = { |  | ||||||
|       type: 'ECDSA' |  | ||||||
|     , bitlength: '256' |  | ||||||
|     }; |  | ||||||
|     return BACME.accounts.generateKeypair(opts).then(function (jwk) { |  | ||||||
|       return crypto.subtle.importKey( |  | ||||||
|         "jwk" |  | ||||||
|       , jwk |  | ||||||
|       , { name: "ECDSA", namedCurve: "P-256" } |  | ||||||
|       , true |  | ||||||
|       , ["sign"] |  | ||||||
|       ).then(function (privateKey) { |  | ||||||
|         return window.crypto.subtle.exportKey("pkcs8", privateKey); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   function testRsaSupport() { |  | ||||||
|     var opts = { |  | ||||||
|       type: 'RSA' |  | ||||||
|     , bitlength: '2048' |  | ||||||
|     }; |  | ||||||
|     return BACME.accounts.generateKeypair(opts).then(function (jwk) { |  | ||||||
|       return crypto.subtle.importKey( |  | ||||||
|         "jwk" |  | ||||||
|       , jwk |  | ||||||
|       , { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } } |  | ||||||
|       , true |  | ||||||
|       , ["sign"] |  | ||||||
|       ).then(function (privateKey) { |  | ||||||
|         return window.crypto.subtle.exportKey("pkcs8", privateKey); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   function testKeypairSupport() { |  | ||||||
|     return testEcdsaSupport().then(function () { |  | ||||||
|       console.info("[crypto] ECDSA is supported"); |  | ||||||
|       BROWSER_SUPPORTS_ECDSA = true; |  | ||||||
|       localStorage.setItem('version', '1'); |  | ||||||
|       return true; |  | ||||||
|     }).catch(function () { |  | ||||||
|       console.warn("[crypto] ECDSA is NOT fully supported"); |  | ||||||
|       BROWSER_SUPPORTS_ECDSA = false; |  | ||||||
| 
 |  | ||||||
|       // fix previous firefox browsers
 |  | ||||||
|       if (!localStorage.getItem('version')) { |  | ||||||
|         localStorage.clear(); |  | ||||||
|         localStorage.setItem('version', '1'); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return false; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   testKeypairSupport().then(function (ecdsaSupport) { |  | ||||||
|     if (ecdsaSupport) { |  | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return testRsaSupport().then(function () { |  | ||||||
|       console.info('[crypto] RSA is supported'); |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       console.error('[crypto] could not use either EC nor RSA.'); |  | ||||||
|       console.error(err); |  | ||||||
|       window.alert("Your browser is cryptography support (neither RSA or EC is usable). Please use Chrome, Firefox, or Safari."); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; |  | ||||||
|   function updateApiType() { |  | ||||||
|     console.log("type updated"); |  | ||||||
|     /*jshint validthis: true */ |  | ||||||
|     var input = this || Array.prototype.filter.call( |  | ||||||
|       $qsa('.js-acme-api-type'), function ($el) { return $el.checked; } |  | ||||||
|     )[0]; |  | ||||||
|     console.log('ACME api type radio:', input.value); |  | ||||||
|     $qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value); |  | ||||||
|   } |  | ||||||
|   $qsa('.js-acme-api-type').forEach(function ($el) { |  | ||||||
|     $el.addEventListener('change', updateApiType); |  | ||||||
|   }); |  | ||||||
|   updateApiType(); |  | ||||||
| 
 |  | ||||||
|   function hideForms() { |  | ||||||
|     $qsa('.js-acme-form').forEach(function (el) { |  | ||||||
|       el.hidden = true; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function updateProgress(currentStep) { |  | ||||||
|     var progressSteps = $qs("#js-progress-bar").children; |  | ||||||
|     for(var j = 0; j < progressSteps.length; j++) { |  | ||||||
|       if(j < currentStep) { |  | ||||||
|         progressSteps[j].classList.add("js-progress-step-complete"); |  | ||||||
|         progressSteps[j].classList.remove("js-progress-step-started"); |  | ||||||
|       } else if(j === currentStep) { |  | ||||||
|         progressSteps[j].classList.remove("js-progress-step-complete"); |  | ||||||
|         progressSteps[j].classList.add("js-progress-step-started"); |  | ||||||
|       } else { |  | ||||||
|         progressSteps[j].classList.remove("js-progress-step-complete"); |  | ||||||
|         progressSteps[j].classList.remove("js-progress-step-started"); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function submitForm(ev) { |  | ||||||
|     var j = i; |  | ||||||
|     i += 1; |  | ||||||
| 
 |  | ||||||
|     return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) { |  | ||||||
|       console.error(err); |  | ||||||
|       window.alert("Something went wrong. It's our fault not yours. Please email aj@rootprojects.org and let him know that 'step " + j + "' failed."); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   $qsa('.js-acme-form').forEach(function ($el) { |  | ||||||
|     $el.addEventListener('submit', function (ev) { |  | ||||||
|       ev.preventDefault(); |  | ||||||
|       submitForm(ev); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   function updateChallengeType() { |  | ||||||
|     /*jshint validthis: true*/ |  | ||||||
|     var input = this || Array.prototype.filter.call( |  | ||||||
|       $qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; } |  | ||||||
|     )[0]; |  | ||||||
|     console.log('ch type radio:', input.value); |  | ||||||
|     $qs('.js-acme-verification-wildcard').hidden = true; |  | ||||||
|     $qs('.js-acme-verification-http-01').hidden = true; |  | ||||||
|     $qs('.js-acme-verification-dns-01').hidden = true; |  | ||||||
|     if (info.challenges.wildcard) { |  | ||||||
|       $qs('.js-acme-verification-wildcard').hidden = false; |  | ||||||
|     } |  | ||||||
|     if (info.challenges[input.value]) { |  | ||||||
|       $qs('.js-acme-verification-' + input.value).hidden = false; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   $qsa('.js-acme-challenge-type').forEach(function ($el) { |  | ||||||
|     $el.addEventListener('change', updateChallengeType); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   function saveContact(email, domains) { |  | ||||||
|     // to be used for good, not evil
 |  | ||||||
|     return window.fetch('https://api.rootprojects.org/api/rootprojects.org/public/community', { |  | ||||||
|       method: 'POST' |  | ||||||
|     , cors: true |  | ||||||
|     , headers: new Headers({ 'Content-Type': 'application/json' }) |  | ||||||
|     , body: JSON.stringify({ |  | ||||||
|         address: email |  | ||||||
|       , project: 'greenlock-domains@rootprojects.org' |  | ||||||
|       , domain: domains.join(',') |  | ||||||
|       }) |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       console.error(err); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   steps[1] = function () { |  | ||||||
|     updateProgress(0); |  | ||||||
|     hideForms(); |  | ||||||
|     $qs('.js-acme-form-domains').hidden = false; |  | ||||||
|   }; |  | ||||||
|   steps[1].submit = function () { |  | ||||||
|     info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) { |  | ||||||
|       return { type: 'dns', value: hostname.toLowerCase().trim() }; |  | ||||||
|     }).slice(0,1); //Disable multiple values for now.  We'll just take the first and work with it.
 |  | ||||||
|     info.identifiers.sort(function (a, b) { |  | ||||||
|       if (a === b) { return 0; } |  | ||||||
|       if (a < b) { return 1; } |  | ||||||
|       if (a > b) { return -1; } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) { |  | ||||||
|       $qs('.js-acme-tos-url').href = directory.meta.termsOfService; |  | ||||||
|       return BACME.nonce().then(function (_nonce) { |  | ||||||
|         nonce = _nonce; |  | ||||||
| 
 |  | ||||||
|         console.log("MAGIC STEP NUMBER in 1 is:", i); |  | ||||||
|         steps[i](); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   steps[2] = function () { |  | ||||||
|     updateProgress(0); |  | ||||||
|     hideForms(); |  | ||||||
|     $qs('.js-acme-form-account').hidden = false; |  | ||||||
|   }; |  | ||||||
|   steps[2].submit = function () { |  | ||||||
|     var email = $qs('.js-acme-account-email').value.toLowerCase().trim(); |  | ||||||
| 
 |  | ||||||
|     info.contact = [ 'mailto:' + email ]; |  | ||||||
|     info.agree = $qs('.js-acme-account-tos').checked; |  | ||||||
|     info.greenlockAgree = $qs('.js-gl-tos').checked; |  | ||||||
|     // TODO
 |  | ||||||
|     // options for
 |  | ||||||
|     // * regenerate key
 |  | ||||||
|     // * ECDSA / RSA / bitlength
 |  | ||||||
| 
 |  | ||||||
|     // TODO ping with version and account creation
 |  | ||||||
|     setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; })); |  | ||||||
| 
 |  | ||||||
|     var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null'); |  | ||||||
|     var p; |  | ||||||
| 
 |  | ||||||
|     function createKeypair() { |  | ||||||
|       var opts; |  | ||||||
| 
 |  | ||||||
|       if(BROWSER_SUPPORTS_ECDSA) { |  | ||||||
|         opts = { |  | ||||||
|           type: 'ECDSA' |  | ||||||
|         , bitlength: '256' |  | ||||||
|         }; |  | ||||||
|       } else { |  | ||||||
|         opts = { |  | ||||||
|           type: 'RSA' |  | ||||||
|         , bitlength: '2048' |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return BACME.accounts.generateKeypair(opts).then(function (jwk) { |  | ||||||
|         localStorage.setItem('account:' + email, JSON.stringify(jwk)); |  | ||||||
|         return jwk; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (jwk) { |  | ||||||
|       p = PromiseA.resolve(jwk); |  | ||||||
|     } else { |  | ||||||
|       p = testKeypairSupport().then(createKeypair); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function createAccount(jwk) { |  | ||||||
|       console.log('account jwk:'); |  | ||||||
|       console.log(jwk); |  | ||||||
|       delete jwk.key_ops; |  | ||||||
|       info.jwk = jwk; |  | ||||||
|       return BACME.accounts.sign({ |  | ||||||
|         jwk: jwk |  | ||||||
|       , contacts: [ 'mailto:' + email ] |  | ||||||
|       , agree: info.agree |  | ||||||
|       , nonce: nonce |  | ||||||
|       , kid: kid |  | ||||||
|       }).then(function (signedAccount) { |  | ||||||
|         return BACME.accounts.set({ |  | ||||||
|           signedAccount: signedAccount |  | ||||||
|         }).then(function (account) { |  | ||||||
|           console.log('account:'); |  | ||||||
|           console.log(account); |  | ||||||
|           kid = account.kid; |  | ||||||
|           return kid; |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return p.then(function (_jwk) { |  | ||||||
|       jwk = _jwk; |  | ||||||
|       kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null'); |  | ||||||
|       var p2; |  | ||||||
| 
 |  | ||||||
|       // TODO save account id rather than always retrieving it
 |  | ||||||
|       if (kid) { |  | ||||||
|         p2 = PromiseA.resolve(kid); |  | ||||||
|       } else { |  | ||||||
|         p2 = createAccount(jwk); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return p2.then(function (_kid) { |  | ||||||
|         kid = _kid; |  | ||||||
|         info.kid = kid; |  | ||||||
|         return BACME.orders.sign({ |  | ||||||
|           jwk: jwk |  | ||||||
|         , identifiers: info.identifiers |  | ||||||
|         , kid: kid |  | ||||||
|         }).then(function (signedOrder) { |  | ||||||
|           return BACME.orders.create({ |  | ||||||
|             signedOrder: signedOrder |  | ||||||
|           }).then(function (order) { |  | ||||||
|             info.finalizeUrl = order.finalize; |  | ||||||
|             info.orderUrl = order.url; // from header Location ???
 |  | ||||||
|             return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) { |  | ||||||
|               return BACME.challenges.all().then(function (claims) { |  | ||||||
|                 console.log('claims:'); |  | ||||||
|                 console.log(claims); |  | ||||||
|                 var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; |  | ||||||
|                 info.challenges = obj; |  | ||||||
|                 var map = { |  | ||||||
|                   'http-01': '.js-acme-verification-http-01' |  | ||||||
|                 , 'dns-01': '.js-acme-verification-dns-01' |  | ||||||
|                 , 'wildcard': '.js-acme-verification-wildcard' |  | ||||||
|                 }; |  | ||||||
| 
 |  | ||||||
|                 /* |  | ||||||
|                 var tpls = {}; |  | ||||||
|                 Object.keys(map).forEach(function (k) { |  | ||||||
|                   var sel = map[k] + ' tbody'; |  | ||||||
|                   console.log(sel); |  | ||||||
|                   tpls[k] = $qs(sel).innerHTML; |  | ||||||
|                   $qs(map[k] + ' tbody').innerHTML = ''; |  | ||||||
|                 }); |  | ||||||
|                 */ |  | ||||||
| 
 |  | ||||||
|                 // TODO make Promise-friendly
 |  | ||||||
|                 return PromiseA.all(claims.map(function (claim) { |  | ||||||
|                   var hostname = claim.identifier.value; |  | ||||||
|                   return PromiseA.all(claim.challenges.map(function (c) { |  | ||||||
|                     var keyAuth = BACME.challenges['http-01']({ |  | ||||||
|                       token: c.token |  | ||||||
|                     , thumbprint: thumbprint |  | ||||||
|                     , challengeDomain: hostname |  | ||||||
|                     }); |  | ||||||
|                     return BACME.challenges['dns-01']({ |  | ||||||
|                       keyAuth: keyAuth.value |  | ||||||
|                     , challengeDomain: hostname |  | ||||||
|                     }).then(function (dnsAuth) { |  | ||||||
|                       var data = { |  | ||||||
|                         type: c.type |  | ||||||
|                       , hostname: hostname |  | ||||||
|                       , url: c.url |  | ||||||
|                       , token: c.token |  | ||||||
|                       , keyAuthorization: keyAuth |  | ||||||
|                       , httpPath: keyAuth.path |  | ||||||
|                       , httpAuth: keyAuth.value |  | ||||||
|                       , dnsType: dnsAuth.type |  | ||||||
|                       , dnsHost: dnsAuth.host |  | ||||||
|                       , dnsAnswer: dnsAuth.answer |  | ||||||
|                       }; |  | ||||||
| 
 |  | ||||||
|                       console.log(''); |  | ||||||
|                       console.log('CHALLENGE'); |  | ||||||
|                       console.log(claim); |  | ||||||
|                       console.log(c); |  | ||||||
|                       console.log(data); |  | ||||||
|                       console.log(''); |  | ||||||
| 
 |  | ||||||
|                       if (claim.wildcard) { |  | ||||||
|                         obj.wildcard.push(data); |  | ||||||
|                         let verification = $qs(".js-acme-verification-wildcard"); |  | ||||||
|                         verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; |  | ||||||
|                         verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; |  | ||||||
|                         verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; |  | ||||||
| 
 |  | ||||||
|                       } else if(obj[data.type]) { |  | ||||||
| 
 |  | ||||||
|                         obj[data.type].push(data); |  | ||||||
| 
 |  | ||||||
|                         if ('dns-01' === data.type) { |  | ||||||
|                           let verification = $qs(".js-acme-verification-dns-01"); |  | ||||||
|                           verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname; |  | ||||||
|                           verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; |  | ||||||
|                           verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; |  | ||||||
|                         } else if ('http-01' === data.type) { |  | ||||||
|                           $qs(".js-acme-ver-file-location").innerHTML = data.httpPath.split("/").slice(-1); |  | ||||||
|                           $qs(".js-acme-ver-content").innerHTML = data.httpAuth; |  | ||||||
|                           $qs(".js-acme-ver-uri").innerHTML = data.httpPath; |  | ||||||
|                           $qs(".js-download-verify-link").href = |  | ||||||
|                             "data:text/octet-stream;base64," + window.btoa(data.httpAuth); |  | ||||||
|                           $qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1); |  | ||||||
|                         } |  | ||||||
|                       } |  | ||||||
| 
 |  | ||||||
|                     }); |  | ||||||
| 
 |  | ||||||
|                   })); |  | ||||||
|                 })).then(function () { |  | ||||||
| 
 |  | ||||||
|                   // hide wildcard if no wildcard
 |  | ||||||
|                   // hide http-01 and dns-01 if only wildcard
 |  | ||||||
|                   if (!obj.wildcard.length) { |  | ||||||
|                     $qs('.js-acme-wildcard-challenges').hidden = true; |  | ||||||
|                   } |  | ||||||
|                   if (!obj['http-01'].length) { |  | ||||||
|                     $qs('.js-acme-challenges').hidden = true; |  | ||||||
|                   } |  | ||||||
| 
 |  | ||||||
|                   updateChallengeType(); |  | ||||||
| 
 |  | ||||||
|                   console.log("MAGIC STEP NUMBER in 2 is:", i); |  | ||||||
|                   steps[i](); |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|               }); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       console.error('Step \'\' Error:'); |  | ||||||
|       console.error(err, err.stack); |  | ||||||
|       window.alert("An error happened at Step " + i + ", but it's not your fault. Email aj@rootprojects.org and let him know."); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   steps[3] = function () { |  | ||||||
|     updateProgress(1); |  | ||||||
|     hideForms(); |  | ||||||
|     $qs('.js-acme-form-challenges').hidden = false; |  | ||||||
|   }; |  | ||||||
|   steps[3].submit = function () { |  | ||||||
|     var chType; |  | ||||||
|     Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) { |  | ||||||
|       if ($el.checked) { |  | ||||||
|         chType = $el.value; |  | ||||||
|         return true; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     console.log('chType is:', chType); |  | ||||||
|     var chs = []; |  | ||||||
| 
 |  | ||||||
|     // do each wildcard, if any
 |  | ||||||
|     // do each challenge, by selected type only
 |  | ||||||
|     [ 'wildcard', chType].forEach(function (typ) { |  | ||||||
|       info.challenges[typ].forEach(function (ch) { |  | ||||||
|         // { jwk, challengeUrl, accountId (kid) }
 |  | ||||||
|         chs.push({ |  | ||||||
|           jwk: info.jwk |  | ||||||
|         , challengeUrl: ch.url |  | ||||||
|         , accountId: info.kid |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|     console.log("INFO.challenges !!!!!", info.challenges); |  | ||||||
| 
 |  | ||||||
|     var results = []; |  | ||||||
|     function nextChallenge() { |  | ||||||
|       var ch = chs.pop(); |  | ||||||
|       if (!ch) { return results; } |  | ||||||
|       return BACME.challenges.accept(ch).then(function (result) { |  | ||||||
|         results.push(result); |  | ||||||
|         return nextChallenge(); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // for now just show the next page immediately (its a spinner)
 |  | ||||||
|     steps[i](); |  | ||||||
|     return nextChallenge().then(function (results) { |  | ||||||
|       console.log('challenge status:', results); |  | ||||||
|       var polls = results.slice(0); |  | ||||||
|       var allsWell = true; |  | ||||||
| 
 |  | ||||||
|       function checkPolls() { |  | ||||||
|         return new PromiseA(function (resolve) { |  | ||||||
|           setTimeout(resolve, 1000); |  | ||||||
|         }).then(function () { |  | ||||||
|           return PromiseA.all(polls.map(function (poll) { |  | ||||||
|             return BACME.challenges.check({ challengePollUrl: poll.url }); |  | ||||||
|           })).then(function (polls) { |  | ||||||
|             console.log(polls); |  | ||||||
| 
 |  | ||||||
|             polls = polls.filter(function (poll) { |  | ||||||
|               //return 'valid' !== poll.status && 'invalid' !== poll.status;
 |  | ||||||
|               if ('pending' === poll.status) { |  | ||||||
|                 return true; |  | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               if ('invalid' === poll.status) { |  | ||||||
|                 allsWell = false; |  | ||||||
|                 window.alert("verification failed:" + poll.error.detail); |  | ||||||
|                 return; |  | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               if (poll.error) { |  | ||||||
|                 window.alert("verification failed:" + poll.error.detail); |  | ||||||
|                 return; |  | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               if ('valid' !== poll.status) { |  | ||||||
|                 allsWell = false; |  | ||||||
|                 console.warn('BAD POLL STATUS', poll); |  | ||||||
|                 window.alert("unknown error: " + JSON.stringify(poll, null, 2)); |  | ||||||
|               } |  | ||||||
|               // TODO show status in HTML
 |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             if (polls.length) { |  | ||||||
|               return checkPolls(); |  | ||||||
|             } |  | ||||||
|             return true; |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return checkPolls().then(function () { |  | ||||||
|         if (allsWell) { |  | ||||||
|           return submitForm(); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
 |  | ||||||
|   function spkiToPEM(keydata, pemName){ |  | ||||||
|       var keydataS = arrayBufferToString(keydata); |  | ||||||
|       var keydataB64 = window.btoa(keydataS); |  | ||||||
|       var keydataB64Pem = formatAsPem(keydataB64, pemName); |  | ||||||
|       return keydataB64Pem; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function arrayBufferToString( buffer ) { |  | ||||||
|       var binary = ''; |  | ||||||
|       var bytes = new Uint8Array( buffer ); |  | ||||||
|       var len = bytes.byteLength; |  | ||||||
|       for (var i = 0; i < len; i++) { |  | ||||||
|           binary += String.fromCharCode( bytes[ i ] ); |  | ||||||
|       } |  | ||||||
|       return binary; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   function formatAsPem(str, pemName) { |  | ||||||
|       var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n'; |  | ||||||
| 
 |  | ||||||
|       while(str.length > 0) { |  | ||||||
|           finalString += str.substring(0, 64) + '\n'; |  | ||||||
|           str = str.substring(64); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----'; |  | ||||||
| 
 |  | ||||||
|       return finalString; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // spinner
 |  | ||||||
|   steps[4] = function () { |  | ||||||
|     updateProgress(1); |  | ||||||
|     hideForms(); |  | ||||||
|     $qs('.js-acme-form-poll').hidden = false; |  | ||||||
|   }; |  | ||||||
|   steps[4].submit = function () { |  | ||||||
|     console.log('Congrats! Auto advancing...'); |  | ||||||
| 
 |  | ||||||
|     var key = info.identifiers.map(function (ident) { return ident.value; }).join(','); |  | ||||||
|     var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null'); |  | ||||||
|     var p; |  | ||||||
| 
 |  | ||||||
|     function createKeypair() { |  | ||||||
|       var opts; |  | ||||||
| 
 |  | ||||||
|       if (BROWSER_SUPPORTS_ECDSA) { |  | ||||||
|         opts = { type: 'ECDSA', bitlength: '256' }; |  | ||||||
|       } else { |  | ||||||
|         opts = { type: 'RSA', bitlength: '2048' }; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return BACME.domains.generateKeypair(opts).then(function (serverJwk) { |  | ||||||
|         localStorage.setItem('server:' + key, JSON.stringify(serverJwk)); |  | ||||||
|         return serverJwk; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (serverJwk) { |  | ||||||
|       p = PromiseA.resolve(serverJwk); |  | ||||||
|     } else { |  | ||||||
|       p = createKeypair(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return p.then(function (_serverJwk) { |  | ||||||
|       serverJwk = _serverJwk; |  | ||||||
|       info.serverJwk = serverJwk; |  | ||||||
|       // { serverJwk, domains }
 |  | ||||||
|       return BACME.orders.generateCsr({ |  | ||||||
|         serverJwk: serverJwk |  | ||||||
|       , domains: info.identifiers.map(function (ident) { |  | ||||||
|           return ident.value; |  | ||||||
|         }) |  | ||||||
|       }).then(function (csrweb64) { |  | ||||||
|         return BACME.orders.finalize({ |  | ||||||
|           csr: csrweb64 |  | ||||||
|         , jwk: info.jwk |  | ||||||
|         , finalizeUrl: info.finalizeUrl |  | ||||||
|         , accountId: info.kid |  | ||||||
|         }); |  | ||||||
|       }).then(function () { |  | ||||||
|         function checkCert() { |  | ||||||
|           return new PromiseA(function (resolve) { |  | ||||||
|             setTimeout(resolve, 1000); |  | ||||||
|           }).then(function () { |  | ||||||
|             return BACME.orders.check({ orderUrl: info.orderUrl }); |  | ||||||
|           }).then(function (reply) { |  | ||||||
|             if ('processing' === reply) { |  | ||||||
|               return checkCert(); |  | ||||||
|             } |  | ||||||
|             return reply; |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return checkCert(); |  | ||||||
|       }).then(function (reply) { |  | ||||||
|         return BACME.orders.receive({ certificateUrl: reply.certificate }); |  | ||||||
|       }).then(function (certs) { |  | ||||||
|         console.log('WINNING!'); |  | ||||||
|         console.log(certs); |  | ||||||
|         $qs('#js-fullchain').innerHTML = certs; |  | ||||||
|         $qs("#js-download-fullchain-link").href = |  | ||||||
|           "data:text/octet-stream;base64," + window.btoa(certs); |  | ||||||
| 
 |  | ||||||
|         var wcOpts; |  | ||||||
|         var pemName; |  | ||||||
|         if (/^R/.test(info.serverJwk.kty)) { |  | ||||||
|           pemName = 'RSA'; |  | ||||||
|           wcOpts = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }; |  | ||||||
|         } else { |  | ||||||
|           pemName = 'EC'; |  | ||||||
|           wcOpts = { name: "ECDSA", namedCurve: "P-256" }; |  | ||||||
|         } |  | ||||||
|         return crypto.subtle.importKey( |  | ||||||
|           "jwk" |  | ||||||
|         , info.serverJwk |  | ||||||
|         , wcOpts |  | ||||||
|         , true |  | ||||||
|         , ["sign"] |  | ||||||
|         ).then(function (privateKey) { |  | ||||||
|           return window.crypto.subtle.exportKey("pkcs8", privateKey); |  | ||||||
|         }).then (function (keydata) { |  | ||||||
|           var pem = spkiToPEM(keydata, pemName); |  | ||||||
|           $qs('#js-privkey').innerHTML = pem; |  | ||||||
|           $qs("#js-download-privkey-link").href = |  | ||||||
|             "data:text/octet-stream;base64," + window.btoa(pem); |  | ||||||
|           steps[i](); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       console.error(err.toString()); |  | ||||||
|       window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know."); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   steps[5] = function () { |  | ||||||
|     updateProgress(2); |  | ||||||
|     hideForms(); |  | ||||||
|     $qs('.js-acme-form-download').hidden = false; |  | ||||||
|   }; |  | ||||||
|   steps[1](); |  | ||||||
| 
 |  | ||||||
|   var params = new URLSearchParams(window.location.search); |  | ||||||
|   var apiType = params.get('acme-api-type') || "staging-v02"; |  | ||||||
| 
 |  | ||||||
|   if(params.has('acme-domains')) { |  | ||||||
|     console.log("acme-domains param: ", params.get('acme-domains')); |  | ||||||
|     $qs('.js-acme-domains').value = params.get('acme-domains'); |  | ||||||
| 
 |  | ||||||
|     $qsa('.js-acme-api-type').forEach(function(ele) { |  | ||||||
|       if(ele.value === apiType) { |  | ||||||
|         ele.checked = true; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     updateApiType(); |  | ||||||
|     steps[2](); |  | ||||||
|     submitForm(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   $qs('body').hidden = false; |  | ||||||
| }()); |  | ||||||
							
								
								
									
										699
									
								
								app/js/bacme.js
									
									
									
									
									
								
							
							
						
						
									
										699
									
								
								app/js/bacme.js
									
									
									
									
									
								
							| @ -1,699 +0,0 @@ | |||||||
| /*global CSR*/ |  | ||||||
| // CSR takes a while to load after the page load
 |  | ||||||
| (function (exports) { |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var BACME = exports.BACME = {}; |  | ||||||
| var webFetch = exports.fetch; |  | ||||||
| var webCrypto = exports.crypto; |  | ||||||
| var Promise = exports.Promise; |  | ||||||
| 
 |  | ||||||
| var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; |  | ||||||
| var directory; |  | ||||||
| 
 |  | ||||||
| var nonceUrl; |  | ||||||
| var nonce; |  | ||||||
| 
 |  | ||||||
| var accountKeypair; |  | ||||||
| var accountJwk; |  | ||||||
| 
 |  | ||||||
| var accountUrl; |  | ||||||
| 
 |  | ||||||
| BACME.challengePrefixes = { |  | ||||||
|   'http-01': '/.well-known/acme-challenge' |  | ||||||
| , 'dns-01': '_acme-challenge' |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME._logHeaders = function (resp) { |  | ||||||
|   console.log('Headers:'); |  | ||||||
|   Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME._logBody = function (body) { |  | ||||||
|   console.log('Body:'); |  | ||||||
|   console.log(JSON.stringify(body, null, 2)); |  | ||||||
|   console.log(''); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.directory = function (opts) { |  | ||||||
|   return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     return resp.json().then(function (reply) { |  | ||||||
|       if (/error/.test(reply.type)) { |  | ||||||
|         return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|       } |  | ||||||
|       directory = reply; |  | ||||||
|       nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce'; |  | ||||||
|       accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account'; |  | ||||||
|       orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; |  | ||||||
|       BACME._logBody(reply); |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.nonce = function () { |  | ||||||
|   return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
|     console.log('Nonce:', nonce); |  | ||||||
|     // resp.body is empty
 |  | ||||||
|     return resp.headers.get('replay-nonce'); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.accounts = {}; |  | ||||||
| 
 |  | ||||||
| // type = ECDSA
 |  | ||||||
| // bitlength = 256
 |  | ||||||
| BACME.accounts.generateKeypair = function (opts) { |  | ||||||
|   return BACME.generateKeypair(opts).then(function (result) { |  | ||||||
|     accountKeypair = result; |  | ||||||
| 
 |  | ||||||
|     return webCrypto.subtle.exportKey( |  | ||||||
|       "jwk" |  | ||||||
|     , result.privateKey |  | ||||||
|     ).then(function (privJwk) { |  | ||||||
| 
 |  | ||||||
|       accountJwk = privJwk; |  | ||||||
|       console.log('private jwk:'); |  | ||||||
|       console.log(JSON.stringify(privJwk, null, 2)); |  | ||||||
| 
 |  | ||||||
|       return privJwk; |  | ||||||
|       /* |  | ||||||
|       return webCrypto.subtle.exportKey( |  | ||||||
|         "pkcs8" |  | ||||||
|       , result.privateKey |  | ||||||
|       ).then(function (keydata) { |  | ||||||
|         console.log('pkcs8:'); |  | ||||||
|         console.log(Array.from(new Uint8Array(keydata))); |  | ||||||
| 
 |  | ||||||
|         return privJwk; |  | ||||||
|         //return accountKeypair;
 |  | ||||||
|       }); |  | ||||||
|       */ |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // json to url-safe base64
 |  | ||||||
| BACME._jsto64 = function (json) { |  | ||||||
|   return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var textEncoder = new TextEncoder(); |  | ||||||
| 
 |  | ||||||
| BACME._importKey = function (jwk) { |  | ||||||
|   var alg; // I think the 256 refers to the hash
 |  | ||||||
|   var wcOpts = {}; |  | ||||||
|   var extractable = true; // TODO make optionally false?
 |  | ||||||
|   var priv = jwk; |  | ||||||
|   var pub; |  | ||||||
| 
 |  | ||||||
|   // ECDSA
 |  | ||||||
|   if (/^EC/i.test(jwk.kty)) { |  | ||||||
|     wcOpts.name = 'ECDSA'; |  | ||||||
|     wcOpts.namedCurve = jwk.crv; |  | ||||||
|     alg = 'ES256'; |  | ||||||
|     pub = { |  | ||||||
|       crv: priv.crv |  | ||||||
|     , kty: priv.kty |  | ||||||
|     , x: priv.x |  | ||||||
|     , y: priv.y |  | ||||||
|     }; |  | ||||||
|     if (!priv.d) { |  | ||||||
|       priv = null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // RSA
 |  | ||||||
|   if (/^RS/i.test(jwk.kty)) { |  | ||||||
|     wcOpts.name = 'RSASSA-PKCS1-v1_5'; |  | ||||||
|     wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|     alg = 'RS256'; |  | ||||||
|     pub = { |  | ||||||
|       e: priv.e |  | ||||||
|     , kty: priv.kty |  | ||||||
|     , n: priv.n |  | ||||||
|     }; |  | ||||||
|     if (!priv.p) { |  | ||||||
|       priv = null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return window.crypto.subtle.importKey( |  | ||||||
|     "jwk" |  | ||||||
|   , pub |  | ||||||
|   , wcOpts |  | ||||||
|   , extractable |  | ||||||
|   , [ "verify" ] |  | ||||||
|   ).then(function (publicKey) { |  | ||||||
|     function give(privateKey) { |  | ||||||
|       return { |  | ||||||
|         wcPub: publicKey |  | ||||||
|       , wcKey: privateKey |  | ||||||
|       , wcKeypair: { publicKey: publicKey, privateKey: privateKey } |  | ||||||
|       , meta: { |  | ||||||
|           alg: alg |  | ||||||
|         , name: wcOpts.name |  | ||||||
|         , hash: wcOpts.hash |  | ||||||
|         } |  | ||||||
|       , jwk: jwk |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|     if (!priv) { |  | ||||||
|       return give(); |  | ||||||
|     } |  | ||||||
|     return window.crypto.subtle.importKey( |  | ||||||
|       "jwk" |  | ||||||
|     , priv |  | ||||||
|     , wcOpts |  | ||||||
|     , extractable |  | ||||||
|     , [ "sign"/*, "verify"*/ ] |  | ||||||
|     ).then(give); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| BACME._sign = function (opts) { |  | ||||||
|   var wcPrivKey = opts.abstractKey.wcKeypair.privateKey; |  | ||||||
|   var wcOpts = opts.abstractKey.meta; |  | ||||||
|   var alg = opts.abstractKey.meta.alg; // I think the 256 refers to the hash
 |  | ||||||
|   var signHash; |  | ||||||
| 
 |  | ||||||
|   console.log('kty', opts.abstractKey.jwk.kty); |  | ||||||
|   signHash = { name: "SHA-" + alg.replace(/[a-z]+/ig, '') }; |  | ||||||
| 
 |  | ||||||
|   var msg = textEncoder.encode(opts.protected64 + '.' + opts.payload64); |  | ||||||
|   console.log('msg:', msg); |  | ||||||
|   return window.crypto.subtle.sign( |  | ||||||
|     { name: wcOpts.name, hash: signHash } |  | ||||||
|   , wcPrivKey |  | ||||||
|   , msg |  | ||||||
|   ).then(function (signature) { |  | ||||||
|     //console.log('sig1:', signature);
 |  | ||||||
|     //console.log('sig2:', new Uint8Array(signature));
 |  | ||||||
|     //console.log('sig3:', Array.prototype.slice.call(new Uint8Array(signature)));
 |  | ||||||
|     // convert buffer to urlsafe base64
 |  | ||||||
|     var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { |  | ||||||
|       return String.fromCharCode(ch); |  | ||||||
|     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| 
 |  | ||||||
|     console.log('[1] URL-safe Base64 Signature:'); |  | ||||||
|     console.log(sig64); |  | ||||||
| 
 |  | ||||||
|     var signedMsg = { |  | ||||||
|       protected: opts.protected64 |  | ||||||
|     , payload: opts.payload64 |  | ||||||
|     , signature: sig64 |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     console.log('Signed Base64 Msg:'); |  | ||||||
|     console.log(JSON.stringify(signedMsg, null, 2)); |  | ||||||
| 
 |  | ||||||
|     return signedMsg; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| // email = john.doe@gmail.com
 |  | ||||||
| // jwk = { ... }
 |  | ||||||
| // agree = true
 |  | ||||||
| BACME.accounts.sign = function (opts) { |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
| 
 |  | ||||||
|     var payloadJson = |  | ||||||
|       { termsOfServiceAgreed: opts.agree |  | ||||||
|       , onlyReturnExisting: false |  | ||||||
|       , contact: opts.contacts || [ 'mailto:' + opts.email ] |  | ||||||
|       }; |  | ||||||
|     console.log('payload:'); |  | ||||||
|     console.log(payloadJson); |  | ||||||
|     var payload64 = BACME._jsto64( |  | ||||||
|       payloadJson |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     var protectedJson = |  | ||||||
|       { nonce: opts.nonce |  | ||||||
|       , url: accountUrl |  | ||||||
|       , alg: abstractKey.meta.alg |  | ||||||
|       , jwk: null |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|     if (/EC/i.test(opts.jwk.kty)) { |  | ||||||
|       protectedJson.jwk = { |  | ||||||
|         crv: opts.jwk.crv |  | ||||||
|       , kty: opts.jwk.kty |  | ||||||
|       , x: opts.jwk.x |  | ||||||
|       , y: opts.jwk.y |  | ||||||
|       }; |  | ||||||
|     } else if (/RS/i.test(opts.jwk.kty)) { |  | ||||||
|       protectedJson.jwk = { |  | ||||||
|         e: opts.jwk.e |  | ||||||
|       , kty: opts.jwk.kty |  | ||||||
|       , n: opts.jwk.n |  | ||||||
|       }; |  | ||||||
|     } else { |  | ||||||
|       return Promise.reject(new Error("[acme.accounts.sign] unsupported key type '" + opts.jwk.kty + "'")); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.log('protected:'); |  | ||||||
|     console.log(protectedJson); |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       protectedJson |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // Note: this function hashes before signing so send data, not the hash
 |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var accountId; |  | ||||||
| 
 |  | ||||||
| BACME.accounts.set = function (opts) { |  | ||||||
|   nonce = null; |  | ||||||
|   return window.fetch(accountUrl, { |  | ||||||
|     mode: 'cors' |  | ||||||
|   , method: 'POST' |  | ||||||
|   , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|   , body: JSON.stringify(opts.signedAccount) |  | ||||||
|   }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
|     accountId = resp.headers.get('location'); |  | ||||||
|     console.log('Next nonce:', nonce); |  | ||||||
|     console.log('Location/kid:', accountId); |  | ||||||
| 
 |  | ||||||
|     if (!resp.headers.get('content-type')) { |  | ||||||
|      console.log('Body: <none>'); |  | ||||||
| 
 |  | ||||||
|      return { kid: accountId }; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (result) { |  | ||||||
|       if (/^Error/i.test(result.detail)) { |  | ||||||
|         return Promise.reject(new Error(result.detail)); |  | ||||||
|       } |  | ||||||
|       result.kid = accountId; |  | ||||||
|       BACME._logBody(result); |  | ||||||
| 
 |  | ||||||
|       return result; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var orderUrl; |  | ||||||
| 
 |  | ||||||
| BACME.orders = {}; |  | ||||||
| 
 |  | ||||||
| // identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
 |  | ||||||
| // signedAccount
 |  | ||||||
| BACME.orders.sign = function (opts) { |  | ||||||
|   var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid } |  | ||||||
|     ); |  | ||||||
|     console.log('abstractKey:'); |  | ||||||
|     console.log(abstractKey); |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }).then(function (sig) { |  | ||||||
|       if (!sig) { |  | ||||||
|         throw new Error('sig is undefined... nonsense!'); |  | ||||||
|       } |  | ||||||
|       console.log('newsig', sig); |  | ||||||
|       return sig; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var currentOrderUrl; |  | ||||||
| var authorizationUrls; |  | ||||||
| var finalizeUrl; |  | ||||||
| 
 |  | ||||||
| BACME.orders.create = function (opts) { |  | ||||||
|   nonce = null; |  | ||||||
|   return window.fetch(orderUrl, { |  | ||||||
|     mode: 'cors' |  | ||||||
|   , method: 'POST' |  | ||||||
|   , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|   , body: JSON.stringify(opts.signedOrder) |  | ||||||
|   }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     currentOrderUrl = resp.headers.get('location'); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
|     console.log('Next nonce:', nonce); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (result) { |  | ||||||
|       if (/^Error/i.test(result.detail)) { |  | ||||||
|         return Promise.reject(new Error(result.detail)); |  | ||||||
|       } |  | ||||||
|       authorizationUrls = result.authorizations; |  | ||||||
|       finalizeUrl = result.finalize; |  | ||||||
|       BACME._logBody(result); |  | ||||||
| 
 |  | ||||||
|       result.url = currentOrderUrl; |  | ||||||
|       return result; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.challenges = {}; |  | ||||||
| BACME.challenges.all = function () { |  | ||||||
|   var challenges = []; |  | ||||||
| 
 |  | ||||||
|   function next() { |  | ||||||
|     if (!authorizationUrls.length) { |  | ||||||
|       return challenges; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return BACME.challenges.view().then(function (challenge) { |  | ||||||
|       challenges.push(challenge); |  | ||||||
|       return next(); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return next(); |  | ||||||
| }; |  | ||||||
| BACME.challenges.view = function () { |  | ||||||
|   var authzUrl = authorizationUrls.pop(); |  | ||||||
|   var token; |  | ||||||
|   var challengeDomain; |  | ||||||
|   var challengeUrl; |  | ||||||
| 
 |  | ||||||
|   return window.fetch(authzUrl, { |  | ||||||
|     mode: 'cors' |  | ||||||
|   }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (result) { |  | ||||||
|       // Note: select the challenge you wish to use
 |  | ||||||
|       var challenge = result.challenges.slice(0).pop(); |  | ||||||
|       token = challenge.token; |  | ||||||
|       challengeUrl = challenge.url; |  | ||||||
|       challengeDomain = result.identifier.value; |  | ||||||
| 
 |  | ||||||
|       BACME._logBody(result); |  | ||||||
| 
 |  | ||||||
|       return { |  | ||||||
|         challenges: result.challenges |  | ||||||
|       , expires: result.expires |  | ||||||
|       , identifier: result.identifier |  | ||||||
|       , status: result.status |  | ||||||
|       , wildcard: result.wildcard |  | ||||||
|       //, token: challenge.token
 |  | ||||||
|       //, url: challenge.url
 |  | ||||||
|       //, domain: result.identifier.value,
 |  | ||||||
|       }; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var thumbprint; |  | ||||||
| var keyAuth; |  | ||||||
| var httpPath; |  | ||||||
| var dnsAuth; |  | ||||||
| var dnsRecord; |  | ||||||
| 
 |  | ||||||
| BACME.thumbprint = function (opts) { |  | ||||||
|   // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 |  | ||||||
| 
 |  | ||||||
|   var accountJwk = opts.jwk; |  | ||||||
|   var keys; |  | ||||||
| 
 |  | ||||||
|   if (/^EC/i.test(opts.jwk.kty)) { |  | ||||||
|     keys = [ 'crv', 'kty', 'x', 'y' ]; |  | ||||||
|   } else if (/^RS/i.test(opts.jwk.kty)) { |  | ||||||
|     keys = [ 'e', 'kty', 'n' ]; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var accountPublicStr = '{' + keys.map(function (key) { |  | ||||||
|     return '"' + key + '":"' + accountJwk[key] + '"'; |  | ||||||
|   }).join(',') + '}'; |  | ||||||
| 
 |  | ||||||
|   return window.crypto.subtle.digest( |  | ||||||
|     { name: "SHA-256" } // SHA-256 is spec'd, non-optional
 |  | ||||||
|   , textEncoder.encode(accountPublicStr) |  | ||||||
|   ).then(function (hash) { |  | ||||||
|     thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |  | ||||||
|       return String.fromCharCode(ch); |  | ||||||
|     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| 
 |  | ||||||
|     console.log('Thumbprint:'); |  | ||||||
|     console.log(opts); |  | ||||||
|     console.log(accountPublicStr); |  | ||||||
|     console.log(thumbprint); |  | ||||||
| 
 |  | ||||||
|     return thumbprint; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // { token, thumbprint, challengeDomain }
 |  | ||||||
| BACME.challenges['http-01'] = function (opts) { |  | ||||||
|   // The contents of the key authorization file
 |  | ||||||
|   keyAuth = opts.token + '.' + opts.thumbprint; |  | ||||||
| 
 |  | ||||||
|   // Where the key authorization file goes
 |  | ||||||
|   httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token; |  | ||||||
| 
 |  | ||||||
|   console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     path: httpPath |  | ||||||
|   , value: keyAuth |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // { keyAuth }
 |  | ||||||
| BACME.challenges['dns-01'] = function (opts) { |  | ||||||
|   console.log('opts.keyAuth for DNS:'); |  | ||||||
|   console.log(opts.keyAuth); |  | ||||||
|   return window.crypto.subtle.digest( |  | ||||||
|     { name: "SHA-256", } |  | ||||||
|   , textEncoder.encode(opts.keyAuth) |  | ||||||
|   ).then(function (hash) { |  | ||||||
|     dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |  | ||||||
|       return String.fromCharCode(ch); |  | ||||||
|     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| 
 |  | ||||||
|     dnsRecord = '_acme-challenge.' + opts.challengeDomain; |  | ||||||
| 
 |  | ||||||
|     console.log('DNS TXT Auth:'); |  | ||||||
|     // The name of the record
 |  | ||||||
|     console.log(dnsRecord); |  | ||||||
|     // The TXT record value
 |  | ||||||
|     console.log(dnsAuth); |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|       type: 'TXT' |  | ||||||
|     , host: dnsRecord |  | ||||||
|     , answer: dnsAuth |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var challengePollUrl; |  | ||||||
| 
 |  | ||||||
| // { jwk, challengeUrl, accountId (kid) }
 |  | ||||||
| BACME.challenges.accept = function (opts) { |  | ||||||
|   var payload64 = BACME._jsto64({}); |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId } |  | ||||||
|     ); |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }); |  | ||||||
|   }).then(function (signedAccept) { |  | ||||||
| 
 |  | ||||||
|     nonce = null; |  | ||||||
|     return window.fetch( |  | ||||||
|       opts.challengeUrl |  | ||||||
|     , { mode: 'cors' |  | ||||||
|       , method: 'POST' |  | ||||||
|       , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|       , body: JSON.stringify(signedAccept) |  | ||||||
|       } |  | ||||||
|     ).then(function (resp) { |  | ||||||
|       BACME._logHeaders(resp); |  | ||||||
|       nonce = resp.headers.get('replay-nonce'); |  | ||||||
|       console.log("ACCEPT NONCE:", nonce); |  | ||||||
| 
 |  | ||||||
|       return resp.json().then(function (reply) { |  | ||||||
|         challengePollUrl = reply.url; |  | ||||||
| 
 |  | ||||||
|         console.log('Challenge ACK:'); |  | ||||||
|         console.log(JSON.stringify(reply)); |  | ||||||
|         return reply; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.challenges.check = function (opts) { |  | ||||||
|   return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (reply) { |  | ||||||
|       if (/error/.test(reply.type)) { |  | ||||||
|         return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|       } |  | ||||||
|       challengePollUrl = reply.url; |  | ||||||
| 
 |  | ||||||
|       BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var domainKeypair; |  | ||||||
| var domainJwk; |  | ||||||
| 
 |  | ||||||
| BACME.generateKeypair = function (opts) { |  | ||||||
|   var wcOpts = {}; |  | ||||||
| 
 |  | ||||||
|   // ECDSA has only the P curves and an associated bitlength
 |  | ||||||
|   if (/^EC/i.test(opts.type)) { |  | ||||||
|     wcOpts.name = 'ECDSA'; |  | ||||||
|     if (/256/.test(opts.bitlength)) { |  | ||||||
|       wcOpts.namedCurve = 'P-256'; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // RSA-PSS is another option, but I don't think it's used for Let's Encrypt
 |  | ||||||
|   // I think the hash is only necessary for signing, not generation or import
 |  | ||||||
|   if (/^RS/i.test(opts.type)) { |  | ||||||
|     wcOpts.name = 'RSASSA-PKCS1-v1_5'; |  | ||||||
|     wcOpts.modulusLength = opts.bitlength; |  | ||||||
|     if (opts.bitlength < 2048) { |  | ||||||
|       wcOpts.modulusLength = opts.bitlength * 8; |  | ||||||
|     } |  | ||||||
|     wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); |  | ||||||
|     wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|   } |  | ||||||
|   var extractable = true; |  | ||||||
|   return window.crypto.subtle.generateKey( |  | ||||||
|     wcOpts |  | ||||||
|   , extractable |  | ||||||
|   , [ 'sign', 'verify' ] |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| BACME.domains = {}; |  | ||||||
| // TODO factor out from BACME.accounts.generateKeypair even more
 |  | ||||||
| BACME.domains.generateKeypair = function (opts) { |  | ||||||
|   return BACME.generateKeypair(opts).then(function (result) { |  | ||||||
|     domainKeypair = result; |  | ||||||
| 
 |  | ||||||
|     return window.crypto.subtle.exportKey( |  | ||||||
|       "jwk" |  | ||||||
|     , result.privateKey |  | ||||||
|     ).then(function (privJwk) { |  | ||||||
| 
 |  | ||||||
|       domainJwk = privJwk; |  | ||||||
|       console.log('private jwk:'); |  | ||||||
|       console.log(JSON.stringify(privJwk, null, 2)); |  | ||||||
| 
 |  | ||||||
|       return privJwk; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // { serverJwk, domains }
 |  | ||||||
| BACME.orders.generateCsr = function (opts) { |  | ||||||
|   return BACME._importKey(opts.serverJwk).then(function (abstractKey) { |  | ||||||
|     return Promise.resolve(CSR.generate({ keypair: abstractKey.wcKeypair, domains: opts.domains })); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var certificateUrl; |  | ||||||
| 
 |  | ||||||
| // { csr, jwk, finalizeUrl, accountId }
 |  | ||||||
| BACME.orders.finalize = function (opts) { |  | ||||||
|   var payload64 = BACME._jsto64( |  | ||||||
|     { csr: opts.csr } |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId } |  | ||||||
|     ); |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }); |  | ||||||
|   }).then(function (signedFinal) { |  | ||||||
| 
 |  | ||||||
|     nonce = null; |  | ||||||
|     return window.fetch( |  | ||||||
|       opts.finalizeUrl |  | ||||||
|     , { mode: 'cors' |  | ||||||
|       , method: 'POST' |  | ||||||
|       , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|       , body: JSON.stringify(signedFinal) |  | ||||||
|       } |  | ||||||
|     ).then(function (resp) { |  | ||||||
|       BACME._logHeaders(resp); |  | ||||||
|       nonce = resp.headers.get('replay-nonce'); |  | ||||||
| 
 |  | ||||||
|       return resp.json().then(function (reply) { |  | ||||||
|         if (/error/.test(reply.type)) { |  | ||||||
|           return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|         } |  | ||||||
|         certificateUrl = reply.certificate; |  | ||||||
|         BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|         return reply; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.orders.receive = function (opts) { |  | ||||||
|   return window.fetch( |  | ||||||
|     opts.certificateUrl |  | ||||||
|   , { mode: 'cors' |  | ||||||
|     , method: 'GET' |  | ||||||
|     } |  | ||||||
|   ).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
| 
 |  | ||||||
|     return resp.text().then(function (reply) { |  | ||||||
|       BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.orders.check = function (opts) { |  | ||||||
|   return window.fetch( |  | ||||||
|     opts.orderUrl |  | ||||||
|   , { mode: 'cors' |  | ||||||
|     , method: 'GET' |  | ||||||
|     } |  | ||||||
|   ).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (reply) { |  | ||||||
|       if (/error/.test(reply.type)) { |  | ||||||
|         return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|       } |  | ||||||
|       BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| }(window)); |  | ||||||
| @ -20,6 +20,15 @@ | |||||||
|   var steps = {}; |   var steps = {}; | ||||||
|   var i = 1; |   var i = 1; | ||||||
|   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; |   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; | ||||||
|  | 
 | ||||||
|  |   // fix previous browsers
 | ||||||
|  |   var isCurrent = (localStorage.getItem('version') === VERSION); | ||||||
|  |   if (!isCurrent) { | ||||||
|  |     localStorage.clear(); | ||||||
|  |     localStorage.setItem('version', VERSION); | ||||||
|  |   } | ||||||
|  |   localStorage.setItem('version', VERSION); | ||||||
|  | 
 | ||||||
|   var challenges = { |   var challenges = { | ||||||
|     'http-01': { |     'http-01': { | ||||||
|       set: function (auth) { |       set: function (auth) { | ||||||
| @ -120,13 +129,6 @@ | |||||||
| 		return Keypairs.generate(RSA_OPTS); | 		return Keypairs.generate(RSA_OPTS); | ||||||
|   } |   } | ||||||
|   function testKeypairSupport() { |   function testKeypairSupport() { | ||||||
| 		// fix previous browsers
 |  | ||||||
| 		var isCurrent = (localStorage.getItem('version') === VERSION); |  | ||||||
| 		if (!isCurrent) { |  | ||||||
| 			localStorage.clear(); |  | ||||||
| 			localStorage.setItem('version', VERSION); |  | ||||||
| 		} |  | ||||||
| 		localStorage.setItem('version', VERSION); |  | ||||||
| 
 | 
 | ||||||
|     return testRsaSupport().then(function () { |     return testRsaSupport().then(function () { | ||||||
|       console.info("[crypto] RSA is supported"); |       console.info("[crypto] RSA is supported"); | ||||||
| @ -221,7 +223,7 @@ | |||||||
|     $qs('.js-acme-form-domains').hidden = false; |     $qs('.js-acme-form-domains').hidden = false; | ||||||
|   }; |   }; | ||||||
|   steps[1].submit = function () { |   steps[1].submit = function () { | ||||||
|     info.domains = $qs('.js-acme-domains').value.replace(/https?:\/\//g, ' ').replace(/,/g, ' ').trim().split(/\s+/g); |     info.domains = $qs('.js-acme-domains').value.replace(/https?:\/\//g, ' ').replace(/[,+]/g, ' ').trim().split(/\s+/g); | ||||||
|     info.identifiers = info.domains.map(function (hostname) { |     info.identifiers = info.domains.map(function (hostname) { | ||||||
|       return { type: 'dns', value: hostname.toLowerCase().trim() }; |       return { type: 'dns', value: hostname.toLowerCase().trim() }; | ||||||
|     }).slice(0,1); //Disable multiple values for now.  We'll just take the first and work with it.
 |     }).slice(0,1); //Disable multiple values for now.  We'll just take the first and work with it.
 | ||||||
|  | |||||||
| @ -45,7 +45,7 @@ | |||||||
|           Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org. |           Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org. | ||||||
|           Please enable Javascript before continuing. |           Please enable Javascript before continuing. | ||||||
|         </div> |         </div> | ||||||
|         <form id="js-acme-form" action="./app/" method=> |         <form id="js-acme-form" action="./app/" method="GET"> | ||||||
|           <div class="domain-psuedo-input"> |           <div class="domain-psuedo-input"> | ||||||
|             <span class="secure-green">Secure</span> | <span class="secure-green">https:</span>//<input aria-label="domains to secure" id="acme-domains" type="text" name="acme-domains" placeholder="Your domain name" required> |             <span class="secure-green">Secure</span> | <span class="secure-green">https:</span>//<input aria-label="domains to secure" id="acme-domains" type="text" name="acme-domains" placeholder="Your domain name" required> | ||||||
|           </div> |           </div> | ||||||
|  | |||||||
| @ -1,5 +1,11 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
| 
 | 
 | ||||||
|  | mkdir -p app/js/ | ||||||
|  | pushd app/js/ | ||||||
|  |   wget -c https://rootprojects.org/acme/bluecrypt-acme.js | ||||||
|  |   wget -c https://rootprojects.org/acme/bluecrypt-acme.min.js | ||||||
|  | popd | ||||||
|  | 
 | ||||||
| mkdir -p app/js/pkijs.org/v1.3.33/ | mkdir -p app/js/pkijs.org/v1.3.33/ | ||||||
| pushd app/js/pkijs.org/v1.3.33/ | pushd app/js/pkijs.org/v1.3.33/ | ||||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js |   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js | ||||||
|  | |||||||
| @ -2,11 +2,11 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
|   var $qs = function (s) { return window.document.querySelector(s); }; |   var $qs = function (s) { return window.document.querySelector(s); }; | ||||||
|   var $qsa = function (s) { return window.document.querySelectorAll(s); }; |  | ||||||
| 
 | 
 | ||||||
|   $qs('.js-javascript-warning').hidden = true; |   $qs('.js-javascript-warning').hidden = true; | ||||||
| 
 | 
 | ||||||
|   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; |   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; | ||||||
|  | 
 | ||||||
|   function updateApiType() { |   function updateApiType() { | ||||||
|     var formData = new FormData($qs("#js-acme-form")); |     var formData = new FormData($qs("#js-acme-form")); | ||||||
| 
 | 
 | ||||||
| @ -15,13 +15,15 @@ | |||||||
|     var value = formData.get("acme-api-type"); |     var value = formData.get("acme-api-type"); | ||||||
|     $qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value); |     $qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   $qs('#js-acme-form').addEventListener('change', updateApiType); |   $qs('#js-acme-form').addEventListener('change', updateApiType); | ||||||
|  |   //$qs('#js-acme-form').addEventListener('submit', prettyRedirect);
 | ||||||
| 
 | 
 | ||||||
|   updateApiType(); |   updateApiType(); | ||||||
|   try { |   try { | ||||||
|     document.fonts.load().then(function() { |     document.fonts.load().then(function() { | ||||||
|       $qs('body').classList.add("js-app-ready"); |       $qs('body').classList.add("js-app-ready"); | ||||||
|     }).catch(function(error) { |     }).catch(function(e) { | ||||||
|       $qs('body').classList.add("js-app-ready"); |       $qs('body').classList.add("js-app-ready"); | ||||||
|     }); |     }); | ||||||
|   } catch(e) { |   } catch(e) { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user