Переглянути джерело

Some styling updates and created a separate front page.

master
John Shaver 1 рік тому
джерело
коміт
7af2671993

+ 2
- 2
.gitignore Переглянути файл

@@ -1,2 +1,2 @@
1
-js/pkijs.org
2
-js/browser-csr
1
+app/js/pkijs.org
2
+app/js/browser-csr

+ 237
- 0
app/index.html Переглянути файл

@@ -0,0 +1,237 @@
1
+<html>
2
+  <head>
3
+    <title>Greenlock&trade;</title>
4
+    <meta property="og:image" content="https://greenlock.ppl.family/img/greenlock-mark-400x400.png" />
5
+    <link href="style/main.css" rel="stylesheet">
6
+  </head>
7
+  <body hidden>
8
+    <div class="column-container wide">
9
+      <div class="column-row">
10
+        <img src="img/greenlock-146.png">
11
+      </div>
12
+      <div class="column-row">
13
+          <h1>Get the green lock for your website</h1>
14
+
15
+        <!-- Step 1 Choose Domain(s) -->
16
+        <form class="js-acme-form js-acme-form-domains">
17
+          <h1><label>What's your domain?</label></h1>
18
+          <h4>Certificates are valid for 90 days. Renewal is free :)</h4>
19
+          <input class="js-acme-domains" type="text" placeholder="example.com,*.example.com" required>
20
+          <br>
21
+          <button type="submit">Next</button>
22
+
23
+          <br>
24
+          <br>
25
+          <br>
26
+          <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="v02" checked required>
27
+            Production</label>
28
+          <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="staging-v02" required>
29
+            Testing</label>
30
+          <br>
31
+          <input class="js-acme-directory-url" type="url" placeholder="ACME directory url">
32
+        </form>
33
+
34
+        <!-- Step 2 Create Account -->
35
+        <form class="js-acme-form js-acme-form-account">
36
+          <h1><label>What's your email?</label></h1>
37
+          <input class="js-acme-account-email" type="email" placeholder="john@doe.family" required>
38
+          <br>
39
+          <br>
40
+          <label><input class="js-acme-account-tos" type="checkbox" required>
41
+            Agree to <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt&trade; Terms of Service</a>?</label>
42
+          <br>
43
+          <br>
44
+          <label><input class="js-greenlock-account-tos" type="checkbox" required>
45
+            Agree to <a class="js-gl-tos" target="_blank" href="./legal.html">Greenlock&trade; Terms of Service</a>?</label>
46
+          <br>
47
+          <br>
48
+          <!--
49
+          <a href="#">advanced (use existing account)</a>
50
+          <br>
51
+          <br>
52
+          -->
53
+          <button type="submit">Next</button>
54
+        </form>
55
+
56
+        <!-- Step 3 Set Challanges -->
57
+        <form class="js-acme-form js-acme-form-challenges">
58
+
59
+          <h1>How will you validate your domain?</h1>
60
+          <br>
61
+          <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="http-01" checked required>
62
+            File Upload to HTTP Web Server</label>
63
+          <br>
64
+          <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="dns-01" required>
65
+            TXT Records on DNS Name Server</label>
66
+          <br>
67
+
68
+          <div class="js-acme-challenges">
69
+
70
+          <h2>Verify Domains &amp; Sub-Domains</h2>
71
+
72
+          <table class="js-acme-table-http-01">
73
+            <thead>
74
+              <tr>
75
+                <th>Hostname</th>
76
+                <th>File Location</th>
77
+                <th>File Contents</th>
78
+              </tr>
79
+            </thead>
80
+            <tbody>
81
+              <tr>
82
+                <td>example.com</td>
83
+                <td>.well-known/acme-challenge/xxx</td>
84
+                <td>sec.ret</td>
85
+              </tr>
86
+            </tbody>
87
+          </table>
88
+
89
+          <table class="js-acme-table-dns-01">
90
+            <thead>
91
+              <tr>
92
+                <th>Hostname</th>
93
+                <th>TXT Host</th>
94
+                <th>TXT Value</th>
95
+              </tr>
96
+            </thead>
97
+            <tbody>
98
+              <tr>
99
+                <td>example.com</td>
100
+                <td>_acme-challenge.example.com</td>
101
+                <td>4A54</td>
102
+              </tr>
103
+            </tbody>
104
+          </table>
105
+          </div>
106
+
107
+          <div class="js-acme-wildcard">
108
+            <h2>Verify Wildcard Domains</h2>
109
+
110
+            <table class="js-acme-table-wildcard">
111
+              <thead>
112
+                <tr>
113
+                  <th>Hostname</th>
114
+                  <th>TXT Host</th>
115
+                  <th>TXT Value</th>
116
+                </tr>
117
+              </thead>
118
+              <tbody>
119
+                <tr>
120
+                  <td>example.com</td>
121
+                  <td>_acme-challenge.example.com</td>
122
+                  <td>4A54</td>
123
+                </tr>
124
+              </tbody>
125
+            </table>
126
+          </div>
127
+
128
+          <button type="submit">Next</button>
129
+        </form>
130
+
131
+        <!-- Step 4 Process Challanges -->
132
+        <form class="js-acme-form js-acme-form-poll">
133
+          Verifying Domains... (give us 5 seconds or so...)
134
+
135
+          <!--
136
+          <table class="js-acme-table-verifying">
137
+            <thead>
138
+              <tr>
139
+                <th>Hostname</th>
140
+                <th>Type</th>
141
+                <th>Pass</th>
142
+              </tr>
143
+            </thead>
144
+            <tbody>
145
+              <tr>
146
+                <td>example.com</td>
147
+                <td>http-01</td>
148
+                <td>-</td>
149
+              </tr>
150
+            </tbody>
151
+          </table>
152
+
153
+          <a href="#">advanced (use existing keypair for domain)</a>
154
+
155
+          <button type="submit">Next</button>
156
+          -->
157
+        </form>
158
+
159
+        <!-- Step 5 Get Certs -->
160
+        <form class="js-acme-form js-acme-form-download">
161
+          <div>
162
+          <h2><label>privkey.pem</label></h2>
163
+          <textarea cols="80" rows="10" class="js-privkey">-</textarea>
164
+          </div>
165
+
166
+          <div>
167
+          <h2><label>fullchain.pem</label></h2>
168
+          <textarea cols="80" rows="60" class="js-fullchain">-</textarea>
169
+          </div>
170
+
171
+          <div>
172
+          <h3>node.js https server example</h3>
173
+          <pre><code>'use strict';
174
+
175
+    var https = require('https');
176
+    var server = https.createServer({
177
+      key: require('fs').readFileSync('./privkey.pem')
178
+    , cert: require('fs').readFileSync('./fullchain.pem')
179
+    }, function (req, res) {
180
+      res.end("Hello, World!");
181
+    }).listen(443, function () {
182
+      console.log('Listening on', this.address());
183
+    })
184
+    </code></pre>
185
+          </div>
186
+
187
+          <!--
188
+            TODO
189
+          <label>cert.pem</label>
190
+          <textarea class="js-cert">-</textarea>
191
+
192
+          <label>chain.pem</label>
193
+          <textarea class="js-chain">-</textarea>
194
+
195
+          <button type="button">Download SSL Certificates</button>
196
+          <br>
197
+          <a href="#">Advanced (copy and paste)</a>
198
+          <br>
199
+          <button type="submit">Start Over</button>
200
+          -->
201
+        </form>
202
+
203
+          <br>
204
+          <br>
205
+          <br>
206
+          <div><small>
207
+          <h3></h3>
208
+          <a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git)
209
+          <!-- or
210
+          <pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre>
211
+          Or view the live site code (same as live-site branch):
212
+          <pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
213
+          -->
214
+          </small></div>
215
+
216
+        <script src="./js/bacme.js"></script>
217
+        <script src="./js/app.js"></script>
218
+
219
+        <script src="./js/pkijs.org/v1.3.33/common.js"></script>
220
+        <script src="./js/pkijs.org/v1.3.33/asn1.js"></script>
221
+        <script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script>
222
+        <script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script>
223
+        <script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script>
224
+
225
+        <!-- Global site tag (gtag.js) - Google Analytics -->
226
+        <script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>
227
+        <script>
228
+          window.dataLayer = window.dataLayer || [];
229
+          function gtag(){dataLayer.push(arguments);}
230
+          gtag('js', new Date());
231
+
232
+          gtag('config', 'UA-118745161-2');
233
+        </script>
234
+      </div>
235
+    </div>
236
+  </body>
237
+</html>

