Compare commits

...

45 Commits

Author SHA1 Message Date
AJ ONeal 7484ffcd11 WIP: moving to bluecrypt 2019-05-19 01:10:49 -06:00
AJ ONeal 98c8db8973 WIP: breaking into smaller pieces 2019-05-19 00:21:26 -06:00
AJ ONeal 8ce44bc414 rebrand Root 2019-04-09 07:50:34 +00:00
AJ ONeal ab67741604 rebrand Root 2019-04-09 07:46:59 +00:00
AJ ONeal b7dd224426 update to rootprojects.org 2019-04-08 19:51:33 +00:00
John Shaver 65974b57c1 Merge branch 'live-site' of ssh://git.coolaj86.com:22042/coolaj86/greenlock.html into live-site 2019-03-16 21:58:16 -07:00
John Shaver b1fc2bcc14 fix wildcard certificate verification 2019-03-16 21:51:41 -07:00
AJ ONeal 8d31bf7754 GA -> web-only 2018-11-16 05:50:15 +00:00
AJ ONeal 1c57a342d0 reposition 2018-11-15 06:57:35 +00:00
AJ ONeal f27b386ba4 update legal link (and link to 'us') 2018-11-15 06:55:10 +00:00
AJ ONeal 55a30888ff add privacy policy 2018-11-15 06:50:37 +00:00
AJ ONeal 95e807be73 test key support BEFORE creating keys 2018-11-10 18:50:05 +00:00
AJ ONeal 268f83b49e tested working in firefox 2018-11-10 09:18:59 +00:00
AJ ONeal 09ff0b3adc use the opts we built, duh 2018-11-10 07:58:02 +00:00
AJ ONeal 8a7183ed9c don't eager assign CSR 2018-11-10 07:20:53 +00:00
AJ ONeal a7462db2c8 test ecdsa support, spaces for whitespace 2018-11-09 21:21:40 -07:00
AJ ONeal 2cc5a41268 lint and fix and use domains.generateKeypair 2018-11-09 21:05:53 -07:00
AJ ONeal d63d8e1aed lint 2018-11-09 20:41:41 -07:00
John Shaver 6f188cefb8 Now uses RSA keys for firefox browser. 2018-11-09 13:57:59 -08:00
AJ ONeal bddf85dfe6 Merge branch 'live-site' of ssh://git.coolaj86.com:22042/coolaj86/greenlock.html into live-site 2018-09-17 05:14:03 +00:00
AJ ONeal c6052bcf73 update contact info 2018-09-17 05:13:55 +00:00
John Shaver 78515f165c Fix legal link and preloading files 2018-09-12 09:29:04 -07:00
John Shaver 68253cbe54 You can now tab select check boxes. 2018-09-10 23:39:49 -07:00
John Shaver 9143545389 Added download links for certs and changed some formatting. 2018-08-21 14:15:09 -07:00
John Shaver 3e865a2fb7 Added styling to key/cert page. 2018-08-21 13:10:18 -07:00
John Shaver 6ce81beaec Fixed link to hosted app. 2018-08-21 13:06:27 -07:00
John Shaver c6c06b06f0 First 2 steps down. 2018-08-17 22:21:12 -07:00
John Shaver a32287c3f8 Added a terrible temorary fix to the tables. 2018-08-16 20:24:51 -07:00
John Shaver 6a05569ab5 Some style chages on first/second step. 2018-08-16 20:01:51 -07:00
John Shaver c3c9696799 Progress bar is in place. 2018-08-11 22:15:06 -07:00
John Shaver b86074920a Added Javascript needed warning. 2018-07-26 14:36:47 -07:00
John Shaver 8610baf0e0 fixed blink on loading strait to step2 2018-07-26 13:38:07 -07:00
John Shaver 3cfbc9339b Tied in new homepage with the rest of the app. 2018-07-26 12:55:31 -07:00
John Shaver 62f3d28a71 Some styling updates and created a separate front page. 2018-07-23 14:23:10 -07:00
John Shaver 430b589038 Center, shrink logo. 2018-07-20 14:41:34 -07:00
John Shaver 6d3c3a9e61 No longer fails due to new challenge types. 2018-07-19 15:50:27 -07:00
AJ ONeal 61b2c76822 Merge branch 'master' into live-site 2018-05-14 19:31:54 +00:00
AJ ONeal d0ea6822ea Merge branch 'master' into live-site 2018-05-14 19:26:52 +00:00
AJ ONeal 4bfef46295 Merge branch 'master' into live-site 2018-05-14 18:56:59 +00:00
AJ ONeal 42a589f5c0 add google analytics 2018-05-14 18:55:21 +00:00
AJ ONeal 5d07cec7a3 Merge branch 'master' into live-site 2018-05-14 18:54:05 +00:00
AJ ONeal ceb109e652 fix typo 2018-05-05 04:42:17 +00:00
AJ ONeal ed2739cb20 add line break 2018-05-05 00:01:54 +00:00
AJ ONeal 2be09788ce Merge branch 'master' into live-site 2018-05-05 00:01:03 +00:00
AJ ONeal 3854e6b430 save contact 2018-05-04 23:59:05 +00:00
22 changed files with 5355 additions and 921 deletions

4
.gitignore vendored
View File

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

View File

@ -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 Symbolic link
View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

366
app/index.html Normal file
View File

@ -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 Normal file
View File

@ -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;
}());

View File

