Compare commits

...

45 Commits

Author SHA1 Message Date
AJ ONeal 7484ffcd11 WIP: moving to bluecrypt 4 years ago
AJ ONeal 98c8db8973 WIP: breaking into smaller pieces 4 years ago
AJ ONeal 8ce44bc414 rebrand Root 5 years ago
AJ ONeal ab67741604 rebrand Root 5 years ago
AJ ONeal b7dd224426 update to rootprojects.org 5 years ago
John Shaver 65974b57c1 Merge branch 'live-site' of ssh://git.coolaj86.com:22042/coolaj86/greenlock.html into live-site 5 years ago
John Shaver b1fc2bcc14 fix wildcard certificate verification 5 years ago
AJ ONeal 8d31bf7754 GA -> web-only 5 years ago
AJ ONeal 1c57a342d0 reposition 5 years ago
AJ ONeal f27b386ba4 update legal link (and link to 'us') 5 years ago
AJ ONeal 55a30888ff add privacy policy 5 years ago
AJ ONeal 95e807be73 test key support BEFORE creating keys 5 years ago
AJ ONeal 268f83b49e tested working in firefox 5 years ago
AJ ONeal 09ff0b3adc use the opts we built, duh 5 years ago
AJ ONeal 8a7183ed9c don't eager assign CSR 5 years ago
AJ ONeal a7462db2c8 test ecdsa support, spaces for whitespace 5 years ago
AJ ONeal 2cc5a41268 lint and fix and use domains.generateKeypair 5 years ago
AJ ONeal d63d8e1aed lint 5 years ago
John Shaver 6f188cefb8 Now uses RSA keys for firefox browser. 5 years ago
AJ ONeal bddf85dfe6 Merge branch 'live-site' of ssh://git.coolaj86.com:22042/coolaj86/greenlock.html into live-site 5 years ago
AJ ONeal c6052bcf73 update contact info 5 years ago
John Shaver 78515f165c Fix legal link and preloading files 5 years ago
John Shaver 68253cbe54 You can now tab select check boxes. 5 years ago
John Shaver 9143545389 Added download links for certs and changed some formatting. 5 years ago
John Shaver 3e865a2fb7 Added styling to key/cert page. 5 years ago
John Shaver 6ce81beaec Fixed link to hosted app. 5 years ago
John Shaver c6c06b06f0 First 2 steps down. 5 years ago
John Shaver a32287c3f8 Added a terrible temorary fix to the tables. 5 years ago
John Shaver 6a05569ab5 Some style chages on first/second step. 5 years ago
John Shaver c3c9696799 Progress bar is in place. 5 years ago
John Shaver b86074920a Added Javascript needed warning. 5 years ago
John Shaver 8610baf0e0 fixed blink on loading strait to step2 5 years ago
John Shaver 3cfbc9339b Tied in new homepage with the rest of the app. 5 years ago
John Shaver 62f3d28a71 Some styling updates and created a separate front page. 5 years ago
John Shaver 430b589038 Center, shrink logo. 5 years ago
John Shaver 6d3c3a9e61 No longer fails due to new challenge types. 5 years ago
AJ ONeal 61b2c76822 Merge branch 'master' into live-site 5 years ago
AJ ONeal d0ea6822ea Merge branch 'master' into live-site 5 years ago
AJ ONeal 4bfef46295 Merge branch 'master' into live-site 5 years ago
AJ ONeal 42a589f5c0 add google analytics 5 years ago
AJ ONeal 5d07cec7a3 Merge branch 'master' into live-site 5 years ago
AJ ONeal ceb109e652 fix typo 5 years ago
AJ ONeal ed2739cb20 add line break 5 years ago
AJ ONeal 2be09788ce Merge branch 'master' into live-site 5 years ago
AJ ONeal 3854e6b430 save contact 5 years ago
  1. 4
      .gitignore
  2. 2
      README.md
  3. 1
      app/favicon.ico
  4. BIN
      app/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2
  5. BIN
      app/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2
  6. BIN
      app/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2
  7. 0
      app/img/greenlock-mark-400x400.png
  8. 366
      app/index.html
  9. 670
      app/js/app.js
  10. 495
      app/js/bacme.js
  11. 2892
      app/js/bluecrypt-acme.js
  12. 494
      app/js/greenlock.js
  13. 263
      app/styles/main.css
  14. BIN
      fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2
  15. BIN
      fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2
  16. BIN
      img/greenlock-146.png
  17. 293
      index.html
  18. 8
      install.sh
  19. 491
      js/app.js
  20. 201
      legal.html
  21. 1
      legal/index.html
  22. 115
      styles/main.css

4
.gitignore

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

2
README.md

@ -6,7 +6,7 @@ Taking greenlock™ (Let's Encrypt v2 / ACME client) where it's never been b
Official Site
=============
This app is available at <https://greenlock.ppl.family>.
This app is available at <https://greenlock.domains>.
We expect that our hosted version will meet all of yours needs.
If it doesn't, please open an issue to let us know why.

1
app/favicon.ico

@ -0,0 +1 @@
../img/favicon-32x32.png

BIN
app/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2

Binary file not shown.

BIN
app/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2

Binary file not shown.

BIN
app/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2

Binary file not shown.

0
img/greenlock-mark-400x400.png → app/img/greenlock-mark-400x400.png

After

Width:  |  Height:  |  Size: 6.1 KiB

366
app/index.html

@ -0,0 +1,366 @@
<html>
<head>
<title>Greenlock&trade;</title>
<meta property="og:image" content="https://greenlock.domains/img/greenlock-mark-400x400.png" />
<style>
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: block;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2) format('woff2');
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;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
font-display: block;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(./fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2) format('woff2');
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;
}
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(./fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2) format('woff2');
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;
}
</style>
<link href="styles/main.css" rel="stylesheet">
<link rel="preload" href="./fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="./fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="./js/bacme.js" as="script">
<link rel="preload" href="./js/app.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/common.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/asn1.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/x509_schema.js" as="script">
<link rel="preload" href="./js/pkijs.org/v1.3.33/x509_simpl.js" as="script">
<link rel="preload" href="./js/browser-csr/v1.0.0-alpha/csr.js" as="script">
</head>
<body hidden>
<!-- let's define our SVG that we will reuse -->
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 24 24">
<defs>
<g id="svg-check">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</g>
<g id="svg-checked">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</g>
<g id="svg-unchecked">
<path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</g>
<g id="svg-download">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</g>
</defs>
</svg>
<div class="column-container wide">
<div class="header-row column-row">
<div id="js-progress-bar" class="progress-bar">
<div class="progress-bar-step">
<div class="circle">
<svg display="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-check"></use>
</svg>
</div>
<div class="progress-step-label"><div>Details</div></div>
</div>
<div class="progress-bar-step">
<div class="circle">
<svg display="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-check"></use>
</svg>
</div>
<div class="progress-step-label"><div>Verify domain</div></div>
</div>
<div class="progress-bar-step">
<div class="circle">
<svg display="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-check"></use>
</svg>
</div>
<div class="progress-step-label"><div>Install certificates</div></div>
</div>
<!-- hide until the steps are all updated
<div class="progress-bar-step">
<div class="circle">
<svg display="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-check"></use>
</svg>
</div>
<div class="progress-step-label"><div>Done</div></div>
</div>
-->
</div>
<div class="greenlock-logo-badge"><img src="./img/greenlock-mark-400x400.png"></div>
<div class="greenlock-name">Greenlock</div>
</div>
<div class="column-row">
<form class="js-acme-form js-acme-form-domains">
<h1><label>What's your domain?</label></h1>
<h4>Certificates are valid for 90 days. Renewal is free :)</h4>
<input class="js-acme-domains" type="text" placeholder="example.com,*.example.com" required>
<br>
<button type="submit">Next</button>
<br>
<br>
<br>
<label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="v02" checked required>
Production</label>
<label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="staging-v02" required>
Testing</label>
<br>
<input class="js-acme-directory-url" type="url" placeholder="ACME directory url">
</form>
<!-- Step 2 Create Account -->
<form class="js-acme-form js-acme-form-account">
<h1><label>What's your email?</label></h1>
<input class="js-acme-account-email acme-account-email" type="email" placeholder="john@doe.family" required>
<div class="checkbox-array">
<label>
<input class="js-acme-account-tos" type="checkbox" required>
<svg class="icon-checked-box" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-checked"></use>
</svg>
<svg class="icon-unchecked-box" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-unchecked"></use>
</svg>
Agree to &nbsp<a class="js-acme-tos-url" target="acme-tos">Let's Encrypt&trade; Terms of Service</a>?
</label>
<label>
<input class="js-greenlock-account-tos" type="checkbox" required>
<svg class="icon-checked-box" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-checked"></use>
</svg>
<svg class="icon-unchecked-box" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-unchecked"></use>
</svg>
Agree to &nbsp<a class="js-gl-tos" target="_blank" href="/legal/#terms">Greenlock&trade; Terms of Service</a>?
</label>
</div>
<!--
<a href="#">advanced (use existing account)</a>
<br>
<br>
-->
<button class="button-next" type="submit">Next</button>
<div class="email-usage">
Why do we need your email? We link your SSL certificates to the
email you use so you can manage your certificates in the future,
and get important email updates about them.
</div>
</form>
<!-- Step 3 Set Challanges -->
<form class="js-acme-form js-acme-form-challenges">
<h1>Let's verify your domain</h1>
<div class="js-acme-challenges">
<div class="tabbed-selector">
<label>
<input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="http-01" checked required>
File Upload
<div></div>
</label>
<label>
<input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="dns-01" required>
DNS Record
<div></div>
</label>
</div>
<div class="js-acme-verification-http-01">
<h3>Upload this file</h3>
<div class="http-verification-info file-preview">
<div class="paper-fold"></div>
<div>
<div class="file-ver-info-header">FILENAME</div>
<pre class="js-acme-ver-file-location">...loading</pre>
</div>
<hr>
<div>
<div class="file-ver-info-header">CONTENTS</div>
<pre class="js-acme-ver-content">...loading</pre>
</div>
</div>
<div class="download-file">
<svg class="mdicon icon-download" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-download"></use>
</svg>
<a class="js-download-verify-link" href="data:text/octet-stream;base64,SGVsbG8gV29ybGQuLi4=" download="hello.txt" target="_blank">
Download
</a>
</div>
<h3>To this location</h3>
<div class="js-acme-ver-uri" class="acme-ver-uri">..loading</div>
</div>
<div class="js-acme-verification-dns-01">
<h3>Set this DNS Record</h3>
<div class="acme-ver-dns-label">Hostname</div>
<div class="js-acme-ver-hostname">loading...</div>
<div class="acme-ver-dns-label">TXT Host</div>
<div class="js-acme-ver-txt-host">loading...</div>
<div class="acme-ver-dns-label">TXT Value</div>
<div class="js-acme-ver-txt-value">loading...</div>
</div>
</div>
<div class="js-acme-wildcard-challenges">
<div class="js-acme-wildcard">
<div class="js-acme-verification-wildcard">
<h3>Set this DNS Record</h3>
<div class="acme-ver-dns-label">Hostname</div>
<div class="js-acme-ver-hostname">loading...</div>
<div class="acme-ver-dns-label">TXT Host</div>
<div class="js-acme-ver-txt-host">loading...</div>
<div class="acme-ver-dns-label">TXT Value</div>
<div class="js-acme-ver-txt-value">loading...</div>
</div>
</div>
</div>
<button class="button-next" type="submit">Next</button>
</form>
<!-- Step 4 Process Challanges -->
<form class="js-acme-form js-acme-form-poll">
Verifying Domains... (give us 5 seconds or so...)
<!--
<table class="js-acme-table-verifying">
<thead>
<tr>
<th>Hostname</th>
<th>Type</th>
<th>Pass</th>
</tr>
</thead>
<tbody>
<tr>
<td>example.com</td>
<td>http-01</td>
<td>-</td>
</tr>
</tbody>
</table>
<a href="#">advanced (use existing keypair for domain)</a>
<button type="submit">Next</button>
-->
</form>
<!-- Step 5 Get Certs -->
<form class="js-acme-form js-acme-form-download">
<div class="cert-download-container">
<h2><label>privkey.pem</label></h2>
<div class="acme-result-privkey file-preview">
<div class="paper-fold"></div>
<pre id="js-privkey">
</pre>
</div>
<div class="download-file">
<svg class="mdicon icon-download" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-download"></use>
</svg>
<a id="js-download-privkey-link" href="data:text/octet-stream;base64,SGVsbG8gV29ybGQuLi4=" download="privkey.pem" target="_blank">
Download
</a>
</div>
<h2><label>fullchain.pem</label></h2>
<div class="acme-result-fullchain file-preview">
<div class="paper-fold"></div>
<pre id="js-fullchain">
</pre>
</div>
<div class="download-file">
<svg class="mdicon icon-download" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<use xlink:href="#svg-download"></use>
</svg>
<a id="js-download-fullchain-link" href="data:text/octet-stream;base64,SGVsbG8gV29ybGQuLi4=" download="fullchain.pem" target="_blank">
Download
</a>
</div>
<div>
<h3>node.js https server example</h3>
<pre><code>'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());
})
</code></pre>
</div>
<!--
TODO
<label>cert.pem</label>
<textarea class="js-cert">-</textarea>
<label>chain.pem</label>
<textarea class="js-chain">-</textarea>
<button type="button">Download SSL Certificates</button>
<br>
<a href="#">Advanced (copy and paste)</a>
<br>
<button type="submit">Start Over</button>
-->
</form>
</div>
<div><small><center>
<div>
A <a href="https://rootprojects.org/" target="_blank">Root</a> Project
| <a href="https://git.coolaj86.com/coolaj86/greenlock.html" target="_blank">View Source</a> (git)
| <a href="https://rootprojects.org/legal/#terms" target="_blank">Terms of Service</a>
| <a href="https://rootprojects.org/legal/#privacy" target="_blank">Privacy Policy</a>
</div>
<!-- or
<pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre>
Or view the live site code (same as live-site branch):
<pre><code>wget https://greenlock.domains --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
-->
</center></small></div>
<br>
<script src="./js/bacme.js"></script>
<script src="./js/app.js"></script>
<script src="./js/pkijs.org/v1.3.33/common.js"></script>
<script src="./js/pkijs.org/v1.3.33/asn1.js"></script>
<script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script>
<script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script>
<script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-118745161-2');
</script>
</div>
</div>
</body>
</html>