+ 514
- 0
app/js/app.js Переглянути файл

@@ -0,0 +1,514 @@
1
+(function () {
2
+'use strict';
3
+
4
+  var $qs = function (s) { return window.document.querySelector(s); };
5
+  var $qsa = function (s) { return window.document.querySelectorAll(s); };
6
+  var info = {};
7
+  var steps = {};
8
+  var nonce;
9
+  var kid;
10
+  var i = 1;
11
+
12
+  var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
13
+  function updateApiType() {
14
+    var input = this || Array.prototype.filter.call(
15
+      $qsa('.js-acme-api-type'), function ($el) { return $el.checked; }
16
+    )[0];
17
+    console.log('ACME api type radio:', input.value);
18
+    $qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value);
19
+  }
20
+  $qsa('.js-acme-api-type').forEach(function ($el) {
21
+    $el.addEventListener('change', updateApiType);
22
+  });
23
+  updateApiType();
24
+
25
+  function hideForms() {
26
+    $qsa('.js-acme-form').forEach(function (el) {
27
+      el.hidden = true;
28
+    });
29
+  }
30
+
31
+  function submitForm(ev) {
32
+    var j = i;
33
+    i += 1;
34
+    steps[j].submit(ev);
35
+  }
36
+  $qsa('.js-acme-form').forEach(function ($el) {
37
+    $el.addEventListener('submit', function (ev) {
38
+      ev.preventDefault();
39
+      submitForm(ev);
40
+    });
41
+  });
42
+  function updateChallengeType() {
43
+    var input = this || Array.prototype.filter.call(
44
+      $qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; }
45
+    )[0];
46
+    console.log('ch type radio:', input.value);
47
+    $qs('.js-acme-table-wildcard').hidden = true;
48
+    $qs('.js-acme-table-http-01').hidden = true;
49
+    $qs('.js-acme-table-dns-01').hidden = true;
50
+    if (info.challenges.wildcard) {
51
+      $qs('.js-acme-table-wildcard').hidden = false;
52
+    }
53
+    if (info.challenges[input.value]) {
54
+      $qs('.js-acme-table-' + input.value).hidden = false;
55
+    }
56
+  }
57
+  $qsa('.js-acme-challenge-type').forEach(function ($el) {
58
+    $el.addEventListener('change', updateChallengeType);
59
+  });
60
+
61
+  function saveContact(email, domains) {
62
+    // to be used for good, not evil
63
+    return window.fetch('https://api.ppl.family/api/ppl.family/public/list', {
64
+      method: 'POST'
65
+    , cors: true
66
+    , headers: new Headers({ 'Content-Type': 'application/json' })
67
+    , body: JSON.stringify({ address: email, comment: 'greenlock sub for ' + domains.join(',') })
68
+    }).then(function (resp) {
69
+      return resp.json().then(function (data) {
70
+        /*
71
+        if (data.error) {
72
+          window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead.");
73
+          return;
74
+        }
75
+        */
76
+      });
77
+    }, function () {
78
+      /*
79
+      window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead.");
80
+      */
81
+    });
82
+  }
83
+
84
+  steps[1] = function () {
85
+    hideForms();
86
+    $qs('.js-acme-form-domains').hidden = false;
87
+  };
88
+  steps[1].submit = function () {
89
+    info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) {
90
+      return { type: 'dns', value: hostname.toLowerCase().trim() };
91
+    });
92
+    info.identifiers.sort(function (a, b) {
93
+      if (a === b) { return 0; }
94
+      if (a < b) { return 1; }
95
+      if (a > b) { return -1; }
96
+    });
97
+
98
+    return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) {
99
+      $qs('.js-acme-tos-url').href = directory.meta.termsOfService;
100
+      return BACME.nonce().then(function (_nonce) {
101
+        nonce = _nonce;
102
+
103
+        console.log("MAGIC STEP NUMBER in 1 is:", i);
104
+        steps[i]();
105
+      });
106
+    });
107
+  };
108
+
109
+  steps[2] = function () {
110
+    hideForms();
111
+    $qs('.js-acme-form-account').hidden = false;
112
+  };
113
+  steps[2].submit = function () {
114
+    var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
115
+
116
+
117
+    info.contact = [ 'mailto:' + email ];
118
+    info.agree = $qs('.js-acme-account-tos').checked;
119
+    info.greenlockAgree = $qs('.js-gl-tos').checked;
120
+    // TODO
121
+    // options for
122
+    // * regenerate key
123
+    // * ECDSA / RSA / bitlength
124
+
125
+    // TODO ping with version and account creation
126
+    setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
127
+
128
+    var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
129
+    var p;
130
+
131
+    function createKeypair() {
132
+      return BACME.accounts.generateKeypair({
133
+        type: 'ECDSA'
134
+      , bitlength: '256'
135
+      }).then(function (jwk) {
136
+        localStorage.setItem('account:' + email, JSON.stringify(jwk));
137
+        return jwk;
138
+      })
139
+    }
140
+
141
+    if (jwk) {
142
+      p = Promise.resolve(jwk);
143
+    } else {
144
+      p = createKeypair();
145
+    }
146
+
147
+    function createAccount(jwk) {
148
+      console.log('account jwk:');
149
+      console.log(jwk);
150
+      delete jwk.key_ops;
151
+      info.jwk = jwk;
152
+      return BACME.accounts.sign({
153
+        jwk: jwk
154
+      , contacts: [ 'mailto:' + email ]
155
+      , agree: info.agree
156
+      , nonce: nonce
157
+      , kid: kid
158
+      }).then(function (signedAccount) {
159
+        return BACME.accounts.set({
160
+          signedAccount: signedAccount
161
+        }).then(function (account) {
162
+          console.log('account:');
163
+          console.log(account);
164
+          kid = account.kid;
165
+          return kid;
166
+        });
167
+      });
168
+    }
169
+
170
+    return p.then(function (_jwk) {
171
+      jwk = _jwk;
172
+      kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
173
+      var p2
174
+
175
+      // TODO save account id rather than always retrieving it
176
+      if (kid) {
177
+        p2 = Promise.resolve(kid);
178
+      } else {
179
+        p2 = createAccount(jwk);
180
+      }
181
+
182
+      return p2.then(function (_kid) {
183
+        kid = _kid;
184
+        info.kid = kid;
185
+        return BACME.orders.sign({
186
+          jwk: jwk
187
+        , identifiers: info.identifiers
188
+        , kid: kid
189
+        }).then(function (signedOrder) {
190
+          return BACME.orders.create({
191
+            signedOrder: signedOrder
192
+          }).then(function (order) {
193
+            info.finalizeUrl = order.finalize;
194
+            info.orderUrl = order.url; // from header Location ???
195
+            return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) {
196
+              return BACME.challenges.all().then(function (claims) {
197
+                console.log('claims:');
198
+                console.log(claims);
199
+                var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
200
+                var map = {
201
+                  'http-01': '.js-acme-table-http-01'
202
+                , 'dns-01': '.js-acme-table-dns-01'
203
+                , 'wildcard': '.js-acme-table-wildcard'
204
+                }
205
+                var tpls = {};
206
+                info.challenges = obj;
207
+                Object.keys(map).forEach(function (k) {
208
+                  var sel = map[k] + ' tbody';
209
+                  console.log(sel);
210
+                  tpls[k] = $qs(sel).innerHTML;
211
+                  $qs(map[k] + ' tbody').innerHTML = '';
212
+                });
213
+
214
+                // TODO make Promise-friendly
215
+                return Promise.all(claims.map(function (claim) {
216
+                  var hostname = claim.identifier.value;
217
+                  return Promise.all(claim.challenges.map(function (c) {
218
+                    var keyAuth = BACME.challenges['http-01']({
219
+                      token: c.token
220
+                    , thumbprint: thumbprint
221
+                    , challengeDomain: hostname
222
+                    });
223
+                    return BACME.challenges['dns-01']({
224
+                      keyAuth: keyAuth.value
225
+                    , challengeDomain: hostname
226
+                    }).then(function (dnsAuth) {
227
+                      var data = {
228
+                        type: c.type
229
+                      , hostname: hostname
230
+                      , url: c.url
231
+                      , token: c.token
232
+                      , keyAuthorization: keyAuth
233
+                      , httpPath: keyAuth.path
234
+                      , httpAuth: keyAuth.value
235
+                      , dnsType: dnsAuth.type
236
+                      , dnsHost: dnsAuth.host
237
+                      , dnsAnswer: dnsAuth.answer
238
+                      };
239
+
240
+                      console.log('');
241
+                      console.log('CHALLENGE');
242
+                      console.log(claim);
243
+                      console.log(c);
244
+                      console.log(data);
245
+                      console.log('');
246
+
247
+                      if (claim.wildcard) {
248
+                        obj.wildcard.push(data);
249
+                        $qs(map.wildcard).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
250
+                      } else if(obj[data.type]) {
251
+
252
+                        obj[data.type].push(data);
253
+                        if ('dns-01' === data.type) {
254
+                          $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
255
+                        } else if ('http-01' === data.type) {
256
+                          $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.httpPath + '</td><td>' + data.httpAuth + '</td></tr>';
257
+                        }
258
+                      }
259
+
260
+                    });
261
+
262
+                  }));
263
+                })).then(function () {
264
+
265
+                  // hide wildcard if no wildcard
266
+                  // hide http-01 and dns-01 if only wildcard
267
+                  if (!obj.wildcard.length) {
268
+                    $qs('.js-acme-wildcard').hidden = true;
269
+                  }
270
+                  if (!obj['http-01'].length) {
271
+                    $qs('.js-acme-challenges').hidden = true;
272
+                  }
273
+
274
+                  updateChallengeType();
275
+
276
+                  console.log("MAGIC STEP NUMBER in 2 is:", i);
277
+                  steps[i]();
278
+                });
279
+
280
+              });
281
+            });
282
+          });
283
+        });
284
+      });
285
+    }).catch(function (err) {
286
+      console.error('Step \'' + i + '\' Error:');
287
+      console.error(err);
288
+    });
289
+  };
290
+
291
+  steps[3] = function () {
292
+    hideForms();
293
+    $qs('.js-acme-form-challenges').hidden = false;
294
+  };
295
+  steps[3].submit = function () {
296
+    var chType;
297
+    Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) {
298
+      if ($el.checked) {
299
+        chType = $el.value;
300
+        return true;
301
+      }
302
+    });
303
+    console.log('chType is:', chType);
304
+    var chs = [];
305
+
306
+    // do each wildcard, if any
307
+    // do each challenge, by selected type only
308
+    [ 'wildcard', chType].forEach(function (typ) {
309
+      info.challenges[typ].forEach(function (ch) {
310
+        // { jwk, challengeUrl, accountId (kid) }
311
+        chs.push({
312
+          jwk: info.jwk
313
+        , challengeUrl: ch.url
314
+        , accountId: info.kid
315
+        });
316
+      });
317
+    });
318
+    console.log("INFO.challenges !!!!!", info.challenges);
319
+
320
+    var results = [];
321
+    function nextChallenge() {
322
+      var ch = chs.pop();
323
+      if (!ch) { return results; }
324
+      return BACME.challenges.accept(ch).then(function (result) {
325
+        results.push(result);
326
+        return nextChallenge();
327
+      });
328
+    }
329
+
330
+    // for now just show the next page immediately (its a spinner)
331
+    steps[i]();
332
+    return nextChallenge().then(function (results) {
333
+      console.log('challenge status:', results);
334
+      var polls = results.slice(0);
335
+      var allsWell = true;
336
+
337
+      function checkPolls() {
338
+        return new Promise(function (resolve) {
339
+          setTimeout(resolve, 1000);
340
+        }).then(function () {
341
+          return Promise.all(polls.map(function (poll) {
342
+            return BACME.challenges.check({ challengePollUrl: poll.url });
343
+          })).then(function (polls) {
344
+            console.log(polls);
345
+
346
+            polls = polls.filter(function (poll) {
347
+              //return 'valid' !== poll.status && 'invalid' !== poll.status;
348
+              if ('pending' === poll.status) {
349
+                return true;
350
+              }
351
+              if ('valid' !== poll.status) {
352
+                allsWell = false;
353
+                console.warn('BAD POLL STATUS', poll);
354
+              }
355
+              // TODO show status in HTML
356
+            });
357
+
358
+            if (polls.length) {
359
+              return checkPolls();
360
+            }
361
+            return true;
362
+          });
363
+        });
364
+      }
365
+
366
+      return checkPolls().then(function () {
367
+        if (allsWell) {
368
+          return submitForm();
369
+        }
370
+      });
371
+    });
372
+  };
373
+
374
+  // spinner
375
+  steps[4] = function () {
376
+    hideForms();
377
+    $qs('.js-acme-form-poll').hidden = false;
378
+  }
379
+  steps[4].submit = function () {
380
+    console.log('Congrats! Auto advancing...');
381
+
382
+    var key = info.identifiers.map(function (ident) { return ident.value; }).join(',');
383
+    var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null');
384
+    var p;
385
+
386
+    function createKeypair() {
387
+      return BACME.accounts.generateKeypair({
388
+        type: 'ECDSA'
389
+      , bitlength: '256'
390
+      }).then(function (serverJwk) {
391
+        localStorage.setItem('server:' + key, JSON.stringify(serverJwk));
392
+        return serverJwk;
393
+      })
394
+    }
395
+
396
+    if (serverJwk) {
397
+      p = Promise.resolve(serverJwk);
398
+    } else {
399
+      p = createKeypair();
400
+    }
401
+
402
+    return p.then(function (_serverJwk) {
403
+      serverJwk = _serverJwk;
404
+      info.serverJwk = serverJwk;
405
+      // { serverJwk, domains }
406
+      return BACME.orders.generateCsr({
407
+        serverJwk: serverJwk
408
+      , domains: info.identifiers.map(function (ident) {
409
+          return ident.value;
410
+        })
411
+      }).then(function (csrweb64) {
412
+        return BACME.orders.finalize({
413
+          csr: csrweb64
414
+        , jwk: info.jwk
415
+        , finalizeUrl: info.finalizeUrl
416
+        , accountId: info.kid
417
+        });
418
+      }).then(function () {
419
+        function checkCert() {
420
+          return new Promise(function (resolve) {
421
+            setTimeout(resolve, 1000);
422
+          }).then(function () {
423
+            return BACME.orders.check({ orderUrl: info.orderUrl });
424
+          }).then(function (reply) {
425
+            if ('processing' === reply) {
426
+              return checkCert();
427
+            }
428
+            return reply;
429
+          });
430
+        }
431
+
432
+        return checkCert();
433
+      }).then(function (reply) {
434
+        return BACME.orders.receive({ certificateUrl: reply.certificate });
435
+      }).then(function (certs) {
436
+        console.log('WINNING!');
437
+        console.log(certs);
438
+        $qs('.js-fullchain').value = certs;
439
+
440
+        // https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
441
+				function spkiToPEM(keydata){
442
+						var keydataS = arrayBufferToString(keydata);
443
+						var keydataB64 = window.btoa(keydataS);
444
+						var keydataB64Pem = formatAsPem(keydataB64);
445
+						return keydataB64Pem;
446
+				}
447
+
448
+				function arrayBufferToString( buffer ) {
449
+						var binary = '';
450
+						var bytes = new Uint8Array( buffer );
451
+						var len = bytes.byteLength;
452
+						for (var i = 0; i < len; i++) {
453
+								binary += String.fromCharCode( bytes[ i ] );
454
+						}
455
+						return binary;
456
+				}
457
+
458
+
459
+				function formatAsPem(str) {
460
+						var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n';
461
+
462
+						while(str.length > 0) {
463
+								finalString += str.substring(0, 64) + '\n';
464
+								str = str.substring(64);
465
+						}
466
+
467
+						finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----';
468
+
469
+						return finalString;
470
+				}
471
+
472
+        var wcOpts;
473
+        var pemName;
474
+        if (/^R/.test(info.serverJwk.kty)) {
475
+          pemName = 'RSA';
476
+          wcOpts = {
477
+            name: "RSASSA-PKCS1-v1_5"
478
+          , hash: { name: "SHA-256" }
479
+          };
480
+        } else {
481
+          pemName = 'EC';
482
+          wcOpts = {
483
+            name: "ECDSA"
484
+          , namedCurve: "P-256"
485
+          }
486
+        }
487
+				return crypto.subtle.importKey(
488
+          "jwk"
489
+        , info.serverJwk
490
+        , wcOpts
491
+        , true
492
+        , ["sign"]
493
+				).then(function (privateKey) {
494
+				  return window.crypto.subtle.exportKey("pkcs8", privateKey);
495
+				}).then (function (keydata) {
496
+					var pem = spkiToPEM(keydata);
497
+					$qs('.js-privkey').value = pem;
498
+          steps[i]();
499
+				}).catch(function(err){
500
+					console.error(err);
501
+				});
502
+      });
503
+    });
504
+  };
505
+
506
+  steps[5] = function () {
507
+    hideForms();
508
+    $qs('.js-acme-form-download').hidden = false;
509
+  }
510
+
511
+  steps[1]();
512
+
513
+  $qs('body').hidden = false;
514
+}());

js/bacme.js → app/js/bacme.js Переглянути файл


BIN
fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2 Переглянути файл


BIN
fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2 Переглянути файл


+ 76
- 219
index.html Переглянути файл

@@ -2,236 +2,93 @@
2 2
   <head>
3 3
     <title>Greenlock&trade;</title>
4 4
     <meta property="og:image" content="https://greenlock.ppl.family/img/greenlock-mark-400x400.png" />
5
-    <link href="style/main.css" rel="stylesheet">
5
+    <link href="styles/main.css" rel="stylesheet">
6
+    <style>
7
+      @font-face {
8
+        font-family: 'Source Sans Pro';
9
+        font-style: normal;
10
+        font-display: block;
11
+        font-weight: 400;
12
+        src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2) format('woff2');
13
+        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
14
+      }
15
+      @font-face {
16
+        font-family: 'Source Sans Pro';
17
+        font-style: normal;
18
+        font-weight: 700;
19
+        font-display: block;
20
+        src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(./fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2) format('woff2');
21
+        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
22
+      }
23
+    </style>
24
+    <link rel="preload" href="./fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous">
25
+    <link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
26
+
27
+    <link rel="prefetch" href="./app/js/app.js">
28
+    <link rel="prefetch" href="./app/js/bacme.js">
29
+    <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/common.js">
30
+    <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/asn1.js">
31
+    <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_schema.js">
32
+    <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_simpl.js">
33
+    <link rel="prefetch" href="./app/js/browser-csr/v1.0.0-alpha/csr.js">
6 34
   </head>
7
-  <body hidden>
35
+  <body class="js-app-ready">
36
+    <script>
37
+      document.querySelector('body').classList.remove("js-app-ready");
38
+    </script>
8 39
     <div class="column-container wide">
40
+
9 41
       <div class="column-row">
10 42
         <img src="img/greenlock-146.png">
11 43
       </div>
12 44
       <div class="column-row">
13
-          <h1>Get the green lock for your website</h1>
14
-
45
+        <h1>Get the green lock for your website</h1>
46
+      </div>
47
+      <div class="column-row">
15 48
         <!-- Step 1 Choose Domain(s) -->
16
-        <form class="js-acme-form js-acme-form-domains">
17
-          <h1><label>What's your domain?</label></h1>
18
-          <h4>Certificates are valid for 90 days. Renewal is free :)</h4>
19
-          <input class="js-acme-domains" type="text" placeholder="example.com,*.example.com" required>
20
-          <br>
21
-          <button type="submit">Next</button>
22
-
23
-          <br>
24
-          <br>
25
-          <br>
26
-          <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="v02" checked required>
27
-            Production</label>
28
-          <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="staging-v02" required>
29
-            Testing</label>
30
-          <br>
31
-          <input class="js-acme-directory-url" type="url" placeholder="ACME directory url">
32
-        </form>
33
-
34
-        <!-- Step 2 Create Account -->
35
-        <form class="js-acme-form js-acme-form-account">
36
-          <h1><label>What's your email?</label></h1>
37
-          <input class="js-acme-account-email" type="email" placeholder="john@doe.family" required>
38
-          <br>
39
-          <br>
40
-          <label><input class="js-acme-account-tos" type="checkbox" required>
41
-            Agree to <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt&trade; Terms of Service</a>?</label>
42
-          <br>
43
-          <br>
44
-          <label><input class="js-greenlock-account-tos" type="checkbox" required>
45
-            Agree to <a class="js-gl-tos" target="_blank" href="./legal.html">Greenlock&trade; Terms of Service</a>?</label>
46
-          <br>
47
-          <br>
48
-          <!--
49
-          <a href="#">advanced (use existing account)</a>
50
-          <br>
51
-          <br>
52
-          -->
53
-          <button type="submit">Next</button>
54
-        </form>
55
-
56
-        <!-- Step 3 Set Challanges -->
57
-        <form class="js-acme-form js-acme-form-challenges">
58
-
59
-          <h1>How will you validate your domain?</h1>
60
-          <br>
61
-          <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="http-01" checked required>
62
-            File Upload to HTTP Web Server</label>
63
-          <br>
64
-          <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="dns-01" required>
65
-            TXT Records on DNS Name Server</label>
66
-          <br>
67
-
68
-          <div class="js-acme-challenges">
69
-
70
-          <h2>Verify Domains &amp; Sub-Domains</h2>
71
-
72
-          <table class="js-acme-table-http-01">
73
-            <thead>
74
-              <tr>
75
-                <th>Hostname</th>
76
-                <th>File Location</th>
77
-                <th>File Contents</th>
78
-              </tr>
79
-            </thead>
80
-            <tbody>
81
-              <tr>
82
-                <td>example.com</td>
83
-                <td>.well-known/acme-challenge/xxx</td>
84
-                <td>sec.ret</td>
85
-              </tr>
86
-            </tbody>
87
-          </table>
88
-
89
-          <table class="js-acme-table-dns-01">
90
-            <thead>
91
-              <tr>
92
-                <th>Hostname</th>
93
-                <th>TXT Host</th>
94
-                <th>TXT Value</th>
95
-              </tr>
96
-            </thead>
97
-            <tbody>
98
-              <tr>
99
-                <td>example.com</td>
100
-                <td>_acme-challenge.example.com</td>
101
-                <td>4A54</td>
102
-              </tr>
103
-            </tbody>
104
-          </table>
105
-          </div>
106
-
107
-          <div class="js-acme-wildcard">
108
-            <h2>Verify Wildcard Domains</h2>
109
-
110
-            <table class="js-acme-table-wildcard">
111
-              <thead>
112
-                <tr>
113
-                  <th>Hostname</th>
114
-                  <th>TXT Host</th>
115
-                  <th>TXT Value</th>
116
-                </tr>
117
-              </thead>
118
-              <tbody>
119
-                <tr>
120
-                  <td>example.com</td>
121
-                  <td>_acme-challenge.example.com</td>
122
-                  <td>4A54</td>
123
-                </tr>
124
-              </tbody>
125
-            </table>
126
-          </div>
127
-
128
-          <button type="submit">Next</button>
129
-        </form>
130
-
131
-        <!-- Step 4 Process Challanges -->
132
-        <form class="js-acme-form js-acme-form-poll">
133
-          Verifying Domains... (give us 5 seconds or so...)
134
-
135
-          <!--
136
-          <table class="js-acme-table-verifying">
137
-            <thead>
138
-              <tr>
139
-                <th>Hostname</th>
140
-                <th>Type</th>
141
-                <th>Pass</th>
142
-              </tr>
143
-            </thead>
144
-            <tbody>
145
-              <tr>
146
-                <td>example.com</td>
147
-                <td>http-01</td>
148
-                <td>-</td>
149
-              </tr>
150
-            </tbody>
151
-          </table>
152
-
153
-          <a href="#">advanced (use existing keypair for domain)</a>
154
-
155
-          <button type="submit">Next</button>
156
-          -->
157
-        </form>
158
-
159
-        <!-- Step 5 Get Certs -->
160
-        <form class="js-acme-form js-acme-form-download">
161
-          <div>
162
-          <h2><label>privkey.pem</label></h2>
163
-          <textarea cols="80" rows="10" class="js-privkey">-</textarea>
164
-          </div>
165
-
166
-          <div>
167
-          <h2><label>fullchain.pem</label></h2>
168
-          <textarea cols="80" rows="60" class="js-fullchain">-</textarea>
49
+        <form id="js-acme-form" action="./app/" method=>
50
+          <div class="domain-psuedo-input">
51
+            <span class="secure-green">Secure</span> | <span class="secure-green">https:</span>//<input aria-label="domains to secure" id="acme-domains" type="text" name="acme-domains" placeholder="Your domain name" required>
169 52
           </div>
170
-
171
-          <div>
172
-          <h3>node.js https server example</h3>
173
-          <pre><code>'use strict';
174
-
175
-    var https = require('https');
176
-    var server = https.createServer({
177
-      key: require('fs').readFileSync('./privkey.pem')
178
-    , cert: require('fs').readFileSync('./fullchain.pem')
179
-    }, function (req, res) {
180
-      res.end("Hello, World!");
181
-    }).listen(443, function () {
182
-      console.log('Listening on', this.address());
183
-    })
184
-    </code></pre>
53
+          <button type="submit">Go</button>
54
+          <div class="domain-subtext">Domain, subdomain, or wildcard domain</div>
55
+          
56
+          <div class="acme-advanced-fields">
57
+            <label><input name="acme-api-type" type="radio" value="v02" checked required>
58
+              Production
59
+            </label>
60
+            <label><input name="acme-api-type" type="radio" value="staging-v02" required>
61
+              Testing</label>
62
+            <input id="js-acme-api-url" name="acme-api-url" type="url" placeholder="ACME directory url">
63
+            <div>
64
+              <a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git)
65
+            </div>
185 66
           </div>
186
-
187
-          <!--
188
-            TODO
189
-          <label>cert.pem</label>
190
-          <textarea class="js-cert">-</textarea>
191
-
192
-          <label>chain.pem</label>
193
-          <textarea class="js-chain">-</textarea>
194
-
195
-          <button type="button">Download SSL Certificates</button>
196
-          <br>
197
-          <a href="#">Advanced (copy and paste)</a>
198
-          <br>
199
-          <button type="submit">Start Over</button>
200
-          -->
201 67
         </form>
202
-
203
-          <br>
204
-          <br>
205
-          <br>
206
-          <div><small>
207
-          <h3></h3>
208
-          <a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git)
209
-          <!-- or
210
-          <pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre>
211
-          Or view the live site code (same as live-site branch):
212
-          <pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
213
-          -->
214
-          </small></div>
215
-
216
-        <script src="./js/bacme.js"></script>
217
-        <script src="./js/app.js"></script>
218
-
219
-        <script src="./js/pkijs.org/v1.3.33/common.js"></script>
220
-        <script src="./js/pkijs.org/v1.3.33/asn1.js"></script>
221
-        <script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script>
222
-        <script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script>
223
-        <script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script>
224
-
225
-        <!-- Global site tag (gtag.js) - Google Analytics -->
226
-        <script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>
227
-        <script>
228
-          window.dataLayer = window.dataLayer || [];
229
-          function gtag(){dataLayer.push(arguments);}
230
-          gtag('js', new Date());
231
-
232
-          gtag('config', 'UA-118745161-2');
233
-        </script>
234 68
       </div>
69
+      <div class="column-row">
70
+        <div class="why-you-need">
71
+          <h2>Why you need SSL certificates</h2>
72
+          If your website doesn't have the green lock from an SSL Certificate, Google Chrome will soon label your website as not secure.
73
+        </div>
74
+      </div>
75
+      <!-- or
76
+      <pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre>
77
+      Or view the live site code (same as live-site branch):
78
+      <pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
79
+      -->
80
+
81
+      <script src="./js/app.js"></script>
82
+
83
+      <!-- Global site tag (gtag.js) - Google Analytics -->
84
+      <script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>
85
+      <script>
86
+        window.dataLayer = window.dataLayer || [];
87
+        function gtag(){dataLayer.push(arguments);}
88
+        gtag('js', new Date());
89
+
90
+        gtag('config', 'UA-118745161-2');
91
+      </script>
235 92
     </div>
