diff --git a/.gitignore b/.gitignore index 1ddfc8e..2842410 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -js/pkijs.org -js/browser-csr +app/js/pkijs.org +app/js/browser-csr diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..3ba101c --- /dev/null +++ b/app/index.html @@ -0,0 +1,237 @@ + + + Greenlock™ + + + + +
+
+ +
+
+

Get the green lock for your website

+ + +
+

+

Certificates are valid for 90 days. Renewal is free :)

+ +
+ + +
+
+
+ + +
+ +
+ + + + + +
+ +

How will you validate your domain?

+
+ +
+ +
+ +
+ +

Verify Domains & Sub-Domains

+ + + + + + + + + + + + + + + + +
HostnameFile LocationFile Contents
example.com.well-known/acme-challenge/xxxsec.ret
+ + + + + + + + + + + + + + + + +
HostnameTXT HostTXT Value
example.com_acme-challenge.example.com4A54
+
+ +
+

Verify Wildcard Domains

+ + + + + + + + + + + + + + + + +
HostnameTXT HostTXT Value
example.com_acme-challenge.example.com4A54
+
+ + +
+ + +
+ Verifying Domains... (give us 5 seconds or so...) + + +
+ + +
+
+

+ +
+ +
+

+ +
+ +
+

node.js https server example

+
'use strict';
+
+    var https = require('https');
+    var server = https.createServer({
+      key: require('fs').readFileSync('./privkey.pem')
+    , cert: require('fs').readFileSync('./fullchain.pem')
+    }, function (req, res) {
+      res.end("Hello, World!");
+    }).listen(443, function () {
+      console.log('Listening on', this.address());
+    })
+    
+
+ + +
+ +
+
+
+
+

+ View Source (git) + +
+ + + + + + + + + + + + + +
+
+ + diff --git a/app/js/app.js b/app/js/app.js new file mode 100644 index 0000000..3bcf59b --- /dev/null +++ b/app/js/app.js @@ -0,0 +1,514 @@ +(function () { +'use strict'; + + 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 apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; + function updateApiType() { + 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 submitForm(ev) { + var j = i; + i += 1; + steps[j].submit(ev); + } + $qsa('.js-acme-form').forEach(function ($el) { + $el.addEventListener('submit', function (ev) { + ev.preventDefault(); + submitForm(ev); + }); + }); + function updateChallengeType() { + 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-table-wildcard').hidden = true; + $qs('.js-acme-table-http-01').hidden = true; + $qs('.js-acme-table-dns-01').hidden = true; + if (info.challenges.wildcard) { + $qs('.js-acme-table-wildcard').hidden = false; + } + if (info.challenges[input.value]) { + $qs('.js-acme-table-' + 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, comment: 'greenlock sub for ' + domains.join(',') }) + }).then(function (resp) { + return resp.json().then(function (data) { + /* + if (data.error) { + window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead."); + return; + } + */ + }); + }, function () { + /* + window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead."); + */ + }); + } + + steps[1] = function () { + 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() }; + }); + 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 () { + 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() { + return BACME.accounts.generateKeypair({ + type: 'ECDSA' + , bitlength: '256' + }).then(function (jwk) { + localStorage.setItem('account:' + email, JSON.stringify(jwk)); + return jwk; + }) + } + + if (jwk) { + p = Promise.resolve(jwk); + } else { + p = 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 = Promise.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': [] }; + var map = { + 'http-01': '.js-acme-table-http-01' + , 'dns-01': '.js-acme-table-dns-01' + , 'wildcard': '.js-acme-table-wildcard' + } + var tpls = {}; + info.challenges = obj; + 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 Promise.all(claims.map(function (claim) { + var hostname = claim.identifier.value; + return Promise.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); + $qs(map.wildcard).innerHTML += '' + data.hostname + '' + data.dnsHost + '' + data.dnsAnswer + ''; + } else if(obj[data.type]) { + + obj[data.type].push(data); + if ('dns-01' === data.type) { + $qs(map[data.type]).innerHTML += '' + data.hostname + '' + data.dnsHost + '' + data.dnsAnswer + ''; + } else if ('http-01' === data.type) { + $qs(map[data.type]).innerHTML += '' + data.hostname + '' + data.httpPath + '' + data.httpAuth + ''; + } + } + + }); + + })); + })).then(function () { + + // hide wildcard if no wildcard + // hide http-01 and dns-01 if only wildcard + if (!obj.wildcard.length) { + $qs('.js-acme-wildcard').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 \'' + i + '\' Error:'); + console.error(err); + }); + }; + + steps[3] = function () { + 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 Promise(function (resolve) { + setTimeout(resolve, 1000); + }).then(function () { + return Promise.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 ('valid' !== poll.status) { + allsWell = false; + console.warn('BAD POLL STATUS', poll); + } + // TODO show status in HTML + }); + + if (polls.length) { + return checkPolls(); + } + return true; + }); + }); + } + + return checkPolls().then(function () { + if (allsWell) { + return submitForm(); + } + }); + }); + }; + + // spinner + steps[4] = function () { + 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() { + return BACME.accounts.generateKeypair({ + type: 'ECDSA' + , bitlength: '256' + }).then(function (serverJwk) { + localStorage.setItem('server:' + key, JSON.stringify(serverJwk)); + return serverJwk; + }) + } + + if (serverJwk) { + p = Promise.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 Promise(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').value = certs; + + // https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format + function spkiToPEM(keydata){ + var keydataS = arrayBufferToString(keydata); + var keydataB64 = window.btoa(keydataS); + var keydataB64Pem = formatAsPem(keydataB64); + 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) { + 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; + } + + 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); + $qs('.js-privkey').value = pem; + steps[i](); + }).catch(function(err){ + console.error(err); + }); + }); + }); + }; + + steps[5] = function () { + hideForms(); + $qs('.js-acme-form-download').hidden = false; + } + + steps[1](); + + $qs('body').hidden = false; +}()); diff --git a/js/bacme.js b/app/js/bacme.js similarity index 100% rename from js/bacme.js rename to app/js/bacme.js diff --git a/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2 b/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2 new file mode 100644 index 0000000..efa300c Binary files /dev/null and b/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2 differ diff --git a/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2 b/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2 new file mode 100644 index 0000000..52b6d69 Binary files /dev/null and b/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2 differ diff --git a/index.html b/index.html index 3ba101c..f3e154a 100644 --- a/index.html +++ b/index.html @@ -2,236 +2,93 @@ Greenlock™ - + + + + + + + + + + + + - + +
+
-

Get the green lock for your website

- +

Get the green lock for your website

+
+
-
-

-

Certificates are valid for 90 days. Renewal is free :)

- -
- - -
-
-
- - -
- -
- - - - - -
- -

How will you validate your domain?

-
- -
- -
- -
- -

Verify Domains & Sub-Domains

- - - - - - - - - - - - - - - - -
HostnameFile LocationFile Contents
example.com.well-known/acme-challenge/xxxsec.ret
- - - - - - - - - - - - - - - - -
HostnameTXT HostTXT Value
example.com_acme-challenge.example.com4A54
-
- -
-

Verify Wildcard Domains

- - - - - - - - - - - - - - - - -
HostnameTXT HostTXT Value
example.com_acme-challenge.example.com4A54
-
- - -
- - -
- Verifying Domains... (give us 5 seconds or so...) - - -
- - -
-
-

- -
- -
-

- + +
+ Secure | https://
- -
-

node.js https server example

-
'use strict';
-
-    var https = require('https');
-    var server = https.createServer({
-      key: require('fs').readFileSync('./privkey.pem')
-    , cert: require('fs').readFileSync('./fullchain.pem')
-    }, function (req, res) {
-      res.end("Hello, World!");
-    }).listen(443, function () {
-      console.log('Listening on', this.address());
-    })
-    
+ +
Domain, subdomain, or wildcard domain
+ +
+ + + +
+ View Source (git) +
- - - -
-
-
-
-

- View Source (git) - -
- - - - - - - - - - - - -
+
+
+

Why you need SSL certificates

+ If your website doesn't have the green lock from an SSL Certificate, Google Chrome will soon label your website as not secure. +
+
+ + + + + + +
diff --git a/install.sh b/install.sh index 968f06a..214b92d 100644 --- a/install.sh +++ b/install.sh @@ -1,14 +1,14 @@ #!/bin/bash -mkdir -p js/pkijs.org/v1.3.33/ -pushd js/pkijs.org/v1.3.33/ +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 wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_schema.js wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.js wget -c https://raw.githubusercontent.com/PeculiarVentures/ASN1.js/f7181c21c61e53a940ea24373ab489ad86d51bc1/org/pkijs/asn1.js popd -mkdir -p js/browser-csr/v1.0.0-alpha/ -pushd js/browser-csr/v1.0.0-alpha/ +mkdir -p app/js/browser-csr/v1.0.0-alpha/ +pushd app/js/browser-csr/v1.0.0-alpha/ wget -c https://git.coolaj86.com/coolaj86/browser-csr.js/raw/commit/01cdc0e91b5bf03f12e1b25b4129e3cde927987c/csr.js popd diff --git a/js/app.js b/js/app.js index 3bcf59b..c1f9a1c 100644 --- a/js/app.js +++ b/js/app.js @@ -3,512 +3,26 @@ 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 apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; function updateApiType() { - 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(); + var formData = new FormData($qs("#js-acme-form")); - function hideForms() { - $qsa('.js-acme-form').forEach(function (el) { - el.hidden = true; - }); - } + console.log('ACME api type radio:'); - function submitForm(ev) { - var j = i; - i += 1; - steps[j].submit(ev); - } - $qsa('.js-acme-form').forEach(function ($el) { - $el.addEventListener('submit', function (ev) { - ev.preventDefault(); - submitForm(ev); - }); - }); - function updateChallengeType() { - 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-table-wildcard').hidden = true; - $qs('.js-acme-table-http-01').hidden = true; - $qs('.js-acme-table-dns-01').hidden = true; - if (info.challenges.wildcard) { - $qs('.js-acme-table-wildcard').hidden = false; - } - if (info.challenges[input.value]) { - $qs('.js-acme-table-' + 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, comment: 'greenlock sub for ' + domains.join(',') }) - }).then(function (resp) { - return resp.json().then(function (data) { - /* - if (data.error) { - window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead."); - return; - } - */ - }); - }, function () { - /* - window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead."); - */ - }); + 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); - steps[1] = function () { - 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() }; - }); - 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 () { - 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() { - return BACME.accounts.generateKeypair({ - type: 'ECDSA' - , bitlength: '256' - }).then(function (jwk) { - localStorage.setItem('account:' + email, JSON.stringify(jwk)); - return jwk; - }) - } - - if (jwk) { - p = Promise.resolve(jwk); - } else { - p = 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 = Promise.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': [] }; - var map = { - 'http-01': '.js-acme-table-http-01' - , 'dns-01': '.js-acme-table-dns-01' - , 'wildcard': '.js-acme-table-wildcard' - } - var tpls = {}; - info.challenges = obj; - 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 Promise.all(claims.map(function (claim) { - var hostname = claim.identifier.value; - return Promise.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); - $qs(map.wildcard).innerHTML += '' + data.hostname + '' + data.dnsHost + '' + data.dnsAnswer + ''; - } else if(obj[data.type]) { - - obj[data.type].push(data); - if ('dns-01' === data.type) { - $qs(map[data.type]).innerHTML += '' + data.hostname + '' + data.dnsHost + '' + data.dnsAnswer + ''; - } else if ('http-01' === data.type) { - $qs(map[data.type]).innerHTML += '' + data.hostname + '' + data.httpPath + '' + data.httpAuth + ''; - } - } - - }); - - })); - })).then(function () { - - // hide wildcard if no wildcard - // hide http-01 and dns-01 if only wildcard - if (!obj.wildcard.length) { - $qs('.js-acme-wildcard').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 \'' + i + '\' Error:'); - console.error(err); - }); - }; - - steps[3] = function () { - 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 Promise(function (resolve) { - setTimeout(resolve, 1000); - }).then(function () { - return Promise.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 ('valid' !== poll.status) { - allsWell = false; - console.warn('BAD POLL STATUS', poll); - } - // TODO show status in HTML - }); - - if (polls.length) { - return checkPolls(); - } - return true; - }); - }); - } - - return checkPolls().then(function () { - if (allsWell) { - return submitForm(); - } - }); - }); - }; - - // spinner - steps[4] = function () { - 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() { - return BACME.accounts.generateKeypair({ - type: 'ECDSA' - , bitlength: '256' - }).then(function (serverJwk) { - localStorage.setItem('server:' + key, JSON.stringify(serverJwk)); - return serverJwk; - }) - } - - if (serverJwk) { - p = Promise.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 Promise(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').value = certs; - - // https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format - function spkiToPEM(keydata){ - var keydataS = arrayBufferToString(keydata); - var keydataB64 = window.btoa(keydataS); - var keydataB64Pem = formatAsPem(keydataB64); - 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) { - 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; - } - - 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); - $qs('.js-privkey').value = pem; - steps[i](); - }).catch(function(err){ - console.error(err); - }); - }); - }); - }; - - steps[5] = function () { - hideForms(); - $qs('.js-acme-form-download').hidden = false; + updateApiType(); + try { + document.fonts.load().then(function() { + $qs('body').classList.add("js-app-ready"); + }).catch(function(error) { + $qs('body').classList.add("js-app-ready"); + }); + } catch(e) { + setTimeout(function() {$qs('body').classList.add("js-app-ready");}, 200); } - - steps[1](); - - $qs('body').hidden = false; }()); diff --git a/styles/main.css b/styles/main.css index d426873..e7d11e8 100644 --- a/styles/main.css +++ b/styles/main.css @@ -1,5 +1,105 @@ .column-row { display: flex; flex-direction: column; + text-align: center; align-items: center; +} + +body { + position: relative; + margin-top: 5.777777778em; + min-height: 36em; + font-size: 18px; + font-family: 'Source Sans Pro', sans-serif; + font-stretch: normal; + line-height: 1.33; + letter-spacing: -0.4px; + color: #1a1a1a; + opacity: 0; +} + +h1 { + font-size: 2.666666667em; + max-width: 8em; + text-align: center; +} + +input { + font-size: 1em; + padding: 0.444444444em; + border: solid #d9d9d9 1px; + border-radius: 2px; + font-family: inherit; +} + +button { + padding: 0.444444444em 1.2em; + font-size: 1em; + background-color: #5bc17f; + border: solid 1px #5bc17f; + border-radius: 2px; + font-weight: normal; + font-stretch: normal; + letter-spacing: -0.4px; + font-family: inherit; + text-align: center; + color: white; + height: 40px; + line-height: 1.13; +} + +.acme-advanced-fields { + position: absolute; + bottom: 0; + padding: 1em; + text-align: center; +} + +.domain-subtext { + font-size: 0.833333333em; + color: #666; + text-align: center; + margin: 0.5em; +} + +input#acme-domains:before { + content: "Secure | https://"; +} + +.domain-psuedo-input { + display: inline-block; + margin-right: .6666667em; + border: solid #d9d9d9 1px; + border-radius: 2px; + padding: 0.44444444em; + color: #d9d9d9; +} + +input#acme-domains { + border: none; + padding: 0; + padding-right: 0; + width: 17.2222222em; + color: #222; +} + +input#acme-domains:focus { + outline: none; +} + +span.secure-green { + color: #5bc17f; +} + +.why-you-need { + width: 26.555556em; +} + +body.js-app-ready { + transition: opacity 0.2s; + opacity: 1; +} + +.acme-advanced-fields > * { + margin: 0 0.5em; } \ No newline at end of file