670
app/js/app.js

@ -0,0 +1,670 @@
(function () {
'use strict';
/*global URLSearchParams,Headers*/
var BROWSER_SUPPORTS_ECDSA;
var $qs = function (s) { return window.document.querySelector(s); };
var $qsa = function (s) { return window.document.querySelectorAll(s); };
var info = {};
var steps = {};
var nonce;
var kid;
var i = 1;
var BACME = window.BACME;
var PromiseA = window.Promise;
var crypto = window.crypto;
function testEcdsaSupport() {
var opts = {
type: 'ECDSA'
, bitlength: '256'
};
return BACME.accounts.generateKeypair(opts).then(function (jwk) {
return crypto.subtle.importKey(
"jwk"
, jwk
, { name: "ECDSA", namedCurve: "P-256" }
, true
, ["sign"]
).then(function (privateKey) {
return window.crypto.subtle.exportKey("pkcs8", privateKey);
});
});
}
function testRsaSupport() {
var opts = {
type: 'RSA'
, bitlength: '2048'
};
return BACME.accounts.generateKeypair(opts).then(function (jwk) {
return crypto.subtle.importKey(
"jwk"
, jwk
, { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }
, true
, ["sign"]
).then(function (privateKey) {
return window.crypto.subtle.exportKey("pkcs8", privateKey);
});
});
}
function testKeypairSupport() {
return testEcdsaSupport().then(function () {
console.info("[crypto] ECDSA is supported");
BROWSER_SUPPORTS_ECDSA = true;
localStorage.setItem('version', '1');
return true;
}).catch(function () {
console.warn("[crypto] ECDSA is NOT fully supported");
BROWSER_SUPPORTS_ECDSA = false;
// fix previous firefox browsers
if (!localStorage.getItem('version')) {
localStorage.clear();
localStorage.setItem('version', '1');
}
return false;
});
}
testKeypairSupport().then(function (ecdsaSupport) {
if (ecdsaSupport) {
return true;
}
return testRsaSupport().then(function () {
console.info('[crypto] RSA is supported');
}).catch(function (err) {
console.error('[crypto] could not use either EC nor RSA.');
console.error(err);
window.alert("Your browser is cryptography support (neither RSA or EC is usable). Please use Chrome, Firefox, or Safari.");
});
});
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
function updateApiType() {
console.log("type updated");
/*jshint validthis: true */
var input = this || Array.prototype.filter.call(
$qsa('.js-acme-api-type'), function ($el) { return $el.checked; }
)[0];
console.log('ACME api type radio:', input.value);
$qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value);
}
$qsa('.js-acme-api-type').forEach(function ($el) {
$el.addEventListener('change', updateApiType);
});
updateApiType();
function hideForms() {
$qsa('.js-acme-form').forEach(function (el) {
el.hidden = true;
});
}
function updateProgress(currentStep) {
var progressSteps = $qs("#js-progress-bar").children;
for(var j = 0; j < progressSteps.length; j++) {
if(j < currentStep) {
progressSteps[j].classList.add("js-progress-step-complete");
progressSteps[j].classList.remove("js-progress-step-started");
} else if(j === currentStep) {
progressSteps[j].classList.remove("js-progress-step-complete");
progressSteps[j].classList.add("js-progress-step-started");
} else {
progressSteps[j].classList.remove("js-progress-step-complete");
progressSteps[j].classList.remove("js-progress-step-started");
}
}
}
function submitForm(ev) {
var j = i;
i += 1;
return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) {
console.error(err);
window.alert("Something went wrong. It's our fault not yours. Please email aj@rootprojects.org and let him know that 'step " + j + "' failed.");
});
}
$qsa('.js-acme-form').forEach(function ($el) {
$el.addEventListener('submit', function (ev) {
ev.preventDefault();
submitForm(ev);
});
});
function updateChallengeType() {
/*jshint validthis: true*/
var input = this || Array.prototype.filter.call(
$qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; }
)[0];
console.log('ch type radio:', input.value);
$qs('.js-acme-verification-wildcard').hidden = true;
$qs('.js-acme-verification-http-01').hidden = true;
$qs('.js-acme-verification-dns-01').hidden = true;
if (info.challenges.wildcard) {
$qs('.js-acme-verification-wildcard').hidden = false;
}
if (info.challenges[input.value]) {
$qs('.js-acme-verification-' + input.value).hidden = false;
}
}
$qsa('.js-acme-challenge-type').forEach(function ($el) {
$el.addEventListener('change', updateChallengeType);
});
function saveContact(email, domains) {
// to be used for good, not evil
return window.fetch('https://api.rootprojects.org/api/rootprojects.org/public/community', {
method: 'POST'
, cors: true
, headers: new Headers({ 'Content-Type': 'application/json' })
, body: JSON.stringify({
address: email
, project: 'greenlock-domains@rootprojects.org'
, domain: domains.join(',')
})
}).catch(function (err) {
console.error(err);
});
}
steps[1] = function () {
updateProgress(0);
hideForms();
$qs('.js-acme-form-domains').hidden = false;
};
steps[1].submit = function () {
info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) {
return { type: 'dns', value: hostname.toLowerCase().trim() };
}).slice(0,1); //Disable multiple values for now. We'll just take the first and work with it.
info.identifiers.sort(function (a, b) {
if (a === b) { return 0; }
if (a < b) { return 1; }
if (a > b) { return -1; }
});
return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) {
$qs('.js-acme-tos-url').href = directory.meta.termsOfService;
return BACME.nonce().then(function (_nonce) {
nonce = _nonce;
console.log("MAGIC STEP NUMBER in 1 is:", i);
steps[i]();
});
});
};
steps[2] = function () {
updateProgress(0);
hideForms();
$qs('.js-acme-form-account').hidden = false;
};
steps[2].submit = function () {
var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
info.contact = [ 'mailto:' + email ];
info.agree = $qs('.js-acme-account-tos').checked;
info.greenlockAgree = $qs('.js-gl-tos').checked;
// TODO
// options for
// * regenerate key
// * ECDSA / RSA / bitlength
// TODO ping with version and account creation
setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
var p;
function createKeypair() {
var opts;
if(BROWSER_SUPPORTS_ECDSA) {
opts = {
type: 'ECDSA'
, bitlength: '256'
};
} else {
opts = {
type: 'RSA'
, bitlength: '2048'
};
}
return BACME.accounts.generateKeypair(opts).then(function (jwk) {
localStorage.setItem('account:' + email, JSON.stringify(jwk));
return jwk;
});
}
if (jwk) {
p = PromiseA.resolve(jwk);
} else {
p = testKeypairSupport().then(createKeypair);
}
function createAccount(jwk) {
console.log('account jwk:');
console.log(jwk);
delete jwk.key_ops;
info.jwk = jwk;
return BACME.accounts.sign({
jwk: jwk
, contacts: [ 'mailto:' + email ]
, agree: info.agree
, nonce: nonce
, kid: kid
}).then(function (signedAccount) {
return BACME.accounts.set({
signedAccount: signedAccount
}).then(function (account) {
console.log('account:');
console.log(account);
kid = account.kid;
return kid;
});
});
}
return p.then(function (_jwk) {
jwk = _jwk;
kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
var p2;
// TODO save account id rather than always retrieving it
if (kid) {
p2 = PromiseA.resolve(kid);
} else {
p2 = createAccount(jwk);
}
return p2.then(function (_kid) {
kid = _kid;
info.kid = kid;
return BACME.orders.sign({
jwk: jwk
, identifiers: info.identifiers
, kid: kid
}).then(function (signedOrder) {
return BACME.orders.create({
signedOrder: signedOrder
}).then(function (order) {
info.finalizeUrl = order.finalize;
info.orderUrl = order.url; // from header Location ???
return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) {
return BACME.challenges.all().then(function (claims) {
console.log('claims:');
console.log(claims);
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
info.challenges = obj;
var map = {
'http-01': '.js-acme-verification-http-01'
, 'dns-01': '.js-acme-verification-dns-01'
, 'wildcard': '.js-acme-verification-wildcard'
};
/*
var tpls = {};
Object.keys(map).forEach(function (k) {
var sel = map[k] + ' tbody';
console.log(sel);
tpls[k] = $qs(sel).innerHTML;
$qs(map[k] + ' tbody').innerHTML = '';
});
*/
// TODO make Promise-friendly
return PromiseA.all(claims.map(function (claim) {
var hostname = claim.identifier.value;
return PromiseA.all(claim.challenges.map(function (c) {
var keyAuth = BACME.challenges['http-01']({
token: c.token
, thumbprint: thumbprint
, challengeDomain: hostname
});
return BACME.challenges['dns-01']({
keyAuth: keyAuth.value
, challengeDomain: hostname
}).then(function (dnsAuth) {
var data = {
type: c.type
, hostname: hostname
, url: c.url
, token: c.token
, keyAuthorization: keyAuth
, httpPath: keyAuth.path
, httpAuth: keyAuth.value
, dnsType: dnsAuth.type
, dnsHost: dnsAuth.host
, dnsAnswer: dnsAuth.answer
};
console.log('');
console.log('CHALLENGE');
console.log(claim);
console.log(c);
console.log(data);
console.log('');
if (claim.wildcard) {
obj.wildcard.push(data);
let verification = $qs(".js-acme-verification-wildcard");
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
} else if(obj[data.type]) {
obj[data.type].push(data);
if ('dns-01' === data.type) {
let verification = $qs(".js-acme-verification-dns-01");
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
} else if ('http-01' === data.type) {
$qs(".js-acme-ver-file-location").innerHTML = data.httpPath.split("/").slice(-1);
$qs(".js-acme-ver-content").innerHTML = data.httpAuth;
$qs(".js-acme-ver-uri").innerHTML = data.httpPath;
$qs(".js-download-verify-link").href =
"data:text/octet-stream;base64," + window.btoa(data.httpAuth);
$qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1);
}
}
});
}));
})).then(function () {
// hide wildcard if no wildcard
// hide http-01 and dns-01 if only wildcard
if (!obj.wildcard.length) {
$qs('.js-acme-wildcard-challenges').hidden = true;
}
if (!obj['http-01'].length) {
$qs('.js-acme-challenges').hidden = true;
}
updateChallengeType();
console.log("MAGIC STEP NUMBER in 2 is:", i);
steps[i]();
});
});
});
});
});
});
}).catch(function (err) {
console.error('Step \'\' Error:');
console.error(err, err.stack);
window.alert("An error happened at Step " + i + ", but it's not your fault. Email aj@rootprojects.org and let him know.");
});
};
steps[3] = function () {
updateProgress(1);
hideForms();
$qs('.js-acme-form-challenges').hidden = false;
};
steps[3].submit = function () {
var chType;
Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) {
if ($el.checked) {
chType = $el.value;
return true;
}
});
console.log('chType is:', chType);
var chs = [];
// do each wildcard, if any
// do each challenge, by selected type only
[ 'wildcard', chType].forEach(function (typ) {
info.challenges[typ].forEach(function (ch) {
// { jwk, challengeUrl, accountId (kid) }
chs.push({
jwk: info.jwk
, challengeUrl: ch.url
, accountId: info.kid
});
});
});
console.log("INFO.challenges !!!!!", info.challenges);
var results = [];
function nextChallenge() {
var ch = chs.pop();
if (!ch) { return results; }
return BACME.challenges.accept(ch).then(function (result) {
results.push(result);
return nextChallenge();
});
}
// for now just show the next page immediately (its a spinner)
steps[i]();
return nextChallenge().then(function (results) {
console.log('challenge status:', results);
var polls = results.slice(0);
var allsWell = true;
function checkPolls() {
return new PromiseA(function (resolve) {
setTimeout(resolve, 1000);
}).then(function () {
return PromiseA.all(polls.map(function (poll) {
return BACME.challenges.check({ challengePollUrl: poll.url });
})).then(function (polls) {
console.log(polls);
polls = polls.filter(function (poll) {
//return 'valid' !== poll.status && 'invalid' !== poll.status;
if ('pending' === poll.status) {
return true;
}
if ('invalid' === poll.status) {
allsWell = false;
window.alert("verification failed:" + poll.error.detail);
return;
}
if (poll.error) {
window.alert("verification failed:" + poll.error.detail);
return;
}
if ('valid' !== poll.status) {
allsWell = false;
console.warn('BAD POLL STATUS', poll);
window.alert("unknown error: " + JSON.stringify(poll, null, 2));
}
// TODO show status in HTML
});
if (polls.length) {
return checkPolls();
}
return true;
});
});
}
return checkPolls().then(function () {
if (allsWell) {
return submitForm();
}
});
});
};
// https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
function spkiToPEM(keydata, pemName){
var keydataS = arrayBufferToString(keydata);
var keydataB64 = window.btoa(keydataS);
var keydataB64Pem = formatAsPem(keydataB64, pemName);
return keydataB64Pem;
}
function arrayBufferToString( buffer ) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return binary;
}
function formatAsPem(str, pemName) {
var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n';
while(str.length > 0) {
finalString += str.substring(0, 64) + '\n';
str = str.substring(64);
}
finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----';
return finalString;
}
// spinner
steps[4] = function () {
updateProgress(1);
hideForms();
$qs('.js-acme-form-poll').hidden = false;
};
steps[4].submit = function () {
console.log('Congrats! Auto advancing...');
var key = info.identifiers.map(function (ident) { return ident.value; }).join(',');
var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null');
var p;
function createKeypair() {
var opts;
if (BROWSER_SUPPORTS_ECDSA) {
opts = { type: 'ECDSA', bitlength: '256' };
} else {
opts = { type: 'RSA', bitlength: '2048' };
}
return BACME.domains.generateKeypair(opts).then(function (serverJwk) {
localStorage.setItem('server:' + key, JSON.stringify(serverJwk));
return serverJwk;
});
}
if (serverJwk) {
p = PromiseA.resolve(serverJwk);
} else {
p = createKeypair();
}
return p.then(function (_serverJwk) {
serverJwk = _serverJwk;
info.serverJwk = serverJwk;
// { serverJwk, domains }
return BACME.orders.generateCsr({
serverJwk: serverJwk
, domains: info.identifiers.map(function (ident) {
return ident.value;
})
}).then(function (csrweb64) {
return BACME.orders.finalize({
csr: csrweb64
, jwk: info.jwk
, finalizeUrl: info.finalizeUrl
, accountId: info.kid
});
}).then(function () {
function checkCert() {
return new PromiseA(function (resolve) {
setTimeout(resolve, 1000);
}).then(function () {
return BACME.orders.check({ orderUrl: info.orderUrl });
}).then(function (reply) {
if ('processing' === reply) {
return checkCert();
}
return reply;
});
}
return checkCert();
}).then(function (reply) {
return BACME.orders.receive({ certificateUrl: reply.certificate });
}).then(function (certs) {
console.log('WINNING!');
console.log(certs);
$qs('#js-fullchain').innerHTML = certs;
$qs("#js-download-fullchain-link").href =
"data:text/octet-stream;base64," + window.btoa(certs);
var wcOpts;
var pemName;
if (/^R/.test(info.serverJwk.kty)) {
pemName = 'RSA';
wcOpts = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } };
} else {
pemName = 'EC';
wcOpts = { name: "ECDSA", namedCurve: "P-256" };
}
return crypto.subtle.importKey(
"jwk"
, info.serverJwk
, wcOpts
, true
, ["sign"]
).then(function (privateKey) {
return window.crypto.subtle.exportKey("pkcs8", privateKey);
}).then (function (keydata) {
var pem = spkiToPEM(keydata, pemName);
$qs('#js-privkey').innerHTML = pem;
$qs("#js-download-privkey-link").href =
"data:text/octet-stream;base64," + window.btoa(pem);
steps[i]();
});
});
}).catch(function (err) {
console.error(err.toString());
window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know.");
});
};
steps[5] = function () {
updateProgress(2);
hideForms();
$qs('.js-acme-form-download').hidden = false;
};
steps[1]();
var params = new URLSearchParams(window.location.search);
var apiType = params.get('acme-api-type') || "staging-v02";
if(params.has('acme-domains')) {
console.log("acme-domains param: ", params.get('acme-domains'));
$qs('.js-acme-domains').value = params.get('acme-domains');
$qsa('.js-acme-api-type').forEach(function(ele) {
if(ele.value === apiType) {
ele.checked = true;
}
});
updateApiType();
steps[2]();
submitForm();
}
$qs('body').hidden = false;
}());