236 93
   </body>
237 94
 </html>

+ 4
- 4
install.sh Переглянути файл

@@ -1,14 +1,14 @@
1 1
 #!/bin/bash
2 2
 
3
-mkdir -p js/pkijs.org/v1.3.33/
4
-pushd js/pkijs.org/v1.3.33/
3
+mkdir -p app/js/pkijs.org/v1.3.33/
4
+pushd app/js/pkijs.org/v1.3.33/
5 5
   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js
6 6
   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_schema.js
7 7
   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.js
8 8
   wget -c https://raw.githubusercontent.com/PeculiarVentures/ASN1.js/f7181c21c61e53a940ea24373ab489ad86d51bc1/org/pkijs/asn1.js
9 9
 popd
10 10
 
11
-mkdir -p js/browser-csr/v1.0.0-alpha/
12
-pushd js/browser-csr/v1.0.0-alpha/
11
+mkdir -p app/js/browser-csr/v1.0.0-alpha/
12
+pushd app/js/browser-csr/v1.0.0-alpha/
13 13
   wget -c https://git.coolaj86.com/coolaj86/browser-csr.js/raw/commit/01cdc0e91b5bf03f12e1b25b4129e3cde927987c/csr.js
14 14
 popd

+ 14
- 500
js/app.js Переглянути файл

@@ -3,512 +3,26 @@
3 3
 
4 4
   var $qs = function (s) { return window.document.querySelector(s); };
5 5
   var $qsa = function (s) { return window.document.querySelectorAll(s); };
6
-  var info = {};
7
-  var steps = {};
8
-  var nonce;
9
-  var kid;
10
-  var i = 1;
11 6
 
12 7
   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
13 8
   function updateApiType() {
14
-    var input = this || Array.prototype.filter.call(
15
-      $qsa('.js-acme-api-type'), function ($el) { return $el.checked; }
16
-    )[0];
17
-    console.log('ACME api type radio:', input.value);
18
-    $qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value);
19
-  }
20
-  $qsa('.js-acme-api-type').forEach(function ($el) {
21
-    $el.addEventListener('change', updateApiType);
22
-  });
23
-  updateApiType();
9
+    var formData = new FormData($qs("#js-acme-form"));
24 10
 
25
-  function hideForms() {
26
-    $qsa('.js-acme-form').forEach(function (el) {
27
-      el.hidden = true;
28
-    });
29
-  }
11
+    console.log('ACME api type radio:');
30 12
 
31
-  function submitForm(ev) {
32
-    var j = i;
33
-    i += 1;
34
-    steps[j].submit(ev);
35
-  }
36
-  $qsa('.js-acme-form').forEach(function ($el) {
37
-    $el.addEventListener('submit', function (ev) {
38
-      ev.preventDefault();
39
-      submitForm(ev);
40
-    });
41
-  });
42
-  function updateChallengeType() {
43
-    var input = this || Array.prototype.filter.call(
44
-      $qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; }
45
-    )[0];
46
-    console.log('ch type radio:', input.value);
47
-    $qs('.js-acme-table-wildcard').hidden = true;
48
-    $qs('.js-acme-table-http-01').hidden = true;
49
-    $qs('.js-acme-table-dns-01').hidden = true;
50
-    if (info.challenges.wildcard) {
51
-      $qs('.js-acme-table-wildcard').hidden = false;
52
-    }
53
-    if (info.challenges[input.value]) {
54
-      $qs('.js-acme-table-' + input.value).hidden = false;
55
-    }
56
-  }
57
-  $qsa('.js-acme-challenge-type').forEach(function ($el) {
58
-    $el.addEventListener('change', updateChallengeType);
59
-  });
60
-
61
-  function saveContact(email, domains) {
62
-    // to be used for good, not evil
63
-    return window.fetch('https://api.ppl.family/api/ppl.family/public/list', {
64
-      method: 'POST'
65
-    , cors: true
66
-    , headers: new Headers({ 'Content-Type': 'application/json' })
67
-    , body: JSON.stringify({ address: email, comment: 'greenlock sub for ' + domains.join(',') })
68
-    }).then(function (resp) {
69
-      return resp.json().then(function (data) {
70
-        /*
71
-        if (data.error) {
72
-          window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead.");
73
-          return;
74
-        }
75
-        */
76
-      });
77
-    }, function () {
78
-      /*
79
-      window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead.");
80
-      */
81
-    });
13
+    var value = formData.get("acme-api-type");
14
+    $qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value);
82 15
   }
16
+  $qs('#js-acme-form').addEventListener('change', updateApiType);
83 17
 
