WIP: moving to bluecrypt
This commit is contained in:
		
							parent
							
								
									98c8db8973
								
							
						
					
					
						commit
						7484ffcd11
					
				
							
								
								
									
										494
									
								
								app/js/greenlock.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										494
									
								
								app/js/greenlock.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,494 @@ | ||||
| (function () { | ||||
| 'use strict'; | ||||
| 
 | ||||
|   /*global URLSearchParams,Headers*/ | ||||
|   var VERSION = '2'; | ||||
| 	// ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses )
 | ||||
| 	// ECDSA P-384, P-521, and RSA 3072, 4096 are NOT recommend standards (and not properly supported)
 | ||||
|   var BROWSER_SUPPORTS_RSA; | ||||
| 	var ECDSA_OPTS = { kty: 'EC', namedCurve: 'P-256' }; | ||||
| 	var RSA_OPTS = { kty: 'RSA', modulusLength: 2048 }; | ||||
|   var Promise = window.Promise; | ||||
|   var Keypairs = window.Keypairs; | ||||
|   var ACME = window.ACME; | ||||
|   var CSR = window.CSR; | ||||
|   var $qs = function (s) { return window.document.querySelector(s); }; | ||||
|   var $qsa = function (s) { return window.document.querySelectorAll(s); }; | ||||
| 	var acme; | ||||
| 	var accountStuff; | ||||
|   var info = {}; | ||||
|   var steps = {}; | ||||
|   var i = 1; | ||||
|   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); | ||||
|   } | ||||
| 
 | ||||
|   function hideForms() { | ||||
|     $qsa('.js-acme-form').forEach(function (el) { | ||||
|       el.hidden = true; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function updateProgress(currentStep) { | ||||
|     var progressSteps = $qs("#js-progress-bar").children; | ||||
| 		var j; | ||||
|     for (j = 0; j < progressSteps.length; j += 1) { | ||||
|       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."); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function testEcdsaSupport() { | ||||
| 		/* | ||||
| 			var opts = { | ||||
| 				kty: $('input[name="kty"]:checked').value | ||||
| 			, namedCurve: $('input[name="ec-crv"]:checked').value | ||||
| 			, modulusLength: $('input[name="rsa-len"]:checked').value | ||||
| 			}; | ||||
| 		*/ | ||||
|   } | ||||
|   function testRsaSupport() { | ||||
| 		return Keypairs.generate(RSA_OPTS); | ||||
|   } | ||||
|   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 () { | ||||
|       console.info("[crypto] RSA is supported"); | ||||
|       BROWSER_SUPPORTS_RSA = true; | ||||
|       return BROWSER_SUPPORTS_RSA; | ||||
|     }).catch(function () { | ||||
|       console.warn("[crypto] RSA is NOT fully supported"); | ||||
|       BROWSER_SUPPORTS_RSA = false; | ||||
|       return BROWSER_SUPPORTS_RSA; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function getServerKeypair() { | ||||
|     var sortedAltnames = info.identifiers.map(function (ident) { return ident.value; }).sort().join(','); | ||||
|     var serverJwk = JSON.parse(localStorage.getItem('server:' + sortedAltnames) || 'null'); | ||||
|     if (serverJwk) { | ||||
|       return PromiseA.resolve(serverJwk); | ||||
|     } | ||||
| 
 | ||||
|     var keypairOpts; | ||||
|     // TODO allow for user preference
 | ||||
|     if (BROWSER_SUPPORTS_RSA) { | ||||
|       keypairOpts = RSA_OPTS; | ||||
|     } else { | ||||
|       keypairOpts = ECDSA_OPTS; | ||||
|     } | ||||
| 
 | ||||
|     return Keypairs.generate(RSA_OPTS).catch(function (err) { | ||||
|       console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):"); | ||||
|       throw err; | ||||
| 		}).then(function (pair) { | ||||
| 			localStorage.setItem('server:'+sortedAltnames, JSON.stringify(pair.private)); | ||||
| 			return pair.private; | ||||
| 		}); | ||||
|   } | ||||
| 
 | ||||
| 	function getAccountKeypair(email) { | ||||
| 		var json = localStorage.getItem('account:'+email); | ||||
| 		if (json) { | ||||
| 			return Promise.resolve(JSON.parse(json)); | ||||
| 		} | ||||
| 
 | ||||
| 		return Keypairs.generate(ECDSA_OPTS).catch(function (err) { | ||||
| 			console.warn("[Error] Keypairs.generate(" + JSON.stringify(ECDSA_OPTS) + "):\n", err); | ||||
| 			return Keypairs.generate(RSA_OPTS).catch(function (err) { | ||||
| 				console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):"); | ||||
| 				throw err; | ||||
| 			}); | ||||
| 		}).then(function (pair) { | ||||
| 			localStorage.setItem('account:'+email, JSON.stringify(pair.private)); | ||||
| 			return pair.private; | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
|   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; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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' | ||||
| 			, timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone | ||||
|       , 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; } | ||||
|     }); | ||||
| 
 | ||||
| 		var acmeDirectoryUrl = $qs('.js-acme-directory-url').value; | ||||
| 		acme = ACME.create({ Keypairs: Keypairs, CSR: CSR }); | ||||
| 		return acme.init(acmeDirectoryUrl).then(function (directory) { | ||||
|       $qs('.js-acme-tos-url').href = directory.meta.termsOfService; | ||||
| 			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 ping with version and account creation
 | ||||
|     setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; })); | ||||
| 
 | ||||
| 		function checkTos(tos) { | ||||
| 			if (info.agree) { | ||||
| 				return tos; | ||||
| 			} else { | ||||
| 				return ''; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|     return getAccountKeypair(email).then(function (jwk) { | ||||
|       // TODO save account id rather than always retrieving it?
 | ||||
| 			return acme.accounts.create({ | ||||
| 				email: email | ||||
| 			, agreeToTerms: checkTos | ||||
| 			, accountKeypair: { privateKeyJwk: jwk } | ||||
| 			}).then(function (account) { | ||||
| 				console.log("account created result:", account); | ||||
| 				accountStuff.account = account; | ||||
| 				accountStuff.privateJwk = jwk; | ||||
| 				accountStuff.email = email; | ||||
| 				accountStuff.acme = acme; // TODO XXX remove
 | ||||
| 			}).catch(function (err) { | ||||
| 				console.error("A bad thing happened:"); | ||||
| 				console.error(err); | ||||
| 				window.alert(err.message || JSON.stringify(err, null, 2)); | ||||
| 				return new Promise(function () { | ||||
|  					// stop the process cold
 | ||||
| 					console.warn('TODO: resume at ask email?'); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}).then(function () { | ||||
|       var jwk = accountStuff.privateJwk; | ||||
|       var account = accountStuff.account; | ||||
| 
 | ||||
| 			return acme.orders.create({ | ||||
| 			  account: account | ||||
| 			, accountKeypair: { privateKeyJwk: jwk } | ||||
| 			, identifiers: info.identifiers | ||||
| 			}).then(function (order) { | ||||
| 				return acme.orders.create({ | ||||
| 					signedOrder: signedOrder | ||||
| 				}).then(function (order) { | ||||
| 					accountStuff.order = order; | ||||
|           var claims = order.challenges; | ||||
|           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' | ||||
|           }; | ||||
|           options.challengePriority = [ 'http-01', 'dns-01' ]; | ||||
| 
 | ||||
|           // 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 () { | ||||
|     options.challengeTypes = [ 'dns-01' ]; | ||||
|     if ('http-01' === $qs('.js-acme-challenge-type:checked').value) { | ||||
|       options.challengeTypes.unshift('http-01'); | ||||
|     } | ||||
|     console.log('primary challenge type is:', options.challengeTypes[0]); | ||||
| 
 | ||||
|     return getAccountKeypair(email).then(function (jwk) { | ||||
|       // for now just show the next page immediately (its a spinner)
 | ||||
|       // TODO put a test challenge in the list
 | ||||
|       // TODO warn about wait-time if DNS
 | ||||
|       steps[i](); | ||||
| 		  return getServerKeypair().then(function () { | ||||
|         return acme.orders.finalize({ | ||||
|           account: accountStuff.account | ||||
|         , accountKeypair: { privateKeyJwk: jwk } | ||||
|         , order: accountStuff.order | ||||
|         , domainKeypair: 'TODO' | ||||
|         }); | ||||
|       }).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](); | ||||
|         }); | ||||
|       }); | ||||
|     }).then(function () { | ||||
|       return submitForm(); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   // spinner
 | ||||
|   steps[4] = function () { | ||||
|     updateProgress(1); | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-poll').hidden = false; | ||||
|   }; | ||||
|   steps[4].submit = function () { | ||||
|     console.log('Congrats! Auto advancing...'); | ||||
| 
 | ||||
| 
 | ||||
|     }).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"; | ||||
| 
 | ||||
|   $qsa('.js-acme-api-type').forEach(function ($el) { | ||||
|     $el.addEventListener('change', updateApiType); | ||||
|   }); | ||||
| 
 | ||||
|   updateApiType(); | ||||
| 
 | ||||
|   $qsa('.js-acme-form').forEach(function ($el) { | ||||
|     $el.addEventListener('submit', function (ev) { | ||||
|       ev.preventDefault(); | ||||
|       submitForm(ev); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   $qsa('.js-acme-challenge-type').forEach(function ($el) { | ||||
|     $el.addEventListener('change', updateChallengeType); | ||||
|   }); | ||||
| 
 | ||||
|   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; | ||||
| 
 | ||||
|   return testKeypairSupport().then(function (rsaSupport) { | ||||
|     if (rsaSupport) { | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return testRsaSupport().then(function () { | ||||
|       console.info('[crypto] RSA is supported'); | ||||
|     }).catch(function (err) { | ||||
|       console.error('[crypto] could not use either RSA nor EC.'); | ||||
|       console.error(err); | ||||
|       window.alert("Generating secure certificates requires a browser with cryptography support." | ||||
| 				+ "Please consider a recent version of Chrome, Firefox, or Safari."); | ||||
| 			throw err; | ||||
|     }); | ||||
|   }); | ||||
| }()); | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user