@ -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 = {};
return BACME.generateKeypair(opts).then(function (result) {
accountKeypair = result;
// 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';
}
}
return webCrypto.subtle.exportKey(
"jwk"
, result.privateKey
).then(function (privJwk) {
// 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" };
}
// 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) {
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);
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>');
if (!resp.headers.get('content-type')) {
console.log('Body: <none>');
return { kid: accountId };
}
return { kid: accountId };
}
return resp.json().then(function (result) {
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: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(opts.signedOrder)
}).then(function (resp) {
BACME._logHeaders(resp);
currentOrderUrl = resp.headers.get('location');
nonce = resp.headers.get('replay-nonce');
console.log('Next nonce:', nonce);
currentOrderUrl = resp.headers.get('location');
nonce = resp.headers.get('replay-nonce');
console.log('Next nonce:', nonce);
return resp.json().then(function (result) {
return resp.json().then(function (result) {
if (/^Error/i.test(result.detail)) {
return Promise.reject(new Error(result.detail));
}
authorizationUrls = result.authorizations;
finalizeUrl = result.finalize;
authorizationUrls = result.authorizations;
finalizeUrl = result.finalize;
BACME._logBody(result);
result.url = currentOrderUrl;
return result;
});
});
});
});
};
BACME.challenges = {};
@ -395,22 +381,22 @@ BACME.challenges.all = function () {
return next();
};
BACME.challenges.view = function () {
var authzUrl = authorizationUrls.pop();
var token;
var challengeDomain;
var challengeUrl;
var authzUrl = authorizationUrls.pop();
var token;
var challengeDomain;
var challengeUrl;
return window.fetch(authzUrl, {
mode: 'cors'
}).then(function (resp) {
return window.fetch(authzUrl, {
mode: 'cors'
}).then(function (resp) {
BACME._logHeaders(resp);
return resp.json().then(function (result) {
// Note: select the challenge you wish to use
var challenge = result.challenges.slice(0).pop();
token = challenge.token;
challengeUrl = challenge.url;
challengeDomain = result.identifier.value;
return resp.json().then(function (result) {
// Note: select the challenge you wish to use
var challenge = result.challenges.slice(0).pop();
token = challenge.token;
challengeUrl = challenge.url;
challengeDomain = result.identifier.value;
BACME._logBody(result);
@ -424,8 +410,8 @@ BACME.challenges.view = function () {
//, url: challenge.url
//, domain: result.identifier.value,
};
});
});
});
});
};
var thumbprint;
@ -435,7 +421,7 @@ var dnsAuth;
var dnsRecord;
BACME.thumbprint = function (opts) {
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
var accountJwk = opts.jwk;
var keys;
@ -446,34 +432,34 @@ BACME.thumbprint = function (opts) {
keys = [ 'e', 'kty', 'n' ];
}
var accountPublicStr = '{' + keys.map(function (key) {
return '"' + key + '":"' + accountJwk[key] + '"';
}).join(',') + '}';
var accountPublicStr = '{' + keys.map(function (key) {
return '"' + key + '":"' + accountJwk[key] + '"';
}).join(',') + '}';
return window.crypto.subtle.digest(
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
, textEncoder.encode(accountPublicStr)
).then(function (hash) {
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
return String.fromCharCode(ch);
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
return window.crypto.subtle.digest(
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
, textEncoder.encode(accountPublicStr)
).then(function (hash) {
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
return String.fromCharCode(ch);
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
console.log('Thumbprint:');
console.log(opts);
console.log(accountPublicStr);
console.log(thumbprint);
console.log('Thumbprint:');
console.log(opts);
console.log(accountPublicStr);
console.log(thumbprint);
return thumbprint;
});
});
};
// { token, thumbprint, challengeDomain }
BACME.challenges['http-01'] = function (opts) {
// The contents of the key authorization file
keyAuth = opts.token + '.' + opts.thumbprint;
// The contents of the key authorization file
keyAuth = opts.token + '.' + opts.thumbprint;
// Where the key authorization file goes
httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token;
// Where the key authorization file goes
httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token;
console.log("echo '" + keyAuth + "' > '" + httpPath + "'");
@ -487,113 +473,138 @@ BACME.challenges['http-01'] = function (opts) {
BACME.challenges['dns-01'] = function (opts) {
console.log('opts.keyAuth for DNS:');
console.log(opts.keyAuth);
return window.crypto.subtle.digest(
{ name: "SHA-256", }
, textEncoder.encode(opts.keyAuth)
).then(function (hash) {
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
return String.fromCharCode(ch);
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
return window.crypto.subtle.digest(
{ name: "SHA-256", }
, textEncoder.encode(opts.keyAuth)
).then(function (hash) {
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
return String.fromCharCode(ch);
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
dnsRecord = '_acme-challenge.' + opts.challengeDomain;
dnsRecord = '_acme-challenge.' + opts.challengeDomain;
console.log('DNS TXT Auth:');
// The name of the record
console.log(dnsRecord);
// The TXT record value
console.log(dnsAuth);
console.log('DNS TXT Auth:');
// The name of the record
console.log(dnsRecord);
// The TXT record value
console.log(dnsAuth);
return {
type: 'TXT'
, host: dnsRecord
, answer: dnsAuth
};
});
});
};
var challengePollUrl;
// { jwk, challengeUrl, accountId (kid) }
BACME.challenges.accept = function (opts) {
var payload64 = BACME._jsto64(
{}
);
var payload64 = BACME._jsto64({});
return BACME._importKey(opts.jwk).then(function (abstractKey) {
var protected64 = BACME._jsto64(
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId }
);
return BACME._sign({
return BACME._sign({
abstractKey: abstractKey
, payload64: payload64
, protected64: protected64
});
}).then(function (signedAccept) {
nonce = null;
return window.fetch(
opts.challengeUrl
, { mode: 'cors'
, method: 'POST'
, headers: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(signedAccept)
}
).then(function (resp) {
nonce = null;
return window.fetch(
opts.challengeUrl
, { mode: 'cors'
, method: 'POST'
, headers: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(signedAccept)
}
).then(function (resp) {
BACME._logHeaders(resp);
nonce = resp.headers.get('replay-nonce');
nonce = resp.headers.get('replay-nonce');
console.log("ACCEPT NONCE:", nonce);
return resp.json().then(function (reply) {
return resp.json().then(function (reply) {
challengePollUrl = reply.url;
console.log('Challenge ACK:');
console.log(JSON.stringify(reply));
console.log('Challenge ACK:');
console.log(JSON.stringify(reply));
return reply;
});
});
});
});
});
});
};
BACME.challenges.check = function (opts) {
return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) {
return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) {
BACME._logHeaders(resp);
return resp.json().then(function (reply) {
challengePollUrl = reply.url;
return resp.json().then(function (reply) {
if (/error/.test(reply.type)) {
return Promise.reject(new Error(reply.detail || reply.type));
}
challengePollUrl = reply.url;
BACME._logBody(reply);
return reply;
});
});
return reply;
});
});
};
var domainKeypair;
var domainJwk;
BACME.generateKeypair = function (opts) {
var wcOpts = {};
// ECDSA has only the P curves and an associated bitlength
if (/^EC/i.test(opts.type)) {
wcOpts.name = 'ECDSA';
if (/256/.test(opts.bitlength)) {
wcOpts.namedCurve = 'P-256';
}
}
// RSA-PSS is another option, but I don't think it's used for Let's Encrypt
// I think the hash is only necessary for signing, not generation or import
if (/^RS/i.test(opts.type)) {
wcOpts.name = 'RSASSA-PKCS1-v1_5';
wcOpts.modulusLength = opts.bitlength;
if (opts.bitlength < 2048) {
wcOpts.modulusLength = opts.bitlength * 8;
}
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
wcOpts.hash = { name: "SHA-256" };
}
var extractable = true;
return window.crypto.subtle.generateKey(
wcOpts
, extractable
, [ 'sign', 'verify' ]
);
};
BACME.domains = {};
// TODO factor out from BACME.accounts.generateKeypair
BACME.domains.generateKeypair = function () {
var extractable = true;
return window.crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" }
, extractable
, [ 'sign', 'verify' ]
).then(function (result) {
domainKeypair = result;
// TODO factor out from BACME.accounts.generateKeypair even more
BACME.domains.generateKeypair = function (opts) {
return BACME.generateKeypair(opts).then(function (result) {
domainKeypair = result;
return window.crypto.subtle.exportKey(
"jwk"
, result.privateKey
).then(function (jwk) {
return window.crypto.subtle.exportKey(
"jwk"
, result.privateKey
).then(function (privJwk) {
domainJwk = jwk;
console.log('private jwk:');
console.log(JSON.stringify(jwk, null, 2));
domainJwk = privJwk;
console.log('private jwk:');
console.log(JSON.stringify(privJwk, null, 2));
return domainKeypair;
})
});
return privJwk;
});
});
};
// { serverJwk, domains }
@ -607,41 +618,44 @@ var certificateUrl;
// { csr, jwk, finalizeUrl, accountId }
BACME.orders.finalize = function (opts) {
var payload64 = BACME._jsto64(
{ csr: opts.csr }
);
var payload64 = BACME._jsto64(
{ csr: opts.csr }
);
return BACME._importKey(opts.jwk).then(function (abstractKey) {
var protected64 = BACME._jsto64(
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId }
);
return BACME._sign({
return BACME._sign({
abstractKey: abstractKey
, payload64: payload64
, protected64: protected64
});
}).then(function (signedFinal) {
nonce = null;
return window.fetch(
opts.finalizeUrl
, { mode: 'cors'
, method: 'POST'
, headers: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(signedFinal)
}
).then(function (resp) {
nonce = null;
return window.fetch(
opts.finalizeUrl
, { mode: 'cors'
, method: 'POST'
, headers: { 'Content-Type': 'application/jose+json' }
, body: JSON.stringify(signedFinal)
}
).then(function (resp) {
BACME._logHeaders(resp);
nonce = resp.headers.get('replay-nonce');
nonce = resp.headers.get('replay-nonce');
return resp.json().then(function (reply) {
certificateUrl = reply.certificate;
return resp.json().then(function (reply) {
if (/error/.test(reply.type)) {
return Promise.reject(new Error(reply.detail || reply.type));
}
certificateUrl = reply.certificate;
BACME._logBody(reply);
return reply;
});
});
});
});
});
});
};
BACME.orders.receive = function (opts) {
@ -672,6 +686,9 @@ BACME.orders.check = function (opts) {
BACME._logHeaders(resp);
return resp.json().then(function (reply) {
if (/error/.test(reply.type)) {
return Promise.reject(new Error(reply.detail || reply.type));
}
BACME._logBody(reply);
return reply;

2892
app/js/bluecrypt-acme.js Normal file

File diff suppressed because it is too large Load Diff

494
app/js/greenlock.js Normal file
View File

@ -0,0 +1,494 @@
(function () {
'use strict';
/*global URLSearchParams,Headers*/
var VERSION = '2';
// ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses )
// ECDSA P-384, P-521, and RSA 3072, 4096 are NOT recommend standards (and not properly supported)
var BROWSER_SUPPORTS_RSA;
var ECDSA_OPTS = { kty: 'EC', namedCurve: 'P-256' };
var RSA_OPTS = { kty: 'RSA', modulusLength: 2048 };
var Promise = window.Promise;
var Keypairs = window.Keypairs;
var ACME = window.ACME;
var CSR = window.CSR;
var $qs = function (s) { return window.document.querySelector(s); };
var $qsa = function (s) { return window.document.querySelectorAll(s); };
var acme;
var accountStuff;
var info = {};
var steps = {};
var i = 1;
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);
}
function hideForms() {
$qsa('.js-acme-form').forEach(function (el) {
el.hidden = true;
});
}
function updateProgress(currentStep) {
var progressSteps = $qs("#js-progress-bar").children;
var j;
for (j = 0; j < progressSteps.length; j += 1) {
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.");
});
}
function testEcdsaSupport() {
/*
var opts = {
kty: $('input[name="kty"]:checked').value
, namedCurve: $('input[name="ec-crv"]:checked').value
, modulusLength: $('input[name="rsa-len"]:checked').value
};
*/
}
function testRsaSupport() {
return Keypairs.generate(RSA_OPTS);
}
function testKeypairSupport() {
// fix previous browsers
var isCurrent = (localStorage.getItem('version') === VERSION);
if (!isCurrent) {
localStorage.clear();
localStorage.setItem('version', VERSION);
}
localStorage.setItem('version', VERSION);
return testRsaSupport().then(function () {
console.info("[crypto] RSA is supported");
BROWSER_SUPPORTS_RSA = true;
return BROWSER_SUPPORTS_RSA;
}).catch(function () {
console.warn("[crypto] RSA is NOT fully supported");
BROWSER_SUPPORTS_RSA = false;
return BROWSER_SUPPORTS_RSA;
});
}
function getServerKeypair() {
var sortedAltnames = info.identifiers.map(function (ident) { return ident.value; }).sort().join(',');
var serverJwk = JSON.parse(localStorage.getItem('server:' + sortedAltnames) || 'null');
if (serverJwk) {
return PromiseA.resolve(serverJwk);
}
var keypairOpts;
// TODO allow for user preference
if (BROWSER_SUPPORTS_RSA) {
keypairOpts = RSA_OPTS;
} else {
keypairOpts = ECDSA_OPTS;
}
return Keypairs.generate(RSA_OPTS).catch(function (err) {
console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):");
throw err;
}).then(function (pair) {
localStorage.setItem('server:'+sortedAltnames, JSON.stringify(pair.private));
return pair.private;
});
}
function getAccountKeypair(email) {
var json = localStorage.getItem('account:'+email);
if (json) {
return Promise.resolve(JSON.parse(json));
}
return Keypairs.generate(ECDSA_OPTS).catch(function (err) {
console.warn("[Error] Keypairs.generate(" + JSON.stringify(ECDSA_OPTS) + "):\n", err);
return Keypairs.generate(RSA_OPTS).catch(function (err) {
console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):");
throw err;
});
}).then(function (pair) {
localStorage.setItem('account:'+email, JSON.stringify(pair.private));
return pair.private;
});
}
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;
}
}
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'
, timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone
, 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; }
});
var acmeDirectoryUrl = $qs('.js-acme-directory-url').value;
acme = ACME.create({ Keypairs: Keypairs, CSR: CSR });
return acme.init(acmeDirectoryUrl).then(function (directory) {
$qs('.js-acme-tos-url').href = directory.meta.termsOfService;
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 ping with version and account creation
setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
function checkTos(tos) {
if (info.agree) {
return tos;
} else {
return '';
}
}
return getAccountKeypair(email).then(function (jwk) {
// TODO save account id rather than always retrieving it?
return acme.accounts.create({
email: email
, agreeToTerms: checkTos
, accountKeypair: { privateKeyJwk: jwk }
}).then(function (account) {
console.log("account created result:", account);
accountStuff.account = account;
accountStuff.privateJwk = jwk;
accountStuff.email = email;
accountStuff.acme = acme; // TODO XXX remove
}).catch(function (err) {
console.error("A bad thing happened:");
console.error(err);
window.alert(err.message || JSON.stringify(err, null, 2));
return new Promise(function () {
// stop the process cold
console.warn('TODO: resume at ask email?');
});
});
}).then(function () {
var jwk = accountStuff.privateJwk;
var account = accountStuff.account;
return acme.orders.create({
account: account
, accountKeypair: { privateKeyJwk: jwk }
, identifiers: info.identifiers
}).then(function (order) {
return acme.orders.create({
signedOrder: signedOrder
}).then(function (order) {
accountStuff.order = order;
var claims = order.challenges;
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'
};
options.challengePriority = [ 'http-01', 'dns-01' ];
// 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 () {
options.challengeTypes = [ 'dns-01' ];
if ('http-01' === $qs('.js-acme-challenge-type:checked').value) {
options.challengeTypes.unshift('http-01');
}
console.log('primary challenge type is:', options.challengeTypes[0]);
return getAccountKeypair(email).then(function (jwk) {
// for now just show the next page immediately (its a spinner)
// TODO put a test challenge in the list
// TODO warn about wait-time if DNS
steps[i]();
return getServerKeypair().then(function () {
return acme.orders.finalize({
account: accountStuff.account
, accountKeypair: { privateKeyJwk: jwk }
, order: accountStuff.order
, domainKeypair: 'TODO'
});
}).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]();
});
});
}).then(function () {
return submitForm();
});
};
// spinner
steps[4] = function () {
updateProgress(1);
hideForms();
$qs('.js-acme-form-poll').hidden = false;
};
steps[4].submit = function () {
console.log('Congrats! Auto advancing...');
}).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";
$qsa('.js-acme-api-type').forEach(function ($el) {
$el.addEventListener('change', updateApiType);
});
updateApiType();
$qsa('.js-acme-form').forEach(function ($el) {
$el.addEventListener('submit', function (ev) {
ev.preventDefault();
submitForm(ev);
});
});
$qsa('.js-acme-challenge-type').forEach(function ($el) {
$el.addEventListener('change', updateChallengeType);
});
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;
return testKeypairSupport().then(function (rsaSupport) {
if (rsaSupport) {
return true;
}
return testRsaSupport().then(function () {
console.info('[crypto] RSA is supported');
}).catch(function (err) {
console.error('[crypto] could not use either RSA nor EC.');
console.error(err);
window.alert("Generating secure certificates requires a browser with cryptography support."
+ "Please consider a recent version of Chrome, Firefox, or Safari.");
throw err;
});
});
}());

263
app/styles/main.css Normal file
View File

@ -0,0 +1,263 @@
body {
font-size: 18px;
font-family: Source Sans Pro, sans-serif;
margin: 0;
line-height: 1.33;
color: #1a1a1a;
}
h1 {
text-align: center;
font-size: 1.77777778em;
}
a {
color: #1a1a1a;
}
input[type=email], input[type=text] {
font-size: 1em;
padding: 0.444444444em 0.888889em;
width: 100%;
border: solid 1px #d9d9d9;
border-radius: 2px;
}
pre {
margin: 0;
font-family: Source Code Pro, monospace;
}
.column-row {
width: 22.222222em;
}
.column-container {
display: flex;
flex-direction: column;
align-items: center;
}
.progress-bar {
height: 0;
border: solid 1px #5bc17f;
background-color: #5bc17f;
display: flex;
justify-content: space-between;
align-items: center;
width: 22em;
margin: 1.388888889em auto;
}
.greenlock-logo-badge > img {
width: 100%;
}
.greenlock-logo-badge {
display: inline-block;
border: solid 1px #d9d9d9;
border-radius: 500px;
width: 5.333333333em;
height: 5.333333333em;
margin-top: 4.277777778em;
}
.header-row {
text-align: center;
}
.progress-bar-step {
position: relative;
margin: -0.722222222em -0.166666667em;
display: inline-block;
background-color: white;
/* border-radius: 100%; */
padding: 0 0.111111em;
}
.progress-bar-step > .circle {
content: "";
display: inline-block;
border: solid 0.111111111em #5bc17f;
width: 0.888888889em;
height: 0.888888889em;
border-radius: 100%;
background: white;
}
.progress-step-label {
text-align: center;
position: absolute;
left: 50%;
=: block font-size: ;
top: 139%;
font-size: 0.722222222em;
white-space: nowrap;
}
.progress-step-label > div {
position: relative;
right: 50%;
}
.greenlock-name {
color: #808080;
}
.file-preview {
background: #f7f7f7;
position: relative;
font-size: 0.833333333em;
padding: 1.6em 2.9333em 1.6em 1.6em;
}
.js-progress-step-complete > .circle, .js-progress-step-started > .circle {
background-color: #5bc17f;
}
.progress-bar-step.js-progress-step-complete svg {
fill: white;
/* stroke: none; */
display: initial;
}
.checkbox-array {
display: flex;
flex-direction: column;
padding: 1em 0;
}
.checkbox-array input[type=checkbox] {
opacity: 0;
position: absolute;
}
.checkbox-array input[type=checkbox] ~ .icon-checked-box {
display: none;
}
.checkbox-array input[type=checkbox] ~ .icon-unchecked-box {
display: initial;
}
.checkbox-array input[type=checkbox]:checked ~ .icon-checked-box {
display: initial;
}
.checkbox-array input[type=checkbox]:checked ~ .icon-unchecked-box {
display: none;
}
.checkbox-array .icon-checked-box, .checkbox-array .icon-unchecked-box {
width: 1.333333333em;
fill: #5bc17f;
margin-right: 0.666666667em;
}
.checkbox-array label {
display: flex;
height: 1.333333333em;
font-size: 0.833333333em;
margin: 0.4em 0;
}
.checkbox-array input[type=checkbox]:focus ~ .icon-checked-box, .checkbox-array input[type=checkbox]:focus ~ .icon-unchecked-box {
background: #5bc17f52;
}
.email-usage {
color: #666666;
font-size: 0.833333333em;
margin: 2em 0;
}
.button-next {
width: 100%;
background-color: #5bc17f;
border: none;
font-size: 1em;
color: white;
padding: 0.44444em;
margin: 1em 0;
}
.tabbed-selector label {
width: 50%;
padding: 0.5em 0;
}
.tabbed-selector {
display: flex;
font-weight: bold;
text-align: center;
}
.tabbed-selector input[type=radio] {
display: none;
}
.download-file svg {
fill: #5bc17f;
width: 1.333333333em;
}
.download-file a {
color: #5bc17f;
}
.mdicon {
position: relative;
top: 0.4em;
}
.http-verification-info {
padding-right: 6.933333333em;
}
.paper-fold {
position: absolute;
width: 2em;
height: 2em;
border-left: solid #d9d9d9 1px;
border-bottom: solid #d9d9d9 1px;
right: 0;
top: 0;
background: linear-gradient(45deg, #f7f7f7 0%,#f7f7f7 50%,#ffffff 50%,#ffffff 100%);
}
.file-ver-info-header {
color: #808080;
}
.http-verification-info hr {
border: none;
border-bottom: solid 1px #d9d9d9;
}
.acme-ver-uri {
word-break: break-all;
margin: auto;
}
.acme-ver-dns-label {
margin: 1.777777778em 0 0.444444444em 0;
border-bottom: solid 1px #d9d9d9;
font-weight: bold;
padding-bottom: 0.166666667em;
}
.tabbed-selector input[type="radio"]:checked ~ div {
border: solid 1px #5bc17f;
background-color: #5bc17f;
}
.file-preview pre {
white-space: pre-line;
word-break: break-all;
}
.cert-download-container {
margin: 0 -31%;
}

Binary file not shown.

Binary file not shown.

BIN
img/greenlock-146.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,227 +1,100 @@
<html>
<head>
<title>Greenlock&trade;</title>
<meta property="og:image" content="https://greenlock.ppl.family/img/greenlock-mark-400x400.png" />
<meta property="og:image" content="https://greenlock.domains/img/greenlock-mark-400x400.png" />
<link href="styles/main.css" rel="stylesheet">
<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;
}
</style>
<link rel="preload" href="./app/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="./app/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
<link rel="prefetch" href="./app/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
<link rel="prefetch" href="./app/js/app.js">
<link rel="prefetch" href="./app/js/bacme.js">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/common.js">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/asn1.js">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_schema.js">
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_simpl.js">
<link rel="prefetch" href="./app/js/browser-csr/v1.0.0-alpha/csr.js">
</head>
<body hidden>
<img width="410px" src="img/greenlock-820x150.png">
<div>
<br>
<h3>Greenlock&trade; - Instant, Free SSL Certificates via Let's Encrypt v2</h3>
<br>
<br>
<br>
</div>
<body class="js-app-ready">
<script>
document.querySelector('body').classList.remove("js-app-ready");
</script>
<div class="column-container wide">
<!-- Step 1 Choose Domain(s) -->
<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" type="email" placeholder="john@doe.family" required>
<br>
<br>
<label><input class="js-acme-account-tos" type="checkbox" required>
Agree to <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt&trade; Terms of Service</a>?</label>
<br>
<br>
<label><input class="js-greenlock-account-tos" type="checkbox" required>
Agree to <a class="js-gl-tos" target="_blank" href="./legal.html">Greenlock&trade; Terms of Service</a>?</label>
<br>
<br>
<!--
<a href="#">advanced (use existing account)</a>
<br>
<br>
-->
<button type="submit">Next</button>
</form>
<!-- Step 3 Set Challanges -->
<form class="js-acme-form js-acme-form-challenges">
<h1>How will you validate your domain?</h1>
<br>
<label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="http-01" checked required>
File Upload to HTTP Web Server</label>
<br>
<label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="dns-01" required>
TXT Records on DNS Name Server</label>
<br>
<div class="js-acme-challenges">
<h2>Verify Domains &amp; Sub-Domains</h2>
<table class="js-acme-table-http-01">
<thead>
<tr>
<th>Hostname</th>
<th>File Location</th>
<th>File Contents</th>
</tr>
</thead>
<tbody>
<tr>
<td>example.com</td>
<td>.well-known/acme-challenge/xxx</td>
<td>sec.ret</td>
</tr>
</tbody>
</table>
<table class="js-acme-table-dns-01">
<thead>
<tr>
<th>Hostname</th>
<th>TXT Host</th>
<th>TXT Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>example.com</td>
<td>_acme-challenge.example.com</td>
<td>4A54</td>
</tr>
</tbody>
</table>
<div class="column-row">
<img src="img/greenlock-146.png">
</div>
<div class="js-acme-wildcard">
<h2>Verify Wildcard Domains</h2>
<table class="js-acme-table-wildcard">
<thead>
<tr>
<th>Hostname</th>
<th>TXT Host</th>
<th>TXT Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>example.com</td>
<td>_acme-challenge.example.com</td>
<td>4A54</td>
</tr>
</tbody>
</table>
<div class="column-row">
<h1>Get the green lock for your website</h1>
</div>
<div class="column-row">
<div class="js-javascript-warning">
Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org. Please enable Javascript before continuing.
</div>
<form id="js-acme-form" action="./app/" method=>
<div class="domain-psuedo-input">
<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>
</div>
<button type="submit">Go</button>
<div class="domain-subtext">Domain, subdomain, or wildcard domain</div>
<button 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>
<h2><label>privkey.pem</label></h2>
<textarea cols="80" rows="10" class="js-privkey">-</textarea>
<div class="acme-advanced-fields">
<label><input name="acme-api-type" type="radio" value="v02" checked required>
Production
</label>
<label><input name="acme-api-type" type="radio" value="staging-v02" required>
Testing</label>
<input id="js-acme-api-url" type="url" placeholder="ACME directory url">
<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>
</div>
</form>
</div>
<div>
<h2><label>fullchain.pem</label></h2>
<textarea cols="80" rows="60" class="js-fullchain">-</textarea>
<div class="column-row">
<div class="why-you-need">
<h2>Why you need HTTPS</h2>
SSL Certificates are required for secure login, accepting payments, and for browsers like Google Chrome to stop showing security warnings to your users.
</div>
</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>
<br>
<br>
<br>
<div><small>
<h3></h3>
<a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git)
<!-- 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.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
</small></div>
<pre><code>wget https://greenlock.domains --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
-->
<script src="./js/bacme.js"></script>
<script src="./js/app.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>
</body>
</html>

View File

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

493
js/app.js
View File

@ -3,487 +3,28 @@
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;
$qs('.js-javascript-warning').hidden = true;
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
function updateApiType() {
var input = this || Array.prototype.filter.call(
$qsa('.js-acme-api-type'), function ($el) { return $el.checked; }
)[0];
console.log('ACME api type radio:', input.value);
$qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value);
var formData = new FormData($qs("#js-acme-form"));
console.log('ACME api type radio:');
var value = formData.get("acme-api-type");
$qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value);
}
$qsa('.js-acme-api-type').forEach(function ($el) {
$el.addEventListener('change', updateApiType);
});
$qs('#js-acme-form').addEventListener('change', updateApiType);
updateApiType();
function hideForms() {
$qsa('.js-acme-form').forEach(function (el) {
el.hidden = true;
try {
document.fonts.load().then(function() {
$qs('body').classList.add("js-app-ready");
}).catch(function(error) {
$qs('body').classList.add("js-app-ready");
});
} catch(e) {
setTimeout(function() {$qs('body').classList.add("js-app-ready");}, 200);
}
function submitForm(ev) {
var j = i;
i += 1;
steps[j].submit(ev);
}
$qsa('.js-acme-form').forEach(function ($el) {
$el.addEventListener('submit', function (ev) {
ev.preventDefault();
submitForm(ev);
});
});
function updateChallengeType() {
var input = this || Array.prototype.filter.call(
$qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; }
)[0];
console.log('ch type radio:', input.value);
$qs('.js-acme-table-wildcard').hidden = true;
$qs('.js-acme-table-http-01').hidden = true;
$qs('.js-acme-table-dns-01').hidden = true;
if (info.challenges.wildcard) {
$qs('.js-acme-table-wildcard').hidden = false;
}
if (info.challenges[input.value]) {
$qs('.js-acme-table-' + input.value).hidden = false;
}
}
$qsa('.js-acme-challenge-type').forEach(function ($el) {
$el.addEventListener('change', updateChallengeType);
});
steps[1] = function () {
hideForms();
$qs('.js-acme-form-domains').hidden = false;
};
steps[1].submit = function () {
info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) {
return { type: 'dns', value: hostname.toLowerCase().trim() };
});
info.identifiers.sort(function (a, b) {
if (a === b) { return 0; }
if (a < b) { return 1; }
if (a > b) { return -1; }
});
return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) {
$qs('.js-acme-tos-url').href = directory.meta.termsOfService;
return BACME.nonce().then(function (_nonce) {
nonce = _nonce;
console.log("MAGIC STEP NUMBER in 1 is:", i);
steps[i]();
});
});
};
steps[2] = function () {
hideForms();
$qs('.js-acme-form-account').hidden = false;
};
steps[2].submit = function () {
var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
info.contact = [ 'mailto:' + email ];
info.agree = $qs('.js-acme-account-tos').checked;
info.greenlockAgree = $qs('.js-gl-tos').checked;
// TODO
// options for
// * regenerate key
// * ECDSA / RSA / bitlength
// TODO ping with version and account creation
var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
var p;
function createKeypair() {
return BACME.accounts.generateKeypair({
type: 'ECDSA'
, bitlength: '256'
}).then(function (jwk) {
localStorage.setItem('account:' + email, JSON.stringify(jwk));
return jwk;
})
}
if (jwk) {
p = Promise.resolve(jwk);
} else {
p = createKeypair();
}
function createAccount(jwk) {
console.log('account jwk:');
console.log(jwk);
delete jwk.key_ops;
info.jwk = jwk;
return BACME.accounts.sign({
jwk: jwk
, contacts: [ 'mailto:' + email ]
, agree: info.agree
, nonce: nonce
, kid: kid
}).then(function (signedAccount) {
return BACME.accounts.set({
signedAccount: signedAccount
}).then(function (account) {
console.log('account:');
console.log(account);
kid = account.kid;
return kid;
});
});
}
return p.then(function (_jwk) {
jwk = _jwk;
kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
var p2
// TODO save account id rather than always retrieving it
if (kid) {
p2 = Promise.resolve(kid);
} else {
p2 = createAccount(jwk);
}
return p2.then(function (_kid) {
kid = _kid;
info.kid = kid;
return BACME.orders.sign({
jwk: jwk
, identifiers: info.identifiers
, kid: kid
}).then(function (signedOrder) {
return BACME.orders.create({
signedOrder: signedOrder
}).then(function (order) {
info.finalizeUrl = order.finalize;
info.orderUrl = order.url; // from header Location ???
return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) {
return BACME.challenges.all().then(function (claims) {
console.log('claims:');
console.log(claims);
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
var map = {
'http-01': '.js-acme-table-http-01'
, 'dns-01': '.js-acme-table-dns-01'
, 'wildcard': '.js-acme-table-wildcard'
}
var tpls = {};
info.challenges = obj;
Object.keys(map).forEach(function (k) {
var sel = map[k] + ' tbody';
console.log(sel);
tpls[k] = $qs(sel).innerHTML;
$qs(map[k] + ' tbody').innerHTML = '';
});
// TODO make Promise-friendly
return Promise.all(claims.map(function (claim) {
var hostname = claim.identifier.value;
return Promise.all(claim.challenges.map(function (c) {
var keyAuth = BACME.challenges['http-01']({
token: c.token
, thumbprint: thumbprint
, challengeDomain: hostname
});
return BACME.challenges['dns-01']({
keyAuth: keyAuth.value
, challengeDomain: hostname
}).then(function (dnsAuth) {
var data = {
type: c.type
, hostname: hostname
, url: c.url
, token: c.token
, keyAuthorization: keyAuth
, httpPath: keyAuth.path
, httpAuth: keyAuth.value
, dnsType: dnsAuth.type
, dnsHost: dnsAuth.host
, dnsAnswer: dnsAuth.answer
};
console.log('');
console.log('CHALLENGE');
console.log(claim);
console.log(c);
console.log(data);
console.log('');
if (claim.wildcard) {
obj.wildcard.push(data);
$qs(map.wildcard).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
} else {
obj[data.type].push(data);
if ('dns-01' === data.type) {
$qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
} else if ('http-01' === data.type) {
$qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.httpPath + '</td><td>' + data.httpAuth + '</td></tr>';
} else {
throw new Error('Unexpected type: ' + data.type);
}
}
});
}));
})).then(function () {
// hide wildcard if no wildcard
// hide http-01 and dns-01 if only wildcard
if (!obj.wildcard.length) {
$qs('.js-acme-wildcard').hidden = true;
}
if (!obj['http-01'].length) {
$qs('.js-acme-challenges').hidden = true;
}
updateChallengeType();
console.log("MAGIC STEP NUMBER in 2 is:", i);
steps[i]();
});
});
});
});
});
});
}).catch(function (err) {
console.error('Step \'' + i + '\' Error:');
console.error(err);
});
};
steps[3] = function () {
hideForms();
$qs('.js-acme-form-challenges').hidden = false;
};
steps[3].submit = function () {
var chType;
Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) {
if ($el.checked) {
chType = $el.value;
return true;
}
});
console.log('chType is:', chType);
var chs = [];
// do each wildcard, if any
// do each challenge, by selected type only
[ 'wildcard', chType].forEach(function (typ) {
info.challenges[typ].forEach(function (ch) {
// { jwk, challengeUrl, accountId (kid) }
chs.push({
jwk: info.jwk
, challengeUrl: ch.url
, accountId: info.kid
});
});
});
console.log("INFO.challenges !!!!!", info.challenges);
var results = [];
function nextChallenge() {
var ch = chs.pop();
if (!ch) { return results; }
return BACME.challenges.accept(ch).then(function (result) {
results.push(result);
return nextChallenge();
});
}
// for now just show the next page immediately (its a spinner)
steps[i]();
return nextChallenge().then(function (results) {
console.log('challenge status:', results);
var polls = results.slice(0);
var allsWell = true;
function checkPolls() {
return new Promise(function (resolve) {
setTimeout(resolve, 1000);
}).then(function () {
return Promise.all(polls.map(function (poll) {
return BACME.challenges.check({ challengePollUrl: poll.url });
})).then(function (polls) {
console.log(polls);
polls = polls.filter(function (poll) {
//return 'valid' !== poll.status && 'invalid' !== poll.status;
if ('pending' === poll.status) {
return true;
}
if ('valid' !== poll.status) {
allsWell = false;
console.warn('BAD POLL STATUS', poll);
}
// TODO show status in HTML
});
if (polls.length) {
return checkPolls();
}
return true;
});
});
}
return checkPolls().then(function () {
if (allsWell) {
return submitForm();
}
});
});
};
// spinner
steps[4] = function () {
hideForms();
$qs('.js-acme-form-poll').hidden = false;
}
steps[4].submit = function () {
console.log('Congrats! Auto advancing...');
var key = info.identifiers.map(function (ident) { return ident.value; }).join(',');
var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null');
var p;
function createKeypair() {
return BACME.accounts.generateKeypair({
type: 'ECDSA'
, bitlength: '256'
}).then(function (serverJwk) {
localStorage.setItem('server:' + key, JSON.stringify(serverJwk));
return serverJwk;
})
}
if (serverJwk) {
p = Promise.resolve(serverJwk);
} else {
p = createKeypair();
}
return p.then(function (_serverJwk) {
serverJwk = _serverJwk;
info.serverJwk = serverJwk;
// { serverJwk, domains }
return BACME.orders.generateCsr({
serverJwk: serverJwk
, domains: info.identifiers.map(function (ident) {
return ident.value;
})
}).then(function (csrweb64) {
return BACME.orders.finalize({
csr: csrweb64
, jwk: info.jwk
, finalizeUrl: info.finalizeUrl
, accountId: info.kid
});
}).then(function () {
function checkCert() {
return new Promise(function (resolve) {
setTimeout(resolve, 1000);
}).then(function () {
return BACME.orders.check({ orderUrl: info.orderUrl });
}).then(function (reply) {
if ('processing' === reply) {
return checkCert();
}
return reply;
});
}
return checkCert();
}).then(function (reply) {
return BACME.orders.receive({ certificateUrl: reply.certificate });
}).then(function (certs) {
console.log('WINNING!');
console.log(certs);
$qs('.js-fullchain').value = certs;
// https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
function spkiToPEM(keydata){
var keydataS = arrayBufferToString(keydata);
var keydataB64 = window.btoa(keydataS);
var keydataB64Pem = formatAsPem(keydataB64);
return keydataB64Pem;
}
function arrayBufferToString( buffer ) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return binary;
}
function formatAsPem(str) {
var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n';
while(str.length > 0) {
finalString += str.substring(0, 64) + '\n';
str = str.substring(64);
}
finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----';
return finalString;
}
var wcOpts;
var pemName;
if (/^R/.test(info.serverJwk.kty)) {
pemName = 'RSA';
wcOpts = {
name: "RSASSA-PKCS1-v1_5"
, hash: { name: "SHA-256" }
};
} else {
pemName = 'EC';
wcOpts = {
name: "ECDSA"
, namedCurve: "P-256"
}
}
return crypto.subtle.importKey(
"jwk"
, info.serverJwk
, wcOpts
, true
, ["sign"]
).then(function (privateKey) {
return window.crypto.subtle.exportKey("pkcs8", privateKey);
}).then (function (keydata) {
var pem = spkiToPEM(keydata);
$qs('.js-privkey').value = pem;
steps[i]();
}).catch(function(err){
console.error(err);
});
});
});
};
steps[5] = function () {
hideForms();
$qs('.js-acme-form-download').hidden = false;
}
steps[1]();
$qs('body').hidden = false;
}());

