(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.ppl.family/api/ppl.family/public/list', { method: 'POST' , cors: true , headers: new Headers({ 'Content-Type': 'application/json' }) , body: JSON.stringify({ address: email , list: 'web@greenlock.domains' , 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; }());