84
-  steps[1] = function () {
85
-    hideForms();
86
-    $qs('.js-acme-form-domains').hidden = false;
87
-  };
88
-  steps[1].submit = function () {
89
-    info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) {
90
-      return { type: 'dns', value: hostname.toLowerCase().trim() };
91
-    });
92
-    info.identifiers.sort(function (a, b) {
93
-      if (a === b) { return 0; }
94
-      if (a < b) { return 1; }
95
-      if (a > b) { return -1; }
96
-    });
97
-
98
-    return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) {
99
-      $qs('.js-acme-tos-url').href = directory.meta.termsOfService;
100
-      return BACME.nonce().then(function (_nonce) {
101
-        nonce = _nonce;
102
-
103
-        console.log("MAGIC STEP NUMBER in 1 is:", i);
104
-        steps[i]();
105
-      });
106
-    });
107
-  };
108
-
109
-  steps[2] = function () {
110
-    hideForms();
111
-    $qs('.js-acme-form-account').hidden = false;
112
-  };
113
-  steps[2].submit = function () {
114
-    var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
115
-
116
-
117
-    info.contact = [ 'mailto:' + email ];
118
-    info.agree = $qs('.js-acme-account-tos').checked;
119
-    info.greenlockAgree = $qs('.js-gl-tos').checked;
120
-    // TODO
121
-    // options for
122
-    // * regenerate key
123
-    // * ECDSA / RSA / bitlength
124
-
125
-    // TODO ping with version and account creation
126
-    setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
127
-
128
-    var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
129
-    var p;
130
-
131
-    function createKeypair() {
132
-      return BACME.accounts.generateKeypair({
133
-        type: 'ECDSA'
134
-      , bitlength: '256'
135
-      }).then(function (jwk) {
136
-        localStorage.setItem('account:' + email, JSON.stringify(jwk));
137
-        return jwk;
138
-      })
139
-    }
140
-
141
-    if (jwk) {
142
-      p = Promise.resolve(jwk);
143
-    } else {
144
-      p = createKeypair();
145
-    }
146
-
147
-    function createAccount(jwk) {
148
-      console.log('account jwk:');
149
-      console.log(jwk);
150
-      delete jwk.key_ops;
151
-      info.jwk = jwk;
152
-      return BACME.accounts.sign({
153
-        jwk: jwk
154
-      , contacts: [ 'mailto:' + email ]
155
-      , agree: info.agree
156
-      , nonce: nonce
157
-      , kid: kid
158
-      }).then(function (signedAccount) {
159
-        return BACME.accounts.set({
160
-          signedAccount: signedAccount
161
-        }).then(function (account) {
162
-          console.log('account:');
163
-          console.log(account);
164
-          kid = account.kid;
165
-          return kid;
166
-        });
167
-      });
168
-    }
169
-
170
-    return p.then(function (_jwk) {
171
-      jwk = _jwk;
172
-      kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
173
-      var p2
174
-
175
-      // TODO save account id rather than always retrieving it
176
-      if (kid) {
177
-        p2 = Promise.resolve(kid);
178
-      } else {
179
-        p2 = createAccount(jwk);
180
-      }
181
-
182
-      return p2.then(function (_kid) {
183
-        kid = _kid;
184
-        info.kid = kid;
185
-        return BACME.orders.sign({
186
-          jwk: jwk
187
-        , identifiers: info.identifiers
188
-        , kid: kid
189
-        }).then(function (signedOrder) {
190
-          return BACME.orders.create({
191
-            signedOrder: signedOrder
192
-          }).then(function (order) {
193
-            info.finalizeUrl = order.finalize;
194
-            info.orderUrl = order.url; // from header Location ???
195
-            return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) {
196
-              return BACME.challenges.all().then(function (claims) {
197
-                console.log('claims:');
198
-                console.log(claims);
199
-                var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
200
-                var map = {
201
-                  'http-01': '.js-acme-table-http-01'
202
-                , 'dns-01': '.js-acme-table-dns-01'
203
-                , 'wildcard': '.js-acme-table-wildcard'
204
-                }
205
-                var tpls = {};
206
-                info.challenges = obj;
207
-                Object.keys(map).forEach(function (k) {
208
-                  var sel = map[k] + ' tbody';
209
-                  console.log(sel);
210
-                  tpls[k] = $qs(sel).innerHTML;
211
-                  $qs(map[k] + ' tbody').innerHTML = '';
212
-                });
213
-
214
-                // TODO make Promise-friendly
215
-                return Promise.all(claims.map(function (claim) {
216
-                  var hostname = claim.identifier.value;
217
-                  return Promise.all(claim.challenges.map(function (c) {
218
-                    var keyAuth = BACME.challenges['http-01']({
219
-                      token: c.token
220
-                    , thumbprint: thumbprint
221
-                    , challengeDomain: hostname
222
-                    });
223
-                    return BACME.challenges['dns-01']({
224
-                      keyAuth: keyAuth.value
225
-                    , challengeDomain: hostname
226
-                    }).then(function (dnsAuth) {
227
-                      var data = {
228
-                        type: c.type
229
-                      , hostname: hostname
230
-                      , url: c.url
231
-                      , token: c.token
232
-                      , keyAuthorization: keyAuth
233
-                      , httpPath: keyAuth.path
234
-                      , httpAuth: keyAuth.value
235
-                      , dnsType: dnsAuth.type
236
-                      , dnsHost: dnsAuth.host
237
-                      , dnsAnswer: dnsAuth.answer
238
-                      };
239
-
240
-                      console.log('');
241
-                      console.log('CHALLENGE');
242
-                      console.log(claim);
243
-                      console.log(c);
244
-                      console.log(data);
245
-                      console.log('');
246
-
247
-                      if (claim.wildcard) {
248
-                        obj.wildcard.push(data);
249
-                        $qs(map.wildcard).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
250
-                      } else if(obj[data.type]) {
251
-
252
-                        obj[data.type].push(data);
253
-                        if ('dns-01' === data.type) {
254
-                          $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
255
-                        } else if ('http-01' === data.type) {
256
-                          $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.httpPath + '</td><td>' + data.httpAuth + '</td></tr>';
257
-                        }
258
-                      }
259
-
260
-                    });
261
-
262
-                  }));
263
-                })).then(function () {
264
-
265
-                  // hide wildcard if no wildcard
266
-                  // hide http-01 and dns-01 if only wildcard
267
-                  if (!obj.wildcard.length) {
268
-                    $qs('.js-acme-wildcard').hidden = true;
269
-                  }
270
-                  if (!obj['http-01'].length) {
271
-                    $qs('.js-acme-challenges').hidden = true;
272
-                  }
273
-
274
-                  updateChallengeType();
275
-
276
-                  console.log("MAGIC STEP NUMBER in 2 is:", i);
277
-                  steps[i]();
278
-                });
279
-
280
-              });
281
-            });
282
-          });
283
-        });
284
-      });
285
-    }).catch(function (err) {
286
-      console.error('Step \'' + i + '\' Error:');
287
-      console.error(err);
288
-    });
289
-  };
290
-
291
-  steps[3] = function () {
292
-    hideForms();
293
-    $qs('.js-acme-form-challenges').hidden = false;
294
-  };
295
-  steps[3].submit = function () {
296
-    var chType;
297
-    Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) {
298
-      if ($el.checked) {
299
-        chType = $el.value;
300
-        return true;
301
-      }
302
-    });
303
-    console.log('chType is:', chType);
304
-    var chs = [];
305
-
306
-    // do each wildcard, if any
307
-    // do each challenge, by selected type only
308
-    [ 'wildcard', chType].forEach(function (typ) {
309
-      info.challenges[typ].forEach(function (ch) {
310
-        // { jwk, challengeUrl, accountId (kid) }
311
-        chs.push({
312
-          jwk: info.jwk
313
-        , challengeUrl: ch.url
314
-        , accountId: info.kid
315
-        });
316
-      });
317
-    });
318
-    console.log("INFO.challenges !!!!!", info.challenges);
319
-
320
-    var results = [];
321
-    function nextChallenge() {
322
-      var ch = chs.pop();
323
-      if (!ch) { return results; }
324
-      return BACME.challenges.accept(ch).then(function (result) {
325
-        results.push(result);
326
-        return nextChallenge();
327
-      });
328
-    }
329
-
330
-    // for now just show the next page immediately (its a spinner)
331
-    steps[i]();
332
-    return nextChallenge().then(function (results) {
333
-      console.log('challenge status:', results);
334
-      var polls = results.slice(0);
335
-      var allsWell = true;
336
-
337
-      function checkPolls() {
338
-        return new Promise(function (resolve) {
339
-          setTimeout(resolve, 1000);
340
-        }).then(function () {
341
-          return Promise.all(polls.map(function (poll) {
342
-            return BACME.challenges.check({ challengePollUrl: poll.url });
343
-          })).then(function (polls) {
344
-            console.log(polls);
345
-
346
-            polls = polls.filter(function (poll) {
347
-              //return 'valid' !== poll.status && 'invalid' !== poll.status;
348
-              if ('pending' === poll.status) {
349
-                return true;
350
-              }
351
-              if ('valid' !== poll.status) {
352
-                allsWell = false;
353
-                console.warn('BAD POLL STATUS', poll);
354
-              }
355
-              // TODO show status in HTML
356
-            });
357
-
358
-            if (polls.length) {
359
-              return checkPolls();
360
-            }
361
-            return true;
362
-          });
363
-        });
364
-      }
365
-
366
-      return checkPolls().then(function () {
367
-        if (allsWell) {
368
-          return submitForm();
369
-        }
370
-      });
371
-    });
372
-  };
373
-
374
-  // spinner
375
-  steps[4] = function () {
376
-    hideForms();
377
-    $qs('.js-acme-form-poll').hidden = false;
378
-  }
379
-  steps[4].submit = function () {
380
-    console.log('Congrats! Auto advancing...');
381
-
382
-    var key = info.identifiers.map(function (ident) { return ident.value; }).join(',');
383
-    var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null');
384
-    var p;
385
-
386
-    function createKeypair() {
387
-      return BACME.accounts.generateKeypair({
388
-        type: 'ECDSA'
389
-      , bitlength: '256'
390
-      }).then(function (serverJwk) {
391
-        localStorage.setItem('server:' + key, JSON.stringify(serverJwk));
392
-        return serverJwk;
393
-      })
394
-    }
395
-
396
-    if (serverJwk) {
397
-      p = Promise.resolve(serverJwk);
398
-    } else {
399
-      p = createKeypair();
400
-    }
401
-
402
-    return p.then(function (_serverJwk) {
403
-      serverJwk = _serverJwk;
404
-      info.serverJwk = serverJwk;
405
-      // { serverJwk, domains }
406
-      return BACME.orders.generateCsr({
407
-        serverJwk: serverJwk
408
-      , domains: info.identifiers.map(function (ident) {
409
-          return ident.value;
410
-        })
411
-      }).then(function (csrweb64) {
412
-        return BACME.orders.finalize({
413
-          csr: csrweb64
414
-        , jwk: info.jwk
415
-        , finalizeUrl: info.finalizeUrl
416
-        , accountId: info.kid
417
-        });
418
-      }).then(function () {
419
-        function checkCert() {
420
-          return new Promise(function (resolve) {
421
-            setTimeout(resolve, 1000);
422
-          }).then(function () {
423
-            return BACME.orders.check({ orderUrl: info.orderUrl });
424
-          }).then(function (reply) {
425
-            if ('processing' === reply) {
426
-              return checkCert();
427
-            }
428
-            return reply;
429
-          });
430
-        }
431
-
432
-        return checkCert();
433
-      }).then(function (reply) {
434
-        return BACME.orders.receive({ certificateUrl: reply.certificate });
435
-      }).then(function (certs) {
436
-        console.log('WINNING!');
437
-        console.log(certs);
438
-        $qs('.js-fullchain').value = certs;
439
-
440
-        // https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
441
-				function spkiToPEM(keydata){
442
-						var keydataS = arrayBufferToString(keydata);
443
-						var keydataB64 = window.btoa(keydataS);
444
-						var keydataB64Pem = formatAsPem(keydataB64);
445
-						return keydataB64Pem;
446
-				}
447
-
448
-				function arrayBufferToString( buffer ) {
449
-						var binary = '';
450
-						var bytes = new Uint8Array( buffer );
451
-						var len = bytes.byteLength;
452
-						for (var i = 0; i < len; i++) {
453
-								binary += String.fromCharCode( bytes[ i ] );
454
-						}
455
-						return binary;
456
-				}
457
-
458
-
459
-				function formatAsPem(str) {
460
-						var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n';
461
-
462
-						while(str.length > 0) {
463
-								finalString += str.substring(0, 64) + '\n';
464
-								str = str.substring(64);
465
-						}
466
-
467
-						finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----';
468
-
469
-						return finalString;
470
-				}
471
-
472
-        var wcOpts;
473
-        var pemName;
474
-        if (/^R/.test(info.serverJwk.kty)) {
475
-          pemName = 'RSA';
476
-          wcOpts = {
477
-            name: "RSASSA-PKCS1-v1_5"
478
-          , hash: { name: "SHA-256" }
479
-          };
480
-        } else {
481
-          pemName = 'EC';
482
-          wcOpts = {
483
-            name: "ECDSA"
484
-          , namedCurve: "P-256"
485
-          }
486
-        }
487
-				return crypto.subtle.importKey(
488
-          "jwk"
489
-        , info.serverJwk
490
-        , wcOpts
491
-        , true
492
-        , ["sign"]
493
-				).then(function (privateKey) {
494
-				  return window.crypto.subtle.exportKey("pkcs8", privateKey);
495
-				}).then (function (keydata) {
496
-					var pem = spkiToPEM(keydata);
497
-					$qs('.js-privkey').value = pem;
498
-          steps[i]();
499
-				}).catch(function(err){
500
-					console.error(err);
501
-				});
502
-      });
503
-    });
504
-  };
505
-
506
-  steps[5] = function () {
507
-    hideForms();
508
-    $qs('.js-acme-form-download').hidden = false;
18
+  updateApiType();
19
+  try {
20
+    document.fonts.load().then(function() {
21
+      $qs('body').classList.add("js-app-ready");
22
+    }).catch(function(error) {
23
+      $qs('body').classList.add("js-app-ready");
24
+    });
25
+  } catch(e) {
26
+    setTimeout(function() {$qs('body').classList.add("js-app-ready");}, 200);
509 27
   }
510
-
511
-  steps[1]();
512
-
513
-  $qs('body').hidden = false;
514 28
 }());