495
js/bacme.js → app/js/bacme.js

@ -1,9 +1,12 @@
/*global CSR*/
// CSR takes a while to load after the page load
(function (exports) {
'use strict';
var BACME = exports.BACME = {};
var webFetch = exports.fetch;
var webCrypto = exports.crypto;
var Promise = exports.Promise;
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
var directory;
@ -15,7 +18,6 @@ var accountKeypair;
var accountJwk;
var accountUrl;
var signedAccount;
BACME.challengePrefixes = {
'http-01': '/.well-known/acme-challenge'
@ -23,38 +25,41 @@ BACME.challengePrefixes = {
};
BACME._logHeaders = function (resp) {
console.log('Headers:');
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
console.log('Headers:');
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
};
BACME._logBody = function (body) {
console.log('Body:');
console.log(JSON.stringify(body, null, 2));
console.log('');
console.log('Body:');
console.log(JSON.stringify(body, null, 2));
console.log('');
};
BACME.directory = function (opts) {
return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) {
BACME._logHeaders(resp);
return resp.json().then(function (body) {
directory = body;
return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) {
BACME._logHeaders(resp);
return resp.json().then(function (reply) {
if (/error/.test(reply.type)) {
return Promise.reject(new Error(reply.detail || reply.type));
}
directory = reply;
nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce';
accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account';
orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order";
BACME._logBody(body);
return body;
});
});
BACME._logBody(reply);
return reply;
});
});
};
BACME.nonce = function () {
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) {
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) {
BACME._logHeaders(resp);
nonce = resp.headers.get('replay-nonce');
console.log('Nonce:', nonce);
// resp.body is empty
return resp.headers.get('replay-nonce');
});
nonce = resp.headers.get('replay-nonce');
console.log('Nonce:', nonce);
// resp.body is empty
return resp.headers.get('replay-nonce');
});
};
BACME.accounts = {};
@ -62,66 +67,38 @@ BACME.accounts = {};
// type = ECDSA
// bitlength = 256
BACME.accounts.generateKeypair = function (opts) {
var wcOpts = {};
// ECDSA has only the P curves and an associated bitlength
if (/^EC/i.test(opts.type)) {
wcOpts.name = 'ECDSA';
if (/256/.test(opts.bitlength)) {
wcOpts.namedCurve = 'P-256';
}
}
// RSA-PSS is another option, but I don't think it's used for Let's Encrypt
// I think the hash is only necessary for signing, not generation or import
if (/^RS/i.test(opts.type)) {
wcOpts.name = 'RSASSA-PKCS1-v1_5';
wcOpts.modulusLength = opts.bitlength;
if (opts.bitlength < 2048) {
wcOpts.modulusLength = opts.bitlength * 8;
}
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
wcOpts.hash = { name: "SHA-256" };
}
return BACME.generateKeypair(opts).then(function (result) {
accountKeypair = result;
// https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey
var extractable = true;
return webCrypto.subtle.generateKey(
wcOpts
, extractable
, [ 'sign', 'verify' ]
).then(function (result) {
accountKeypair = result;
return webCrypto.subtle.exportKey(
"jwk"
, result.privateKey
).then(function (privJwk) {
return webCrypto.subtle.exportKey(
"jwk"
, result.privateKey
).then(function (privJwk) {
accountJwk = privJwk;
console.log('private jwk:');
console.log(JSON.stringify(privJwk, null, 2));
accountJwk = privJwk;
console.log('private jwk:');
console.log(JSON.stringify(privJwk, null, 2));
return privJwk;
/*
return webCrypto.subtle.exportKey(
"pkcs8"
, result.privateKey
).then(function (keydata) {
console.log('pkcs8:');
console.log(Array.from(new Uint8Array(keydata)));
return webCrypto.subtle.exportKey(
"pkcs8"
, result.privateKey
).then(function (keydata) {
console.log('pkcs8:');
console.log(Array.from(new Uint8Array(keydata)));
return privJwk;
//return accountKeypair;
});
});
*/
})
});
});
});
};
// json to url-safe base64
BACME._jsto64 = function (json) {
return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
};
var textEncoder = new TextEncoder();
@ -158,7 +135,7 @@ BACME._importKey = function (jwk) {
e: priv.e
, kty: priv.kty
, n: priv.n
}
};
if (!priv.p) {
priv = null;
}
@ -167,7 +144,7 @@ BACME._importKey = function (jwk) {
return window.crypto.subtle.importKey(
"jwk"
, pub
, wcOpts
, wcOpts
, extractable
, [ "verify" ]
).then(function (publicKey) {
@ -253,26 +230,38 @@ BACME.accounts.sign = function (opts) {
payloadJson
);
// TODO RSA
var protectedJson =
{ nonce: opts.nonce
, url: accountUrl
, alg: abstractKey.meta.alg
, jwk: {
kty: opts.jwk.kty
, crv: opts.jwk.crv
, x: opts.jwk.x
, y: opts.jwk.y
}
, jwk: null
};
if (/EC/i.test(opts.jwk.kty)) {
protectedJson.jwk = {
crv: opts.jwk.crv
, kty: opts.jwk.kty
, x: opts.jwk.x
, y: opts.jwk.y
};
} else if (/RS/i.test(opts.jwk.kty)) {
protectedJson.jwk = {
e: opts.jwk.e
, kty: opts.jwk.kty
, n: opts.jwk.n
};
} else {
return Promise.reject(new Error("[acme.accounts.sign] unsupported key type '" + opts.jwk.kty + "'"));
}
console.log('protected:');
console.log(protectedJson);
var protected64 = BACME._jsto64(
protectedJson
);
// Note: this function hashes before signing so send data, not the hash
return BACME._sign({
// Note: this function hashes before signing so send data, not the hash
return BACME._sign({
abstractKey: abstractKey
, payload64: payload64
, protected64: protected64
@ -280,30 +269,29 @@ BACME.accounts.sign = function (opts) {
});
};
var account;
var accountId;
BACME.accounts.set = function (opts) {
nonce = null;
return window.fetch(accountUrl, {
mode: 'cors'
, method: 'POST'
, headers: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(opts.signedAccount)
}).then(function (resp) {
BACME._logHeaders(resp);
nonce = resp.headers.get('replay-nonce');
accountId = resp.headers.get('location');
console.log('Next nonce:', nonce);
console.log('Location/kid:', accountId);
if (!resp.headers.get('content-type')) {
console.log('Body: <none>');
return { kid: accountId };
}
return resp.json().then(function (result) {
nonce = null;
return window.fetch(accountUrl, {
mode: 'cors'
, method: 'POST'
, headers: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(opts.signedAccount)
}).then(function (resp) {
BACME._logHeaders(resp);
nonce = resp.headers.get('replay-nonce');
accountId = resp.headers.get('location');
console.log('Next nonce:', nonce);
console.log('Location/kid:', accountId);
if (!resp.headers.get('content-type')) {
console.log('Body: <none>');
return { kid: accountId };
}
return resp.json().then(function (result) {
if (/^Error/i.test(result.detail)) {
return Promise.reject(new Error(result.detail));
}
@ -311,21 +299,20 @@ BACME.accounts.set = function (opts) {
BACME._logBody(result);
return result;
});
});
});
});
};
var orderUrl;
var signedOrder;
BACME.orders = {};
// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
// signedAccount
BACME.orders.sign = function (opts) {
var payload64 = BACME._jsto64({ identifiers: opts.identifiers });
var payload64 = BACME._jsto64({ identifiers: opts.identifiers });
return BACME._importKey(opts.jwk).then(function (abstractKey) {
return BACME._importKey(opts.jwk).then(function (abstractKey) {
var protected64 = BACME._jsto64(
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid }
);
@ -345,36 +332,35 @@ BACME.orders.sign = function (opts) {
});
};
var order;
var currentOrderUrl;
var authorizationUrls;
var finalizeUrl;
BACME.orders.create = function (opts) {
nonce = null;
return window.fetch(orderUrl, {
mode: 'cors'
, method: 'POST'
, headers: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(opts.signedOrder)
}).then(function (resp) {
nonce = null;
return window.fetch(orderUrl, {
mode: 'cors'
, method: 'POST'
, headers