diff --git a/app/index.html b/app/index.html index 72771c3..5bb4e95 100644 --- a/app/index.html +++ b/app/index.html @@ -153,9 +153,10 @@ -->
- Why do we need your email? We link your SSL certificates to the - email you use so you can manage your certificates in the future, - and get important email updates about them. + Why do we need your email? + We link your SSL certificates to the email you use so that you'll + be notified before the certificate expires and so you can manage + your certificates in the future.
diff --git a/app/js/app.js b/app/js/app.js deleted file mode 100644 index 95ab751..0000000 --- a/app/js/app.js +++ /dev/null @@ -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; -}()); diff --git a/app/js/bacme.js b/app/js/bacme.js deleted file mode 100644 index fb63042..0000000 --- a/app/js/bacme.js +++ /dev/null @@ -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: '); - - 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)); diff --git a/app/js/greenlock.js b/app/js/greenlock.js index dafc748..02442a8 100644 --- a/app/js/greenlock.js +++ b/app/js/greenlock.js @@ -20,6 +20,15 @@ var steps = {}; var i = 1; 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 = { 'http-01': { set: function (auth) { @@ -120,13 +129,6 @@ 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"); @@ -221,7 +223,7 @@ $qs('.js-acme-form-domains').hidden = false; }; 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) { 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. diff --git a/index.html b/index.html index 6874cc6..4bd744c 100644 --- a/index.html +++ b/index.html @@ -45,7 +45,7 @@ Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org. Please enable Javascript before continuing. -
+
Secure | https://
diff --git a/install.sh b/install.sh index 214b92d..59a2e29 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,11 @@ #!/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/ pushd app/js/pkijs.org/v1.3.33/ wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js diff --git a/js/app.js b/js/app.js index bab617d..c4e2cb4 100644 --- a/js/app.js +++ b/js/app.js @@ -2,11 +2,11 @@ 'use strict'; var $qs = function (s) { return window.document.querySelector(s); }; - var $qsa = function (s) { return window.document.querySelectorAll(s); }; $qs('.js-javascript-warning').hidden = true; var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; + function updateApiType() { var formData = new FormData($qs("#js-acme-form")); @@ -15,13 +15,15 @@ var value = formData.get("acme-api-type"); $qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value); } + $qs('#js-acme-form').addEventListener('change', updateApiType); + //$qs('#js-acme-form').addEventListener('submit', prettyRedirect); updateApiType(); try { document.fonts.load().then(function() { $qs('body').classList.add("js-app-ready"); - }).catch(function(error) { + }).catch(function(e) { $qs('body').classList.add("js-app-ready"); }); } catch(e) {