Compare commits
45 Commits
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 7484ffcd11 | |
AJ ONeal | 98c8db8973 | |
AJ ONeal | 8ce44bc414 | |
AJ ONeal | ab67741604 | |
AJ ONeal | b7dd224426 | |
John Shaver | 65974b57c1 | |
John Shaver | b1fc2bcc14 | |
AJ ONeal | 8d31bf7754 | |
AJ ONeal | 1c57a342d0 | |
AJ ONeal | f27b386ba4 | |
AJ ONeal | 55a30888ff | |
AJ ONeal | 95e807be73 | |
AJ ONeal | 268f83b49e | |
AJ ONeal | 09ff0b3adc | |
AJ ONeal | 8a7183ed9c | |
AJ ONeal | a7462db2c8 | |
AJ ONeal | 2cc5a41268 | |
AJ ONeal | d63d8e1aed | |
John Shaver | 6f188cefb8 | |
AJ ONeal | bddf85dfe6 | |
AJ ONeal | c6052bcf73 | |
John Shaver | 78515f165c | |
John Shaver | 68253cbe54 | |
John Shaver | 9143545389 | |
John Shaver | 3e865a2fb7 | |
John Shaver | 6ce81beaec | |
John Shaver | c6c06b06f0 | |
John Shaver | a32287c3f8 | |
John Shaver | 6a05569ab5 | |
John Shaver | c3c9696799 | |
John Shaver | b86074920a | |
John Shaver | 8610baf0e0 | |
John Shaver | 3cfbc9339b | |
John Shaver | 62f3d28a71 | |
John Shaver | 430b589038 | |
John Shaver | 6d3c3a9e61 | |
AJ ONeal | 61b2c76822 | |
AJ ONeal | d0ea6822ea | |
AJ ONeal | 4bfef46295 | |
AJ ONeal | 42a589f5c0 | |
AJ ONeal | 5d07cec7a3 | |
AJ ONeal | ceb109e652 | |
AJ ONeal | ed2739cb20 | |
AJ ONeal | 2be09788ce | |
AJ ONeal | 3854e6b430 |
|
@ -1,2 +1,2 @@
|
||||||
js/pkijs.org
|
app/js/pkijs.org
|
||||||
js/browser-csr
|
app/js/browser-csr
|
||||||
|
|
|
@ -6,7 +6,7 @@ Taking greenlock™ (Let's Encrypt v2 / ACME client) where it's never been b
|
||||||
Official Site
|
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.
|
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.
|
If it doesn't, please open an issue to let us know why.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
../img/favicon-32x32.png
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
@ -0,0 +1,366 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Greenlock™</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  <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt™ 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  <a class="js-gl-tos" target="_blank" href="/legal/#terms">Greenlock™ 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>
|
|
@ -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;
|
||||||
|
}());
|
|
@ -1,9 +1,12 @@
|
||||||
|
/*global CSR*/
|
||||||
|
// CSR takes a while to load after the page load
|
||||||
(function (exports) {
|
(function (exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var BACME = exports.BACME = {};
|
var BACME = exports.BACME = {};
|
||||||
var webFetch = exports.fetch;
|
var webFetch = exports.fetch;
|
||||||
var webCrypto = exports.crypto;
|
var webCrypto = exports.crypto;
|
||||||
|
var Promise = exports.Promise;
|
||||||
|
|
||||||
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||||
var directory;
|
var directory;
|
||||||
|
@ -15,7 +18,6 @@ var accountKeypair;
|
||||||
var accountJwk;
|
var accountJwk;
|
||||||
|
|
||||||
var accountUrl;
|
var accountUrl;
|
||||||
var signedAccount;
|
|
||||||
|
|
||||||
BACME.challengePrefixes = {
|
BACME.challengePrefixes = {
|
||||||
'http-01': '/.well-known/acme-challenge'
|
'http-01': '/.well-known/acme-challenge'
|
||||||
|
@ -23,38 +25,41 @@ BACME.challengePrefixes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME._logHeaders = function (resp) {
|
BACME._logHeaders = function (resp) {
|
||||||
console.log('Headers:');
|
console.log('Headers:');
|
||||||
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
|
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME._logBody = function (body) {
|
BACME._logBody = function (body) {
|
||||||
console.log('Body:');
|
console.log('Body:');
|
||||||
console.log(JSON.stringify(body, null, 2));
|
console.log(JSON.stringify(body, null, 2));
|
||||||
console.log('');
|
console.log('');
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME.directory = function (opts) {
|
BACME.directory = function (opts) {
|
||||||
return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) {
|
return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
return resp.json().then(function (body) {
|
return resp.json().then(function (reply) {
|
||||||
directory = body;
|
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';
|
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';
|
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";
|
orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order";
|
||||||
BACME._logBody(body);
|
BACME._logBody(reply);
|
||||||
return body;
|
return reply;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME.nonce = function () {
|
BACME.nonce = function () {
|
||||||
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) {
|
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
nonce = resp.headers.get('replay-nonce');
|
nonce = resp.headers.get('replay-nonce');
|
||||||
console.log('Nonce:', nonce);
|
console.log('Nonce:', nonce);
|
||||||
// resp.body is empty
|
// resp.body is empty
|
||||||
return resp.headers.get('replay-nonce');
|
return resp.headers.get('replay-nonce');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME.accounts = {};
|
BACME.accounts = {};
|
||||||
|
@ -62,66 +67,38 @@ BACME.accounts = {};
|
||||||
// type = ECDSA
|
// type = ECDSA
|
||||||
// bitlength = 256
|
// bitlength = 256
|
||||||
BACME.accounts.generateKeypair = function (opts) {
|
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
|
return webCrypto.subtle.exportKey(
|
||||||
if (/^EC/i.test(opts.type)) {
|
"jwk"
|
||||||
wcOpts.name = 'ECDSA';
|
, result.privateKey
|
||||||
if (/256/.test(opts.bitlength)) {
|
).then(function (privJwk) {
|
||||||
wcOpts.namedCurve = 'P-256';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSA-PSS is another option, but I don't think it's used for Let's Encrypt
|
accountJwk = privJwk;
|
||||||
// I think the hash is only necessary for signing, not generation or import
|
console.log('private jwk:');
|
||||||
if (/^RS/i.test(opts.type)) {
|
console.log(JSON.stringify(privJwk, null, 2));
|
||||||
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));
|
|
||||||
|
|
||||||
return privJwk;
|
return privJwk;
|
||||||
/*
|
/*
|
||||||
return webCrypto.subtle.exportKey(
|
return webCrypto.subtle.exportKey(
|
||||||
"pkcs8"
|
"pkcs8"
|
||||||
, result.privateKey
|
, result.privateKey
|
||||||
).then(function (keydata) {
|
).then(function (keydata) {
|
||||||
console.log('pkcs8:');
|
console.log('pkcs8:');
|
||||||
console.log(Array.from(new Uint8Array(keydata)));
|
console.log(Array.from(new Uint8Array(keydata)));
|
||||||
|
|
||||||
return privJwk;
|
return privJwk;
|
||||||
//return accountKeypair;
|
//return accountKeypair;
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// json to url-safe base64
|
// json to url-safe base64
|
||||||
BACME._jsto64 = function (json) {
|
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();
|
var textEncoder = new TextEncoder();
|
||||||
|
@ -158,7 +135,7 @@ BACME._importKey = function (jwk) {
|
||||||
e: priv.e
|
e: priv.e
|
||||||
, kty: priv.kty
|
, kty: priv.kty
|
||||||
, n: priv.n
|
, n: priv.n
|
||||||
}
|
};
|
||||||
if (!priv.p) {
|
if (!priv.p) {
|
||||||
priv = null;
|
priv = null;
|
||||||
}
|
}
|
||||||
|
@ -167,7 +144,7 @@ BACME._importKey = function (jwk) {
|
||||||
return window.crypto.subtle.importKey(
|
return window.crypto.subtle.importKey(
|
||||||
"jwk"
|
"jwk"
|
||||||
, pub
|
, pub
|
||||||
, wcOpts
|
, wcOpts
|
||||||
, extractable
|
, extractable
|
||||||
, [ "verify" ]
|
, [ "verify" ]
|
||||||
).then(function (publicKey) {
|
).then(function (publicKey) {
|
||||||
|
@ -253,26 +230,38 @@ BACME.accounts.sign = function (opts) {
|
||||||
payloadJson
|
payloadJson
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO RSA
|
|
||||||
var protectedJson =
|
var protectedJson =
|
||||||
{ nonce: opts.nonce
|
{ nonce: opts.nonce
|
||||||
, url: accountUrl
|
, url: accountUrl
|
||||||
, alg: abstractKey.meta.alg
|
, alg: abstractKey.meta.alg
|
||||||
, jwk: {
|
, jwk: null
|
||||||
kty: opts.jwk.kty
|
|
||||||
, crv: opts.jwk.crv
|
|
||||||
, x: opts.jwk.x
|
|
||||||
, y: opts.jwk.y
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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('protected:');
|
||||||
console.log(protectedJson);
|
console.log(protectedJson);
|
||||||
var protected64 = BACME._jsto64(
|
var protected64 = BACME._jsto64(
|
||||||
protectedJson
|
protectedJson
|
||||||
);
|
);
|
||||||
|
|
||||||
// Note: this function hashes before signing so send data, not the hash
|
// Note: this function hashes before signing so send data, not the hash
|
||||||
return BACME._sign({
|
return BACME._sign({
|
||||||
abstractKey: abstractKey
|
abstractKey: abstractKey
|
||||||
, payload64: payload64
|
, payload64: payload64
|
||||||
, protected64: protected64
|
, protected64: protected64
|
||||||
|
@ -280,30 +269,29 @@ BACME.accounts.sign = function (opts) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var account;
|
|
||||||
var accountId;
|
var accountId;
|
||||||
|
|
||||||
BACME.accounts.set = function (opts) {
|
BACME.accounts.set = function (opts) {
|
||||||
nonce = null;
|
nonce = null;
|
||||||
return window.fetch(accountUrl, {
|
return window.fetch(accountUrl, {
|
||||||
mode: 'cors'
|
mode: 'cors'
|
||||||
, method: 'POST'
|
, method: 'POST'
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
, headers: { 'Content-Type': 'application/jose+json' }
|
||||||
, body: JSON.stringify(opts.signedAccount)
|
, body: JSON.stringify(opts.signedAccount)
|
||||||
}).then(function (resp) {
|
}).then(function (resp) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
nonce = resp.headers.get('replay-nonce');
|
nonce = resp.headers.get('replay-nonce');
|
||||||
accountId = resp.headers.get('location');
|
accountId = resp.headers.get('location');
|
||||||
console.log('Next nonce:', nonce);
|
console.log('Next nonce:', nonce);
|
||||||
console.log('Location/kid:', accountId);
|
console.log('Location/kid:', accountId);
|
||||||
|
|
||||||
if (!resp.headers.get('content-type')) {
|
if (!resp.headers.get('content-type')) {
|
||||||
console.log('Body: <none>');
|
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)) {
|
if (/^Error/i.test(result.detail)) {
|
||||||
return Promise.reject(new Error(result.detail));
|
return Promise.reject(new Error(result.detail));
|
||||||
}
|
}
|
||||||
|
@ -311,21 +299,20 @@ BACME.accounts.set = function (opts) {
|
||||||
BACME._logBody(result);
|
BACME._logBody(result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var orderUrl;
|
var orderUrl;
|
||||||
var signedOrder;
|
|
||||||
|
|
||||||
BACME.orders = {};
|
BACME.orders = {};
|
||||||
|
|
||||||
// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
|
// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
|
||||||
// signedAccount
|
// signedAccount
|
||||||
BACME.orders.sign = function (opts) {
|
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(
|
var protected64 = BACME._jsto64(
|
||||||
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid }
|
{ 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 currentOrderUrl;
|
||||||
var authorizationUrls;
|
var authorizationUrls;
|
||||||
var finalizeUrl;
|
var finalizeUrl;
|
||||||
|
|
||||||
BACME.orders.create = function (opts) {
|
BACME.orders.create = function (opts) {
|
||||||
nonce = null;
|
nonce = null;
|
||||||
return window.fetch(orderUrl, {
|
return window.fetch(orderUrl, {
|
||||||
mode: 'cors'
|
mode: 'cors'
|
||||||
, method: 'POST'
|
, method: 'POST'
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
, headers: { 'Content-Type': 'application/jose+json' }
|
||||||
, body: JSON.stringify(opts.signedOrder)
|
, body: JSON.stringify(opts.signedOrder)
|
||||||
}).then(function (resp) {
|
}).then(function (resp) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
currentOrderUrl = resp.headers.get('location');
|
currentOrderUrl = resp.headers.get('location');
|
||||||
nonce = resp.headers.get('replay-nonce');
|
nonce = resp.headers.get('replay-nonce');
|
||||||
console.log('Next nonce:', nonce);
|
console.log('Next nonce:', nonce);
|
||||||
|
|
||||||
return resp.json().then(function (result) {
|
return resp.json().then(function (result) {
|
||||||
if (/^Error/i.test(result.detail)) {
|
if (/^Error/i.test(result.detail)) {
|
||||||
return Promise.reject(new Error(result.detail));
|
return Promise.reject(new Error(result.detail));
|
||||||
}
|
}
|
||||||
authorizationUrls = result.authorizations;
|
authorizationUrls = result.authorizations;
|
||||||
finalizeUrl = result.finalize;
|
finalizeUrl = result.finalize;
|
||||||
BACME._logBody(result);
|
BACME._logBody(result);
|
||||||
|
|
||||||
result.url = currentOrderUrl;
|
result.url = currentOrderUrl;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME.challenges = {};
|
BACME.challenges = {};
|
||||||
|
@ -395,22 +381,22 @@ BACME.challenges.all = function () {
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
BACME.challenges.view = function () {
|
BACME.challenges.view = function () {
|
||||||
var authzUrl = authorizationUrls.pop();
|
var authzUrl = authorizationUrls.pop();
|
||||||
var token;
|
var token;
|
||||||
var challengeDomain;
|
var challengeDomain;
|
||||||
var challengeUrl;
|
var challengeUrl;
|
||||||
|
|
||||||
return window.fetch(authzUrl, {
|
return window.fetch(authzUrl, {
|
||||||
mode: 'cors'
|
mode: 'cors'
|
||||||
}).then(function (resp) {
|
}).then(function (resp) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
|
|
||||||
return resp.json().then(function (result) {
|
return resp.json().then(function (result) {
|
||||||
// Note: select the challenge you wish to use
|
// Note: select the challenge you wish to use
|
||||||
var challenge = result.challenges.slice(0).pop();
|
var challenge = result.challenges.slice(0).pop();
|
||||||
token = challenge.token;
|
token = challenge.token;
|
||||||
challengeUrl = challenge.url;
|
challengeUrl = challenge.url;
|
||||||
challengeDomain = result.identifier.value;
|
challengeDomain = result.identifier.value;
|
||||||
|
|
||||||
BACME._logBody(result);
|
BACME._logBody(result);
|
||||||
|
|
||||||
|
@ -424,8 +410,8 @@ BACME.challenges.view = function () {
|
||||||
//, url: challenge.url
|
//, url: challenge.url
|
||||||
//, domain: result.identifier.value,
|
//, domain: result.identifier.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var thumbprint;
|
var thumbprint;
|
||||||
|
@ -435,7 +421,7 @@ var dnsAuth;
|
||||||
var dnsRecord;
|
var dnsRecord;
|
||||||
|
|
||||||
BACME.thumbprint = function (opts) {
|
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 accountJwk = opts.jwk;
|
||||||
var keys;
|
var keys;
|
||||||
|
@ -446,34 +432,34 @@ BACME.thumbprint = function (opts) {
|
||||||
keys = [ 'e', 'kty', 'n' ];
|
keys = [ 'e', 'kty', 'n' ];
|
||||||
}
|
}
|
||||||
|
|
||||||
var accountPublicStr = '{' + keys.map(function (key) {
|
var accountPublicStr = '{' + keys.map(function (key) {
|
||||||
return '"' + key + '":"' + accountJwk[key] + '"';
|
return '"' + key + '":"' + accountJwk[key] + '"';
|
||||||
}).join(',') + '}';
|
}).join(',') + '}';
|
||||||
|
|
||||||
return window.crypto.subtle.digest(
|
return window.crypto.subtle.digest(
|
||||||
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
|
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
|
||||||
, textEncoder.encode(accountPublicStr)
|
, textEncoder.encode(accountPublicStr)
|
||||||
).then(function (hash) {
|
).then(function (hash) {
|
||||||
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
||||||
return String.fromCharCode(ch);
|
return String.fromCharCode(ch);
|
||||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||||
|
|
||||||
console.log('Thumbprint:');
|
console.log('Thumbprint:');
|
||||||
console.log(opts);
|
console.log(opts);
|
||||||
console.log(accountPublicStr);
|
console.log(accountPublicStr);
|
||||||
console.log(thumbprint);
|
console.log(thumbprint);
|
||||||
|
|
||||||
return thumbprint;
|
return thumbprint;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// { token, thumbprint, challengeDomain }
|
// { token, thumbprint, challengeDomain }
|
||||||
BACME.challenges['http-01'] = function (opts) {
|
BACME.challenges['http-01'] = function (opts) {
|
||||||
// The contents of the key authorization file
|
// The contents of the key authorization file
|
||||||
keyAuth = opts.token + '.' + opts.thumbprint;
|
keyAuth = opts.token + '.' + opts.thumbprint;
|
||||||
|
|
||||||
// Where the key authorization file goes
|
// Where the key authorization file goes
|
||||||
httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token;
|
httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token;
|
||||||
|
|
||||||
console.log("echo '" + keyAuth + "' > '" + httpPath + "'");
|
console.log("echo '" + keyAuth + "' > '" + httpPath + "'");
|
||||||
|
|
||||||
|
@ -487,113 +473,138 @@ BACME.challenges['http-01'] = function (opts) {
|
||||||
BACME.challenges['dns-01'] = function (opts) {
|
BACME.challenges['dns-01'] = function (opts) {
|
||||||
console.log('opts.keyAuth for DNS:');
|
console.log('opts.keyAuth for DNS:');
|
||||||
console.log(opts.keyAuth);
|
console.log(opts.keyAuth);
|
||||||
return window.crypto.subtle.digest(
|
return window.crypto.subtle.digest(
|
||||||
{ name: "SHA-256", }
|
{ name: "SHA-256", }
|
||||||
, textEncoder.encode(opts.keyAuth)
|
, textEncoder.encode(opts.keyAuth)
|
||||||
).then(function (hash) {
|
).then(function (hash) {
|
||||||
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
||||||
return String.fromCharCode(ch);
|
return String.fromCharCode(ch);
|
||||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||||
|
|
||||||
dnsRecord = '_acme-challenge.' + opts.challengeDomain;
|
dnsRecord = '_acme-challenge.' + opts.challengeDomain;
|
||||||
|
|
||||||
console.log('DNS TXT Auth:');
|
console.log('DNS TXT Auth:');
|
||||||
// The name of the record
|
// The name of the record
|
||||||
console.log(dnsRecord);
|
console.log(dnsRecord);
|
||||||
// The TXT record value
|
// The TXT record value
|
||||||
console.log(dnsAuth);
|
console.log(dnsAuth);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'TXT'
|
type: 'TXT'
|
||||||
, host: dnsRecord
|
, host: dnsRecord
|
||||||
, answer: dnsAuth
|
, answer: dnsAuth
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var challengePollUrl;
|
var challengePollUrl;
|
||||||
|
|
||||||
// { jwk, challengeUrl, accountId (kid) }
|
// { jwk, challengeUrl, accountId (kid) }
|
||||||
BACME.challenges.accept = function (opts) {
|
BACME.challenges.accept = function (opts) {
|
||||||
var payload64 = BACME._jsto64(
|
var payload64 = BACME._jsto64({});
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
||||||
var protected64 = BACME._jsto64(
|
var protected64 = BACME._jsto64(
|
||||||
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId }
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId }
|
||||||
);
|
);
|
||||||
return BACME._sign({
|
return BACME._sign({
|
||||||
abstractKey: abstractKey
|
abstractKey: abstractKey
|
||||||
, payload64: payload64
|
, payload64: payload64
|
||||||
, protected64: protected64
|
, protected64: protected64
|
||||||
});
|
});
|
||||||
}).then(function (signedAccept) {
|
}).then(function (signedAccept) {
|
||||||
|
|
||||||
nonce = null;
|
nonce = null;
|
||||||
return window.fetch(
|
return window.fetch(
|
||||||
opts.challengeUrl
|
opts.challengeUrl
|
||||||
, { mode: 'cors'
|
, { mode: 'cors'
|
||||||
, method: 'POST'
|
, method: 'POST'
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
, headers: { 'Content-Type': 'application/jose+json' }
|
||||||
, body: JSON.stringify(signedAccept)
|
, body: JSON.stringify(signedAccept)
|
||||||
}
|
}
|
||||||
).then(function (resp) {
|
).then(function (resp) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
nonce = resp.headers.get('replay-nonce');
|
nonce = resp.headers.get('replay-nonce');
|
||||||
console.log("ACCEPT NONCE:", nonce);
|
console.log("ACCEPT NONCE:", nonce);
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
return resp.json().then(function (reply) {
|
||||||
challengePollUrl = reply.url;
|
challengePollUrl = reply.url;
|
||||||
|
|
||||||
console.log('Challenge ACK:');
|
console.log('Challenge ACK:');
|
||||||
console.log(JSON.stringify(reply));
|
console.log(JSON.stringify(reply));
|
||||||
return reply;
|
return reply;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME.challenges.check = function (opts) {
|
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);
|
BACME._logHeaders(resp);
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
return resp.json().then(function (reply) {
|
||||||
challengePollUrl = reply.url;
|
if (/error/.test(reply.type)) {
|
||||||
|
return Promise.reject(new Error(reply.detail || reply.type));
|
||||||
|
}
|
||||||
|
challengePollUrl = reply.url;
|
||||||
|
|
||||||
BACME._logBody(reply);
|
BACME._logBody(reply);
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var domainKeypair;
|
var domainKeypair;
|
||||||
var domainJwk;
|
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 = {};
|
BACME.domains = {};
|
||||||
// TODO factor out from BACME.accounts.generateKeypair
|
// TODO factor out from BACME.accounts.generateKeypair even more
|
||||||
BACME.domains.generateKeypair = function () {
|
BACME.domains.generateKeypair = function (opts) {
|
||||||
var extractable = true;
|
return BACME.generateKeypair(opts).then(function (result) {
|
||||||
return window.crypto.subtle.generateKey(
|
domainKeypair = result;
|
||||||
{ name: "ECDSA", namedCurve: "P-256" }
|
|
||||||
, extractable
|
|
||||||
, [ 'sign', 'verify' ]
|
|
||||||
).then(function (result) {
|
|
||||||
domainKeypair = result;
|
|
||||||
|
|
||||||
return window.crypto.subtle.exportKey(
|
return window.crypto.subtle.exportKey(
|
||||||
"jwk"
|
"jwk"
|
||||||
, result.privateKey
|
, result.privateKey
|
||||||
).then(function (jwk) {
|
).then(function (privJwk) {
|
||||||
|
|
||||||
domainJwk = jwk;
|
domainJwk = privJwk;
|
||||||
console.log('private jwk:');
|
console.log('private jwk:');
|
||||||
console.log(JSON.stringify(jwk, null, 2));
|
console.log(JSON.stringify(privJwk, null, 2));
|
||||||
|
|
||||||
return domainKeypair;
|
return privJwk;
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// { serverJwk, domains }
|
// { serverJwk, domains }
|
||||||
|
@ -607,41 +618,44 @@ var certificateUrl;
|
||||||
|
|
||||||
// { csr, jwk, finalizeUrl, accountId }
|
// { csr, jwk, finalizeUrl, accountId }
|
||||||
BACME.orders.finalize = function (opts) {
|
BACME.orders.finalize = function (opts) {
|
||||||
var payload64 = BACME._jsto64(
|
var payload64 = BACME._jsto64(
|
||||||
{ csr: opts.csr }
|
{ csr: opts.csr }
|
||||||
);
|
);
|
||||||
|
|
||||||
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
return BACME._importKey(opts.jwk).then(function (abstractKey) {
|
||||||
var protected64 = BACME._jsto64(
|
var protected64 = BACME._jsto64(
|
||||||
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId }
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId }
|
||||||
);
|
);
|
||||||
return BACME._sign({
|
return BACME._sign({
|
||||||
abstractKey: abstractKey
|
abstractKey: abstractKey
|
||||||
, payload64: payload64
|
, payload64: payload64
|
||||||
, protected64: protected64
|
, protected64: protected64
|
||||||
});
|
});
|
||||||
}).then(function (signedFinal) {
|
}).then(function (signedFinal) {
|
||||||
|
|
||||||
nonce = null;
|
nonce = null;
|
||||||
return window.fetch(
|
return window.fetch(
|
||||||
opts.finalizeUrl
|
opts.finalizeUrl
|
||||||
, { mode: 'cors'
|
, { mode: 'cors'
|
||||||
, method: 'POST'
|
, method: 'POST'
|
||||||
, headers: { 'Content-Type': 'application/jose+json' }
|
, headers: { 'Content-Type': 'application/jose+json' }
|
||||||
, body: JSON.stringify(signedFinal)
|
, body: JSON.stringify(signedFinal)
|
||||||
}
|
}
|
||||||
).then(function (resp) {
|
).then(function (resp) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
nonce = resp.headers.get('replay-nonce');
|
nonce = resp.headers.get('replay-nonce');
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
return resp.json().then(function (reply) {
|
||||||
certificateUrl = reply.certificate;
|
if (/error/.test(reply.type)) {
|
||||||
|
return Promise.reject(new Error(reply.detail || reply.type));
|
||||||
|
}
|
||||||
|
certificateUrl = reply.certificate;
|
||||||
BACME._logBody(reply);
|
BACME._logBody(reply);
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BACME.orders.receive = function (opts) {
|
BACME.orders.receive = function (opts) {
|
||||||
|
@ -672,6 +686,9 @@ BACME.orders.check = function (opts) {
|
||||||
BACME._logHeaders(resp);
|
BACME._logHeaders(resp);
|
||||||
|
|
||||||
return resp.json().then(function (reply) {
|
return resp.json().then(function (reply) {
|
||||||
|
if (/error/.test(reply.type)) {
|
||||||
|
return Promise.reject(new Error(reply.detail || reply.type));
|
||||||
|
}
|
||||||
BACME._logBody(reply);
|
BACME._logBody(reply);
|
||||||
|
|
||||||
return reply;
|
return reply;
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}());
|
|
@ -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.
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
289
index.html
289
index.html
|
@ -1,227 +1,100 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Greenlock™</title>
|
<title>Greenlock™</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>
|
</head>
|
||||||
<body hidden>
|
<body class="js-app-ready">
|
||||||
<img width="410px" src="img/greenlock-820x150.png">
|
<script>
|
||||||
<div>
|
document.querySelector('body').classList.remove("js-app-ready");
|
||||||
<br>
|
</script>
|
||||||
<h3>Greenlock™ - Instant, Free SSL Certificates via Let's Encrypt v2</h3>
|
<div class="column-container wide">
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 1 Choose Domain(s) -->
|
<div class="column-row">
|
||||||
<form class="js-acme-form js-acme-form-domains">
|
<img src="img/greenlock-146.png">
|
||||||
<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™ 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™ 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 & 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>
|
</div>
|
||||||
|
<div class="column-row">
|
||||||
<div class="js-acme-wildcard">
|
<h1>Get the green lock for your website</h1>
|
||||||
<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>
|
</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>
|
<div class="acme-advanced-fields">
|
||||||
</form>
|
<label><input name="acme-api-type" type="radio" value="v02" checked required>
|
||||||
|
Production
|
||||||
<!-- Step 4 Process Challanges -->
|
</label>
|
||||||
<form class="js-acme-form js-acme-form-poll">
|
<label><input name="acme-api-type" type="radio" value="staging-v02" required>
|
||||||
Verifying Domains... (give us 5 seconds or so...)
|
Testing</label>
|
||||||
|
<input id="js-acme-api-url" type="url" placeholder="ACME directory url">
|
||||||
<!--
|
<div>
|
||||||
<table class="js-acme-table-verifying">
|
A <a href="https://rootprojects.org/" target="_blank">Root</a> Project
|
||||||
<thead>
|
| <a href="https://git.coolaj86.com/coolaj86/greenlock.html" target="_blank">View Source</a> (git)
|
||||||
<tr>
|
| <a href="https://rootprojects.org/legal/#terms" target="_blank">Terms of Service</a>
|
||||||
<th>Hostname</th>
|
| <a href="https://rootprojects.org/legal/#privacy" target="_blank">Privacy Policy</a>
|
||||||
<th>Type</th>
|
</div>
|
||||||
<th>Pass</th>
|
</div>
|
||||||
</tr>
|
</form>
|
||||||
</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>
|
</div>
|
||||||
|
<div class="column-row">
|
||||||
<div>
|
<div class="why-you-need">
|
||||||
<h2><label>fullchain.pem</label></h2>
|
<h2>Why you need HTTPS</h2>
|
||||||
<textarea cols="80" rows="60" class="js-fullchain">-</textarea>
|
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>
|
||||||
|
|
||||||
<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
|
<!-- or
|
||||||
<pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre>
|
<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):
|
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>
|
<pre><code>wget https://greenlock.domains --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
|
||||||
</small></div>
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<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>
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
<script src="./js/pkijs.org/v1.3.33/asn1.js"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>
|
||||||
<script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script>
|
<script>
|
||||||
<script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script>
|
window.dataLayer = window.dataLayer || [];
|
||||||
<script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script>
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'UA-118745161-2');
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
mkdir -p js/pkijs.org/v1.3.33/
|
mkdir -p app/js/pkijs.org/v1.3.33/
|
||||||
pushd 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/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_schema.js
|
||||||
wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.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
|
wget -c https://raw.githubusercontent.com/PeculiarVentures/ASN1.js/f7181c21c61e53a940ea24373ab489ad86d51bc1/org/pkijs/asn1.js
|
||||||
popd
|
popd
|
||||||
|
|
||||||
mkdir -p js/browser-csr/v1.0.0-alpha/
|
mkdir -p app/js/browser-csr/v1.0.0-alpha/
|
||||||
pushd 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
|
wget -c https://git.coolaj86.com/coolaj86/browser-csr.js/raw/commit/01cdc0e91b5bf03f12e1b25b4129e3cde927987c/csr.js
|
||||||
popd
|
popd
|
||||||
|
|
493
js/app.js
493
js/app.js
|
@ -3,487 +3,28 @@
|
||||||
|
|
||||||
var $qs = function (s) { return window.document.querySelector(s); };
|
var $qs = function (s) { return window.document.querySelector(s); };
|
||||||
var $qsa = function (s) { return window.document.querySelectorAll(s); };
|
var $qsa = function (s) { return window.document.querySelectorAll(s); };
|
||||||
var info = {};
|
|
||||||
var steps = {};
|
$qs('.js-javascript-warning').hidden = true;
|
||||||
var nonce;
|
|
||||||
var kid;
|
|
||||||
var i = 1;
|
|
||||||
|
|
||||||
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
||||||
function updateApiType() {
|
function updateApiType() {
|
||||||
var input = this || Array.prototype.filter.call(
|
var formData = new FormData($qs("#js-acme-form"));
|
||||||
$qsa('.js-acme-api-type'), function ($el) { return $el.checked; }
|
|
||||||
)[0];
|
console.log('ACME api type radio:');
|
||||||
console.log('ACME api type radio:', input.value);
|
|
||||||
$qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value);
|
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) {
|
$qs('#js-acme-form').addEventListener('change', updateApiType);
|
||||||
$el.addEventListener('change', updateApiType);
|
|
||||||
});
|
|
||||||
updateApiType();
|
updateApiType();
|
||||||
|
try {
|
||||||
function hideForms() {
|
document.fonts.load().then(function() {
|
||||||
$qsa('.js-acme-form').forEach(function (el) {
|
$qs('body').classList.add("js-app-ready");
|
||||||
el.hidden = true;
|
}).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;
|
|
||||||
}());
|
}());
|
||||||
|
|
|
@ -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™</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>, & 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™ 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™,
|
||||||
|
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
|
|
@ -0,0 +1 @@
|
||||||
|
../legal.html
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue