Some styling updates and created a separate front page.
This commit is contained in:
parent
5db9c2010f
commit
7af2671993
|
@ -1,2 +1,2 @@
|
||||||
js/pkijs.org
|
app/js/pkijs.org
|
||||||
js/browser-csr
|
app/js/browser-csr
|
||||||
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Greenlock™</title>
|
||||||
|
<meta property="og:image" content="https://greenlock.ppl.family/img/greenlock-mark-400x400.png" />
|
||||||
|
<link href="style/main.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body hidden>
|
||||||
|
<div class="column-container wide">
|
||||||
|
<div class="column-row">
|
||||||
|
<img src="img/greenlock-146.png">
|
||||||
|
</div>
|
||||||
|
<div class="column-row">
|
||||||
|
<h1>Get the green lock for your website</h1>
|
||||||
|
|
||||||
|
<!-- Step 1 Choose Domain(s) -->
|
||||||
|
<form class="js-acme-form js-acme-form-domains">
|
||||||
|
<h1><label>What's your domain?</label></h1>
|
||||||
|
<h4>Certificates are valid for 90 days. Renewal is free :)</h4>
|
||||||
|
<input class="js-acme-domains" type="text" placeholder="example.com,*.example.com" required>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Next</button>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="v02" checked required>
|
||||||
|
Production</label>
|
||||||
|
<label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="staging-v02" required>
|
||||||
|
Testing</label>
|
||||||
|
<br>
|
||||||
|
<input class="js-acme-directory-url" type="url" placeholder="ACME directory url">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Step 2 Create Account -->
|
||||||
|
<form class="js-acme-form js-acme-form-account">
|
||||||
|
<h1><label>What's your email?</label></h1>
|
||||||
|
<input class="js-acme-account-email" type="email" placeholder="john@doe.family" required>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<label><input class="js-acme-account-tos" type="checkbox" required>
|
||||||
|
Agree to <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt™ 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 class="js-acme-wildcard">
|
||||||
|
<h2>Verify Wildcard Domains</h2>
|
||||||
|
|
||||||
|
<table class="js-acme-table-wildcard">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>TXT Host</th>
|
||||||
|
<th>TXT Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>example.com</td>
|
||||||
|
<td>_acme-challenge.example.com</td>
|
||||||
|
<td>4A54</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Next</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Step 4 Process Challanges -->
|
||||||
|
<form class="js-acme-form js-acme-form-poll">
|
||||||
|
Verifying Domains... (give us 5 seconds or so...)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<table class="js-acme-table-verifying">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Pass</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>example.com</td>
|
||||||
|
<td>http-01</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a href="#">advanced (use existing keypair for domain)</a>
|
||||||
|
|
||||||
|
<button type="submit">Next</button>
|
||||||
|
-->
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Step 5 Get Certs -->
|
||||||
|
<form class="js-acme-form js-acme-form-download">
|
||||||
|
<div>
|
||||||
|
<h2><label>privkey.pem</label></h2>
|
||||||
|
<textarea cols="80" rows="10" class="js-privkey">-</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2><label>fullchain.pem</label></h2>
|
||||||
|
<textarea cols="80" rows="60" class="js-fullchain">-</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>node.js https server example</h3>
|
||||||
|
<pre><code>'use strict';
|
||||||
|
|
||||||
|
var https = require('https');
|
||||||
|
var server = https.createServer({
|
||||||
|
key: require('fs').readFileSync('./privkey.pem')
|
||||||
|
, cert: require('fs').readFileSync('./fullchain.pem')
|
||||||
|
}, function (req, res) {
|
||||||
|
res.end("Hello, World!");
|
||||||
|
}).listen(443, function () {
|
||||||
|
console.log('Listening on', this.address());
|
||||||
|
})
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO
|
||||||
|
<label>cert.pem</label>
|
||||||
|
<textarea class="js-cert">-</textarea>
|
||||||
|
|
||||||
|
<label>chain.pem</label>
|
||||||
|
<textarea class="js-chain">-</textarea>
|
||||||
|
|
||||||
|
<button type="button">Download SSL Certificates</button>
|
||||||
|
<br>
|
||||||
|
<a href="#">Advanced (copy and paste)</a>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Start Over</button>
|
||||||
|
-->
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div><small>
|
||||||
|
<h3></h3>
|
||||||
|
<a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git)
|
||||||
|
<!-- or
|
||||||
|
<pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre>
|
||||||
|
Or view the live site code (same as live-site branch):
|
||||||
|
<pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
|
||||||
|
-->
|
||||||
|
</small></div>
|
||||||
|
|
||||||
|
<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,514 @@
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var $qs = function (s) { return window.document.querySelector(s); };
|
||||||
|
var $qsa = function (s) { return window.document.querySelectorAll(s); };
|
||||||
|
var info = {};
|
||||||
|
var steps = {};
|
||||||
|
var nonce;
|
||||||
|
var kid;
|
||||||
|
var i = 1;
|
||||||
|
|
||||||
|
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
||||||
|
function updateApiType() {
|
||||||
|
var input = this || Array.prototype.filter.call(
|
||||||
|
$qsa('.js-acme-api-type'), function ($el) { return $el.checked; }
|
||||||
|
)[0];
|
||||||
|
console.log('ACME api type radio:', input.value);
|
||||||
|
$qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value);
|
||||||
|
}
|
||||||
|
$qsa('.js-acme-api-type').forEach(function ($el) {
|
||||||
|
$el.addEventListener('change', updateApiType);
|
||||||
|
});
|
||||||
|
updateApiType();
|
||||||
|
|
||||||
|
function hideForms() {
|
||||||
|
$qsa('.js-acme-form').forEach(function (el) {
|
||||||
|
el.hidden = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitForm(ev) {
|
||||||
|
var j = i;
|
||||||
|
i += 1;
|
||||||
|
steps[j].submit(ev);
|
||||||
|
}
|
||||||
|
$qsa('.js-acme-form').forEach(function ($el) {
|
||||||
|
$el.addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
submitForm(ev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function updateChallengeType() {
|
||||||
|
var input = this || Array.prototype.filter.call(
|
||||||
|
$qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; }
|
||||||
|
)[0];
|
||||||
|
console.log('ch type radio:', input.value);
|
||||||
|
$qs('.js-acme-table-wildcard').hidden = true;
|
||||||
|
$qs('.js-acme-table-http-01').hidden = true;
|
||||||
|
$qs('.js-acme-table-dns-01').hidden = true;
|
||||||
|
if (info.challenges.wildcard) {
|
||||||
|
$qs('.js-acme-table-wildcard').hidden = false;
|
||||||
|
}
|
||||||
|
if (info.challenges[input.value]) {
|
||||||
|
$qs('.js-acme-table-' + input.value).hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$qsa('.js-acme-challenge-type').forEach(function ($el) {
|
||||||
|
$el.addEventListener('change', updateChallengeType);
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveContact(email, domains) {
|
||||||
|
// to be used for good, not evil
|
||||||
|
return window.fetch('https://api.ppl.family/api/ppl.family/public/list', {
|
||||||
|
method: 'POST'
|
||||||
|
, cors: true
|
||||||
|
, headers: new Headers({ 'Content-Type': 'application/json' })
|
||||||
|
, body: JSON.stringify({ address: email, comment: 'greenlock sub for ' + domains.join(',') })
|
||||||
|
}).then(function (resp) {
|
||||||
|
return resp.json().then(function (data) {
|
||||||
|
/*
|
||||||
|
if (data.error) {
|
||||||
|
window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
}, function () {
|
||||||
|
/*
|
||||||
|
window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead.");
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
steps[1] = function () {
|
||||||
|
hideForms();
|
||||||
|
$qs('.js-acme-form-domains').hidden = false;
|
||||||
|
};
|
||||||
|
steps[1].submit = function () {
|
||||||
|
info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) {
|
||||||
|
return { type: 'dns', value: hostname.toLowerCase().trim() };
|
||||||
|
});
|
||||||
|
info.identifiers.sort(function (a, b) {
|
||||||
|
if (a === b) { return 0; }
|
||||||
|
if (a < b) { return 1; }
|
||||||
|
if (a > b) { return -1; }
|
||||||
|
});
|
||||||
|
|
||||||
|
return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) {
|
||||||
|
$qs('.js-acme-tos-url').href = directory.meta.termsOfService;
|
||||||
|
return BACME.nonce().then(function (_nonce) {
|
||||||
|
nonce = _nonce;
|
||||||
|
|
||||||
|
console.log("MAGIC STEP NUMBER in 1 is:", i);
|
||||||
|
steps[i]();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
steps[2] = function () {
|
||||||
|
hideForms();
|
||||||
|
$qs('.js-acme-form-account').hidden = false;
|
||||||
|
};
|
||||||
|
steps[2].submit = function () {
|
||||||
|
var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
|
||||||
|
|
||||||
|
|
||||||
|
info.contact = [ 'mailto:' + email ];
|
||||||
|
info.agree = $qs('.js-acme-account-tos').checked;
|
||||||
|
info.greenlockAgree = $qs('.js-gl-tos').checked;
|
||||||
|
// TODO
|
||||||
|
// options for
|
||||||
|
// * regenerate key
|
||||||
|
// * ECDSA / RSA / bitlength
|
||||||
|
|
||||||
|
// TODO ping with version and account creation
|
||||||
|
setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
|
||||||
|
|
||||||
|
var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
|
||||||
|
var p;
|
||||||
|
|
||||||
|
function createKeypair() {
|
||||||
|
return BACME.accounts.generateKeypair({
|
||||||
|
type: 'ECDSA'
|
||||||
|
, bitlength: '256'
|
||||||
|
}).then(function (jwk) {
|
||||||
|
localStorage.setItem('account:' + email, JSON.stringify(jwk));
|
||||||
|
return jwk;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwk) {
|
||||||
|
p = Promise.resolve(jwk);
|
||||||
|
} else {
|
||||||
|
p = createKeypair();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAccount(jwk) {
|
||||||
|
console.log('account jwk:');
|
||||||
|
console.log(jwk);
|
||||||
|
delete jwk.key_ops;
|
||||||
|
info.jwk = jwk;
|
||||||
|
return BACME.accounts.sign({
|
||||||
|
jwk: jwk
|
||||||
|
, contacts: [ 'mailto:' + email ]
|
||||||
|
, agree: info.agree
|
||||||
|
, nonce: nonce
|
||||||
|
, kid: kid
|
||||||
|
}).then(function (signedAccount) {
|
||||||
|
return BACME.accounts.set({
|
||||||
|
signedAccount: signedAccount
|
||||||
|
}).then(function (account) {
|
||||||
|
console.log('account:');
|
||||||
|
console.log(account);
|
||||||
|
kid = account.kid;
|
||||||
|
return kid;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.then(function (_jwk) {
|
||||||
|
jwk = _jwk;
|
||||||
|
kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
|
||||||
|
var p2
|
||||||
|
|
||||||
|
// TODO save account id rather than always retrieving it
|
||||||
|
if (kid) {
|
||||||
|
p2 = Promise.resolve(kid);
|
||||||
|
} else {
|
||||||
|
p2 = createAccount(jwk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return p2.then(function (_kid) {
|
||||||
|
kid = _kid;
|
||||||
|
info.kid = kid;
|
||||||
|
return BACME.orders.sign({
|
||||||
|
jwk: jwk
|
||||||
|
, identifiers: info.identifiers
|
||||||
|
, kid: kid
|
||||||
|
}).then(function (signedOrder) {
|
||||||
|
return BACME.orders.create({
|
||||||
|
signedOrder: signedOrder
|
||||||
|
}).then(function (order) {
|
||||||
|
info.finalizeUrl = order.finalize;
|
||||||
|
info.orderUrl = order.url; // from header Location ???
|
||||||
|
return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) {
|
||||||
|
return BACME.challenges.all().then(function (claims) {
|
||||||
|
console.log('claims:');
|
||||||
|
console.log(claims);
|
||||||
|
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
|
||||||
|
var map = {
|
||||||
|
'http-01': '.js-acme-table-http-01'
|
||||||
|
, 'dns-01': '.js-acme-table-dns-01'
|
||||||
|
, 'wildcard': '.js-acme-table-wildcard'
|
||||||
|
}
|
||||||
|
var tpls = {};
|
||||||
|
info.challenges = obj;
|
||||||
|
Object.keys(map).forEach(function (k) {
|
||||||
|
var sel = map[k] + ' tbody';
|
||||||
|
console.log(sel);
|
||||||
|
tpls[k] = $qs(sel).innerHTML;
|
||||||
|
$qs(map[k] + ' tbody').innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO make Promise-friendly
|
||||||
|
return Promise.all(claims.map(function (claim) {
|
||||||
|
var hostname = claim.identifier.value;
|
||||||
|
return Promise.all(claim.challenges.map(function (c) {
|
||||||
|
var keyAuth = BACME.challenges['http-01']({
|
||||||
|
token: c.token
|
||||||
|
, thumbprint: thumbprint
|
||||||
|
, challengeDomain: hostname
|
||||||
|
});
|
||||||
|
return BACME.challenges['dns-01']({
|
||||||
|
keyAuth: keyAuth.value
|
||||||
|
, challengeDomain: hostname
|
||||||
|
}).then(function (dnsAuth) {
|
||||||
|
var data = {
|
||||||
|
type: c.type
|
||||||
|
, hostname: hostname
|
||||||
|
, url: c.url
|
||||||
|
, token: c.token
|
||||||
|
, keyAuthorization: keyAuth
|
||||||
|
, httpPath: keyAuth.path
|
||||||
|
, httpAuth: keyAuth.value
|
||||||
|
, dnsType: dnsAuth.type
|
||||||
|
, dnsHost: dnsAuth.host
|
||||||
|
, dnsAnswer: dnsAuth.answer
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('CHALLENGE');
|
||||||
|
console.log(claim);
|
||||||
|
console.log(c);
|
||||||
|
console.log(data);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (claim.wildcard) {
|
||||||
|
obj.wildcard.push(data);
|
||||||
|
$qs(map.wildcard).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
|
||||||
|
} else if(obj[data.type]) {
|
||||||
|
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
})).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;
|
||||||
|
}());
|
Binary file not shown.
Binary file not shown.
303
index.html
303
index.html
|
@ -2,236 +2,93 @@
|
||||||
<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.ppl.family/img/greenlock-mark-400x400.png" />
|
||||||
<link href="style/main.css" rel="stylesheet">
|
<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="./fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous">
|
||||||
|
<link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.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">
|
||||||
|
<script>
|
||||||
|
document.querySelector('body').classList.remove("js-app-ready");
|
||||||
|
</script>
|
||||||
<div class="column-container wide">
|
<div class="column-container wide">
|
||||||
|
|
||||||
<div class="column-row">
|
<div class="column-row">
|
||||||
<img src="img/greenlock-146.png">
|
<img src="img/greenlock-146.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="column-row">
|
<div class="column-row">
|
||||||
<h1>Get the green lock for your website</h1>
|
<h1>Get the green lock for your website</h1>
|
||||||
|
|
||||||
<!-- Step 1 Choose Domain(s) -->
|
|
||||||
<form class="js-acme-form js-acme-form-domains">
|
|
||||||
<h1><label>What's your domain?</label></h1>
|
|
||||||
<h4>Certificates are valid for 90 days. Renewal is free :)</h4>
|
|
||||||
<input class="js-acme-domains" type="text" placeholder="example.com,*.example.com" required>
|
|
||||||
<br>
|
|
||||||
<button type="submit">Next</button>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="v02" checked required>
|
|
||||||
Production</label>
|
|
||||||
<label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="staging-v02" required>
|
|
||||||
Testing</label>
|
|
||||||
<br>
|
|
||||||
<input class="js-acme-directory-url" type="url" placeholder="ACME directory url">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Step 2 Create Account -->
|
|
||||||
<form class="js-acme-form js-acme-form-account">
|
|
||||||
<h1><label>What's your email?</label></h1>
|
|
||||||
<input class="js-acme-account-email" type="email" placeholder="john@doe.family" required>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<label><input class="js-acme-account-tos" type="checkbox" required>
|
|
||||||
Agree to <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt™ 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 class="js-acme-wildcard">
|
|
||||||
<h2>Verify Wildcard Domains</h2>
|
|
||||||
|
|
||||||
<table class="js-acme-table-wildcard">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Hostname</th>
|
|
||||||
<th>TXT Host</th>
|
|
||||||
<th>TXT Value</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>example.com</td>
|
|
||||||
<td>_acme-challenge.example.com</td>
|
|
||||||
<td>4A54</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit">Next</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Step 4 Process Challanges -->
|
|
||||||
<form class="js-acme-form js-acme-form-poll">
|
|
||||||
Verifying Domains... (give us 5 seconds or so...)
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<table class="js-acme-table-verifying">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Hostname</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Pass</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>example.com</td>
|
|
||||||
<td>http-01</td>
|
|
||||||
<td>-</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<a href="#">advanced (use existing keypair for domain)</a>
|
|
||||||
|
|
||||||
<button type="submit">Next</button>
|
|
||||||
-->
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Step 5 Get Certs -->
|
|
||||||
<form class="js-acme-form js-acme-form-download">
|
|
||||||
<div>
|
|
||||||
<h2><label>privkey.pem</label></h2>
|
|
||||||
<textarea cols="80" rows="10" class="js-privkey">-</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2><label>fullchain.pem</label></h2>
|
|
||||||
<textarea cols="80" rows="60" class="js-fullchain">-</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3>node.js https server example</h3>
|
|
||||||
<pre><code>'use strict';
|
|
||||||
|
|
||||||
var https = require('https');
|
|
||||||
var server = https.createServer({
|
|
||||||
key: require('fs').readFileSync('./privkey.pem')
|
|
||||||
, cert: require('fs').readFileSync('./fullchain.pem')
|
|
||||||
}, function (req, res) {
|
|
||||||
res.end("Hello, World!");
|
|
||||||
}).listen(443, function () {
|
|
||||||
console.log('Listening on', this.address());
|
|
||||||
})
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
TODO
|
|
||||||
<label>cert.pem</label>
|
|
||||||
<textarea class="js-cert">-</textarea>
|
|
||||||
|
|
||||||
<label>chain.pem</label>
|
|
||||||
<textarea class="js-chain">-</textarea>
|
|
||||||
|
|
||||||
<button type="button">Download SSL Certificates</button>
|
|
||||||
<br>
|
|
||||||
<a href="#">Advanced (copy and paste)</a>
|
|
||||||
<br>
|
|
||||||
<button type="submit">Start Over</button>
|
|
||||||
-->
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<div><small>
|
|
||||||
<h3></h3>
|
|
||||||
<a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git)
|
|
||||||
<!-- or
|
|
||||||
<pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre>
|
|
||||||
Or view the live site code (same as live-site branch):
|
|
||||||
<pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
|
|
||||||
-->
|
|
||||||
</small></div>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
<div class="column-row">
|
||||||
|
<!-- Step 1 Choose Domain(s) -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="acme-advanced-fields">
|
||||||
|
<label><input name="acme-api-type" type="radio" value="v02" checked required>
|
||||||
|
Production
|
||||||
|
</label>
|
||||||
|
<label><input name="acme-api-type" type="radio" value="staging-v02" required>
|
||||||
|
Testing</label>
|
||||||
|
<input id="js-acme-api-url" name="acme-api-url" type="url" placeholder="ACME directory url">
|
||||||
|
<div>
|
||||||
|
<a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="column-row">
|
||||||
|
<div class="why-you-need">
|
||||||
|
<h2>Why you need SSL certificates</h2>
|
||||||
|
If your website doesn't have the green lock from an SSL Certificate, Google Chrome will soon label your website as not secure.
|
||||||
|
</div>
|
||||||
|
</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.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script src="./js/app.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>
|
</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
|
||||||
|
|
516
js/app.js
516
js/app.js
|
@ -3,512 +3,26 @@
|
||||||
|
|
||||||
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 = {};
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
function saveContact(email, domains) {
|
|
||||||
// to be used for good, not evil
|
|
||||||
return window.fetch('https://api.ppl.family/api/ppl.family/public/list', {
|
|
||||||
method: 'POST'
|
|
||||||
, cors: true
|
|
||||||
, headers: new Headers({ 'Content-Type': 'application/json' })
|
|
||||||
, body: JSON.stringify({ address: email, comment: 'greenlock sub for ' + domains.join(',') })
|
|
||||||
}).then(function (resp) {
|
|
||||||
return resp.json().then(function (data) {
|
|
||||||
/*
|
|
||||||
if (data.error) {
|
|
||||||
window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
}, function () {
|
|
||||||
/*
|
|
||||||
window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead.");
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
steps[1] = function () {
|
|
||||||
hideForms();
|
|
||||||
$qs('.js-acme-form-domains').hidden = false;
|
|
||||||
};
|
|
||||||
steps[1].submit = function () {
|
|
||||||
info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) {
|
|
||||||
return { type: 'dns', value: hostname.toLowerCase().trim() };
|
|
||||||
});
|
|
||||||
info.identifiers.sort(function (a, b) {
|
|
||||||
if (a === b) { return 0; }
|
|
||||||
if (a < b) { return 1; }
|
|
||||||
if (a > b) { return -1; }
|
|
||||||
});
|
|
||||||
|
|
||||||
return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) {
|
|
||||||
$qs('.js-acme-tos-url').href = directory.meta.termsOfService;
|
|
||||||
return BACME.nonce().then(function (_nonce) {
|
|
||||||
nonce = _nonce;
|
|
||||||
|
|
||||||
console.log("MAGIC STEP NUMBER in 1 is:", i);
|
|
||||||
steps[i]();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
steps[2] = function () {
|
|
||||||
hideForms();
|
|
||||||
$qs('.js-acme-form-account').hidden = false;
|
|
||||||
};
|
|
||||||
steps[2].submit = function () {
|
|
||||||
var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
|
|
||||||
|
|
||||||
|
|
||||||
info.contact = [ 'mailto:' + email ];
|
|
||||||
info.agree = $qs('.js-acme-account-tos').checked;
|
|
||||||
info.greenlockAgree = $qs('.js-gl-tos').checked;
|
|
||||||
// TODO
|
|
||||||
// options for
|
|
||||||
// * regenerate key
|
|
||||||
// * ECDSA / RSA / bitlength
|
|
||||||
|
|
||||||
// TODO ping with version and account creation
|
|
||||||
setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
|
|
||||||
|
|
||||||
var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
|
|
||||||
var p;
|
|
||||||
|
|
||||||
function createKeypair() {
|
|
||||||
return BACME.accounts.generateKeypair({
|
|
||||||
type: 'ECDSA'
|
|
||||||
, bitlength: '256'
|
|
||||||
}).then(function (jwk) {
|
|
||||||
localStorage.setItem('account:' + email, JSON.stringify(jwk));
|
|
||||||
return jwk;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jwk) {
|
|
||||||
p = Promise.resolve(jwk);
|
|
||||||
} else {
|
|
||||||
p = createKeypair();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAccount(jwk) {
|
|
||||||
console.log('account jwk:');
|
|
||||||
console.log(jwk);
|
|
||||||
delete jwk.key_ops;
|
|
||||||
info.jwk = jwk;
|
|
||||||
return BACME.accounts.sign({
|
|
||||||
jwk: jwk
|
|
||||||
, contacts: [ 'mailto:' + email ]
|
|
||||||
, agree: info.agree
|
|
||||||
, nonce: nonce
|
|
||||||
, kid: kid
|
|
||||||
}).then(function (signedAccount) {
|
|
||||||
return BACME.accounts.set({
|
|
||||||
signedAccount: signedAccount
|
|
||||||
}).then(function (account) {
|
|
||||||
console.log('account:');
|
|
||||||
console.log(account);
|
|
||||||
kid = account.kid;
|
|
||||||
return kid;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.then(function (_jwk) {
|
|
||||||
jwk = _jwk;
|
|
||||||
kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
|
|
||||||
var p2
|
|
||||||
|
|
||||||
// TODO save account id rather than always retrieving it
|
|
||||||
if (kid) {
|
|
||||||
p2 = Promise.resolve(kid);
|
|
||||||
} else {
|
|
||||||
p2 = createAccount(jwk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return p2.then(function (_kid) {
|
|
||||||
kid = _kid;
|
|
||||||
info.kid = kid;
|
|
||||||
return BACME.orders.sign({
|
|
||||||
jwk: jwk
|
|
||||||
, identifiers: info.identifiers
|
|
||||||
, kid: kid
|
|
||||||
}).then(function (signedOrder) {
|
|
||||||
return BACME.orders.create({
|
|
||||||
signedOrder: signedOrder
|
|
||||||
}).then(function (order) {
|
|
||||||
info.finalizeUrl = order.finalize;
|
|
||||||
info.orderUrl = order.url; // from header Location ???
|
|
||||||
return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) {
|
|
||||||
return BACME.challenges.all().then(function (claims) {
|
|
||||||
console.log('claims:');
|
|
||||||
console.log(claims);
|
|
||||||
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
|
|
||||||
var map = {
|
|
||||||
'http-01': '.js-acme-table-http-01'
|
|
||||||
, 'dns-01': '.js-acme-table-dns-01'
|
|
||||||
, 'wildcard': '.js-acme-table-wildcard'
|
|
||||||
}
|
|
||||||
var tpls = {};
|
|
||||||
info.challenges = obj;
|
|
||||||
Object.keys(map).forEach(function (k) {
|
|
||||||
var sel = map[k] + ' tbody';
|
|
||||||
console.log(sel);
|
|
||||||
tpls[k] = $qs(sel).innerHTML;
|
|
||||||
$qs(map[k] + ' tbody').innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO make Promise-friendly
|
|
||||||
return Promise.all(claims.map(function (claim) {
|
|
||||||
var hostname = claim.identifier.value;
|
|
||||||
return Promise.all(claim.challenges.map(function (c) {
|
|
||||||
var keyAuth = BACME.challenges['http-01']({
|
|
||||||
token: c.token
|
|
||||||
, thumbprint: thumbprint
|
|
||||||
, challengeDomain: hostname
|
|
||||||
});
|
|
||||||
return BACME.challenges['dns-01']({
|
|
||||||
keyAuth: keyAuth.value
|
|
||||||
, challengeDomain: hostname
|
|
||||||
}).then(function (dnsAuth) {
|
|
||||||
var data = {
|
|
||||||
type: c.type
|
|
||||||
, hostname: hostname
|
|
||||||
, url: c.url
|
|
||||||
, token: c.token
|
|
||||||
, keyAuthorization: keyAuth
|
|
||||||
, httpPath: keyAuth.path
|
|
||||||
, httpAuth: keyAuth.value
|
|
||||||
, dnsType: dnsAuth.type
|
|
||||||
, dnsHost: dnsAuth.host
|
|
||||||
, dnsAnswer: dnsAuth.answer
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('CHALLENGE');
|
|
||||||
console.log(claim);
|
|
||||||
console.log(c);
|
|
||||||
console.log(data);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
if (claim.wildcard) {
|
|
||||||
obj.wildcard.push(data);
|
|
||||||
$qs(map.wildcard).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>';
|
|
||||||
} else if(obj[data.type]) {
|
|
||||||
|
|
||||||
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>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}));
|
|
||||||
})).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;
|
|
||||||
}());
|
}());
|
||||||
|
|
100
styles/main.css
100
styles/main.css
|
@ -1,5 +1,105 @@
|
||||||
.column-row {
|
.column-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
align-items: 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;
|
||||||
|
}
|
Loading…
Reference in New Issue