+ 100
- 0
styles/main.css Переглянути файл

@@ -1,5 +1,105 @@
1 1
 .column-row {
2 2
   display: flex;
3 3
   flex-direction: column;
4
+  text-align: center;
4 5
   align-items: center;
6
+}
7
+
8
+body {
9
+  position: relative;
10
+  margin-top: 5.777777778em;
11
+  min-height: 36em;
12
+  font-size: 18px;
13
+  font-family: 'Source Sans Pro', sans-serif;
14
+  font-stretch: normal;
15
+  line-height: 1.33;
16
+  letter-spacing:  -0.4px;
17
+  color: #1a1a1a;
18
+  opacity: 0;
19
+}
20
+
21
+h1 {
22
+  font-size: 2.666666667em;
23
+  max-width: 8em;
24
+  text-align: center;
25
+}
26
+
27
+input {
28
+  font-size: 1em;
29
+  padding: 0.444444444em;
30
+  border: solid #d9d9d9 1px;
31
+  border-radius: 2px;
32
+  font-family: inherit;
33
+}
34
+
35
+button {
36
+  padding: 0.444444444em 1.2em;
37
+  font-size: 1em;
38
+  background-color: #5bc17f;
39
+  border: solid 1px #5bc17f;
40
+  border-radius: 2px;
41
+  font-weight: normal;
42
+  font-stretch: normal;
43
+  letter-spacing: -0.4px;
44
+  font-family: inherit;
45
+  text-align: center;
46
+  color: white;
47
+  height: 40px;
48
+  line-height: 1.13;
49
+}
50
+
51
+.acme-advanced-fields {
52
+  position: absolute;
53
+  bottom: 0;
54
+  padding: 1em;
55
+  text-align: center;
56
+}
57
+
58
+.domain-subtext {
59
+  font-size: 0.833333333em;
60
+  color: #666;
61
+  text-align: center;
62
+  margin: 0.5em;
63
+}
64
+
65
+input#acme-domains:before {
66
+  content: "Secure | https://";
67
+}
68
+
69
+.domain-psuedo-input {
70
+  display: inline-block;
71
+  margin-right: .6666667em;
72
+  border: solid #d9d9d9 1px;
73
+  border-radius: 2px;
74
+  padding: 0.44444444em;
75
+  color: #d9d9d9;
76
+}
77
+
78
+input#acme-domains {
79
+  border: none;
80
+  padding: 0;
81
+  padding-right: 0;
82
+  width: 17.2222222em;
83
+  color: #222;
84
+}
85
+
86
+input#acme-domains:focus {
87
+  outline: none;
88
+}
89
+
90
+span.secure-green {
91
+  color: #5bc17f;
92
+}
93
+
94
+.why-you-need {
95
+  width: 26.555556em;
96
+}
97
+
98
+body.js-app-ready {
99
+  transition: opacity 0.2s;
100
+  opacity: 1;
101
+}
102
+
103
+.acme-advanced-fields > * {
104
+  margin: 0 0.5em;
5 105
 }

Завантаження…
Відмінити
Зберегти