201
legal.html Normal file
View File

@ -0,0 +1,201 @@
<h1>Greetings!</h1>
<p>I, AJ ONeal, am not a big fan of legalize, but I am a fan of communicating
clearly. I hope that this accomplish both defining some legal boundaries as well
as communicating in a friendly and clear way, at least to the degree that suits
our needs for the current stage of our products and services.
<p>This is important because it is our intent to create sustainable open source
projects, which means that we do want to create brand value, grow community,
and, eventually, be able to work full time on creating more great software and services.
<p>If you'd like to contact me, especially if you feel that I (or we) have made
a mistake in how we operate, please do so:
<ul>
<li><a href="mailto:coolaj86@gmail.com">coolaj86@gmail.com</a></li>
<li><a href="tel:+13852360466">+1 (385) 236-0466</a></li>
<li><a href="http://coolaj86.com">https://coolaj86.com</a></li>
</ul>
<h1>Contents</h1>
<p>Here's what I've worked through so far:
<ul>
<li><a href="#greenlock">Greelock Domains</a></li>
<li><a href="#licensing">Licensing</a></li>
<li><a href="#terms">Terms of Service</a></li>
<li><a href="#trademark">Trademark</a></li>
<li><a href="#privacy">Privacy</a></li>
</ul>
<h1 id="greenlock">Greenlock Domains&trade;</h1>
<p>Greenlock Domains is a service provided by
<em><a href="https://coolaj86.com">AJ</a>, Brian,
<a href="https://jshaver.net">John</a>, &amp; Josh</em>
(collectively <a href="https://therootcompany.com">Root</a>)
for automated TLS, SSL, and HTTPS.
<ul>
<li><a href="https://greenlock.domains" target="_blank">
https://greenlock.domains</a></li>
<li><a href="https://git.coolaj86.com/coolaj86/greenlock-express.js" target="_blank">
https://git.coolaj86.com/coolaj86/greenlock-express.js</a></li>
<li><a href="https://git.coolaj86.com/coolaj86/greenlock.js" target="_blank">
https://git.coolaj86.com/coolaj86/greenlock.js</a></li>
<li><a href="https://git.coolaj86.com/coolaj86/greenlock.html" target="_blank">
https://git.coolaj86.com/coolaj86/greenlock.html</a></li>
</ul>
<p>Greenlock Domains is an important product / service combo to us
because it's a huge milestone on the path to a more decentralized web.
We believe in <em>ownership</em> and <em>control</em> and we're
building a <a href="https://therootcompany.com">Home Server</a>
because we envision a world in which everyone is empowered to make
the choice of whether to rent or own their stuff.
<p>If we don't do this, well, with the way the cloud is headed,
renting may be the only option in the future.
<p>We need <em>Root</em> because we want ownership.
<p>If at any time you feel that any of our messaging or practices
are in conflict with our mission or these values, please let us know.
<h1 id="licensing">Licensing</h1>
<p>Each of our products comes with its own LICENSE file and the license(s)
may alse be in some sort of manifest file (such as package.json).
<p>We typically use the MIT and Apache-2.0 licenses for libraries that we
actively want others to copy, modify, use and redistribute.
<p>We typically use ISC and MPL-2.0 with products for which we're a little more
concerned about branding or about which we have particularly strong opinions.
<p>Although we do keep some of our software proprietary and we do use trademarks,
because we believe in empowerment and choice we do our best to provide usable
self-service forms of our products and services for personal use.
<p>If at any time you feel that our Licensing is in conflict with our mission or values,
please let us know.
<h1 id="terms">Terms of Service</h1>
<p>We want to make the world a better place.
Everyone has a different definition of what "a better place" means,
so the purpose of our terms is to rule out some things that
we think makes the world (and particularly our world) a worse place:
<p>You agree that you will use the Greenlock&trade; service, code, libraries,
documentation, etc (provided by <a href="#greenlock">us</a>)
primarily for securing network connections for yourself, your customers,
on your and your customer's devices on internets, intranets, and... other nets.
<p>You agree that you will take reasonable measures to keep up-to-date with security
releases.
<p>You agree to not use our products or services in a way that would cause unusual
or undue burden on our servers or services, our partners servers or services, or our
customers servers or services, or in a way that harms or misrepresents the reputation
or brand value (including causing brand confusion) of the aforementioned parties
(or really anybody).
<p>This is not to say that you can't publicly have a negative opinion, but don't
bite the hand that feeds and don't be vicious or misrepresentative.
<p>If you have a use case that may be in violation of these terms (particularly
the part about undue burden), but you feel contributes to making the world a better
place, we're here to help (assuming it also aligns with our values).
Although it may not be appropriate to use our services, but perhaps we can help
you with a solution based on our no-cost, low-cost or open source products.
<p>If at any time you feel that our Terms of Service are in conflict with our
mission or values, please let us know.
<h1 id="trademark">Trademark</h1>
<p>"Greenlock" and the "green G lock" mark are Trademarks of
<a href="https://coolaj86.com" target="_blank">AJ ONeal</a>.
<p>We'll be coming out with a brand guide as to how you should use
the marks. In the meantime: don't change the proportions, colors
(excepting the case of greyscale and black and white).
<p>It is appropriate to use the trademark in a way that promotes the
brand with proper attribution, linking to the official project repositories, etc.
<p>It is appropriate use the name greenlock in a plugin for Greenlock&trade;,
as long as it is clear that it is a community contribution.
<p>If you create a "hard" fork of our code or any products or services,
you should give your fork its own name, and not use ours.
That sound, we gladly welcome your suggestiosn and pull requests.
<p>If you mirror our code you should make it clear that it is a mirror
and link to the official repository.
in association with usand the disclose that you use Greenlock
<p>If at any time you feel that our Trademark policies are in conflict with our
values, please let us know.
<h1 id="privacy">Privacy Policy</h1>
<p>What we collect and (more importantly) <em>Why</em>:
<p><strong>Name</strong>:
<p>In the cases that we collect your name, it's because we want to know how to address you.
All four of us want to be personable if and when we reach out.
<p><strong>Email</strong>:
<p>There are three main purposes for which we may use your email address:
<p>1. A one-time outreach to ask if you were able to do what you intended to do.
We want to make a great product. Although open source projects traditionally have
a <em>reactive</em> approach to communication (i.e. you file a bug and wait for a response),
we believe that creating sustainable open source requires a <em>proactive</em> approach.
<p>2. Security and legal notifications. It's important that we have a way to contact you
if we've made a mistake or discover a mistake that needs to be addressed. This
may include vulnerabilities as well as mandatory upgrades (such as when a
significant change to the Let's Encrypt API is made). Making sure that our products
work and are secure aligns with our values and contributes to our brand identity.
<p>3. Opt-in updates. Many of you want to know when we have significant feature updates
or when we have something that we believe is really valuable to share. We've created an
opt-in avenue for that. And you can always opt-out as well.
<p><strong>Telemetry</strong>:
<p>We believe that the current open source model needs improvement - it often
relies heavily on large centralized platforms which aggregate a lot of user
information for the platform without appropriately targeting the relationship
between authors and users of projcts (i.e. npm, github, etc). We believe that
making open source sustainable means a greater focus on empowering authors
and users. We've learned from other projects (Caddy, Heroku, and others) which
use telemetry as part of a proactive approach to open source and we believe that
it can be a great avenue for us to be proactive as well.
<p>We may use telemetry about operating system, browser, node version, code version,
and other system-level information to better understand how we can serve our users (you)
and proactively solve problems that we might not otherwise hear about. For example, if
we see many page visits in a certain browser (or installs with a new version of node),
but few successful registrations, we know that something is wrong.
<p><strong>Other</strong>:
<p>We also use Google Analytics on our web sites for basic functionality.
Other than that, nothing else comes to mind right now.
As we consider what we will do in the future, it will be measured against our mission and values.
We never want to come across as spammy or forceful. We want to do things that help us build
our brand, acknowledge our customers; things that are proactive, and that
promote sustainable source.
<p>If at any time you feel that our Privacy policy is in conflict with our mission or values,
please let us know.
<br>
<br>
<p>Copyright 2018 AJ ONeal

1
legal/index.html Symbolic link
View File

@ -0,0 +1 @@
../legal.html

115
styles/main.css Normal file
View File

@ -0,0 +1,115 @@
.column-row {
display: flex;
flex-direction: column;
text-align: center;
align-items: center;
}
body {
position: relative;
margin-top: 5.777777778em;
min-height: 36em;
font-size: 18px;
font-family: 'Source Sans Pro', sans-serif;
font-stretch: normal;
line-height: 1.33;
letter-spacing: -0.4px;
color: #1a1a1a;
opacity: 0;
}
h1 {
font-size: 2.666666667em;
max-width: 8em;
text-align: center;
}
input {
font-size: 1em;
padding: 0.444444444em;
border: solid #d9d9d9 1px;
border-radius: 2px;
font-family: inherit;
}
button {
padding: 0.444444444em 1.2em;
font-size: 1em;
background-color: #5bc17f;
border: solid 1px #5bc17f;
border-radius: 2px;
font-weight: normal;
font-stretch: normal;
letter-spacing: -0.4px;
font-family: inherit;
text-align: center;
color: white;
height: 40px;
line-height: 1.13;
}
.acme-advanced-fields {
position: absolute;
bottom: 0;
padding: 1em;
text-align: center;
}
.domain-subtext {
font-size: 0.833333333em;
color: #666;
text-align: center;
margin: 0.5em;
}
input#acme-domains:before {
content: "Secure | https://";
}
.domain-psuedo-input {
display: inline-block;
margin-right: .6666667em;
border: solid #d9d9d9 1px;
border-radius: 2px;
padding: 0.44444444em;
color: #d9d9d9;
}
input#acme-domains {
border: none;
padding: 0;
padding-right: 0;
width: 17.2222222em;
color: #222;
}
input#acme-domains:focus {
outline: none;
}
span.secure-green {
color: #5bc17f;
}
.why-you-need {
width: 26.555556em;
}
body.js-app-ready {
transition: opacity 0.2s;
opacity: 1;
}
.acme-advanced-fields > * {
margin: 0 0.5em;
}
.js-javascript-warning {
border: solid 1px red;
background-color: #ffc0cb40;
border-radius: 2px;
margin: 0.6em;
padding: 0.5em 1em;
width: 30em;
}