WIP Building out all features necessary for Let's Encrypt #6
158
app.js
158
app.js
|
@ -6,7 +6,9 @@
|
||||||
var Rasha = window.Rasha;
|
var Rasha = window.Rasha;
|
||||||
var Eckles = window.Eckles;
|
var Eckles = window.Eckles;
|
||||||
var x509 = window.x509;
|
var x509 = window.x509;
|
||||||
|
var CSR = window.CSR;
|
||||||
var ACME = window.ACME;
|
var ACME = window.ACME;
|
||||||
|
var accountStuff = {};
|
||||||
|
|
||||||
function $(sel) {
|
function $(sel) {
|
||||||
return document.querySelector(sel);
|
return document.querySelector(sel);
|
||||||
|
@ -15,6 +17,11 @@
|
||||||
return Array.prototype.slice.call(document.querySelectorAll(sel));
|
return Array.prototype.slice.call(document.querySelectorAll(sel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkTos(tos) {
|
||||||
|
console.log("TODO checkbox for agree to terms");
|
||||||
|
return tos;
|
||||||
|
}
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
console.log('hello');
|
console.log('hello');
|
||||||
|
|
||||||
|
@ -101,6 +108,9 @@
|
||||||
$$('input').map(function ($el) { $el.disabled = false; });
|
$$('input').map(function ($el) { $el.disabled = false; });
|
||||||
$$('button').map(function ($el) { $el.disabled = false; });
|
$$('button').map(function ($el) { $el.disabled = false; });
|
||||||
$('.js-toc-jwk').hidden = false;
|
$('.js-toc-jwk').hidden = false;
|
||||||
|
|
||||||
|
$('.js-create-account').hidden = false;
|
||||||
|
$('.js-create-csr').hidden = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -115,74 +125,17 @@
|
||||||
console.log('acme result', result);
|
console.log('acme result', result);
|
||||||
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||||
var email = $('.js-email').value;
|
var email = $('.js-email').value;
|
||||||
function checkTos(tos) {
|
|
||||||
console.log("TODO checkbox for agree to terms");
|
|
||||||
return tos;
|
|
||||||
}
|
|
||||||
return acme.accounts.create({
|
return acme.accounts.create({
|
||||||
email: email
|
email: email
|
||||||
, agreeToTerms: checkTos
|
, agreeToTerms: checkTos
|
||||||
, accountKeypair: { privateKeyJwk: privJwk }
|
, accountKeypair: { privateKeyJwk: privJwk }
|
||||||
}).then(function (account) {
|
}).then(function (account) {
|
||||||
console.log("account created result:", account);
|
console.log("account created result:", account);
|
||||||
return Keypairs.generate({
|
accountStuff.account = account;
|
||||||
kty: 'RSA'
|
accountStuff.privateJwk = privJwk;
|
||||||
, modulusLength: 2048
|
accountStuff.email = email;
|
||||||
}).then(function (pair) {
|
accountStuff.acme = acme;
|
||||||
console.log('domain keypair:', pair);
|
$('.js-create-order').hidden = false;
|
||||||
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
|
||||||
return acme.certificates.create({
|
|
||||||
accountKeypair: { privateKeyJwk: privJwk }
|
|
||||||
, account: account
|
|
||||||
, domainKeypair: { privateKeyJwk: pair.private }
|
|
||||||
, email: email
|
|
||||||
, domains: domains
|
|
||||||
, agreeToTerms: checkTos
|
|
||||||
, challenges: {
|
|
||||||
'dns-01': {
|
|
||||||
set: function (opts) {
|
|
||||||
console.info('dns-01 set challenge:');
|
|
||||||
console.info('TXT', opts.dnsHost);
|
|
||||||
console.info(opts.dnsAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you set the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
, remove: function (opts) {
|
|
||||||
console.log('dns-01 remove challenge:');
|
|
||||||
console.info('TXT', opts.dnsHost);
|
|
||||||
console.info(opts.dnsAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you delete the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, 'http-01': {
|
|
||||||
set: function (opts) {
|
|
||||||
console.info('http-01 set challenge:');
|
|
||||||
console.info(opts.challengeUrl);
|
|
||||||
console.info(opts.keyAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you set the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
, remove: function (opts) {
|
|
||||||
console.log('http-01 remove challenge:');
|
|
||||||
console.info(opts.challengeUrl);
|
|
||||||
console.info(opts.keyAuthorization);
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
while (!window.confirm("Did you delete the challenge?")) {}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, challengeTypes: [$('input[name="acme-challenge-type"]:checked').value]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.error("A bad thing happened:");
|
console.error("A bad thing happened:");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -191,8 +144,87 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('form.js-csr').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
||||||
|
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||||
|
return CSR({ jwk: privJwk, domains: domains }).then(function (web64) {
|
||||||
|
// Verify with https://www.sslshopper.com/csr-decoder.html
|
||||||
|
console.log('urlBase64 CSR:');
|
||||||
|
console.log(web64);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form.js-acme-order').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
var account = accountStuff.account;
|
||||||
|
var privJwk = accountStuff.privateJwk;
|
||||||
|
var email = accountStuff.email;
|
||||||
|
var acme = accountStuff.acme;
|
||||||
|
|
||||||
|
return Keypairs.generate({
|
||||||
|
kty: 'RSA'
|
||||||
|
, modulusLength: 2048
|
||||||
|
}).then(function (pair) {
|
||||||
|
console.log('domain keypair:', pair);
|
||||||
|
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
||||||
|
return acme.certificates.create({
|
||||||
|
accountKeypair: { privateKeyJwk: privJwk }
|
||||||
|
, account: account
|
||||||
|
, domainKeypair: { privateKeyJwk: pair.private }
|
||||||
|
, email: email
|
||||||
|
, domains: domains
|
||||||
|
, agreeToTerms: checkTos
|
||||||
|
, challenges: {
|
||||||
|
'dns-01': {
|
||||||
|
set: function (opts) {
|
||||||
|
console.info('dns-01 set challenge:');
|
||||||
|
console.info('TXT', opts.dnsHost);
|
||||||
|
console.info(opts.dnsAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (!window.confirm("Did you set the challenge?")) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
, remove: function (opts) {
|
||||||
|
console.log('dns-01 remove challenge:');
|
||||||
|
console.info('TXT', opts.dnsHost);
|
||||||
|
console.info(opts.dnsAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (!window.confirm("Did you delete the challenge?")) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, 'http-01': {
|
||||||
|
set: function (opts) {
|
||||||
|
console.info('http-01 set challenge:');
|
||||||
|
console.info(opts.challengeUrl);
|
||||||
|
console.info(opts.keyAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (!window.confirm("Did you set the challenge?")) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
, remove: function (opts) {
|
||||||
|
console.log('http-01 remove challenge:');
|
||||||
|
console.info(opts.challengeUrl);
|
||||||
|
console.info(opts.keyAuthorization);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
while (!window.confirm("Did you delete the challenge?")) {}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, challengeTypes: [$('input[name="acme-challenge-type"]:checked').value]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('.js-generate').hidden = false;
|
$('.js-generate').hidden = false;
|
||||||
$('.js-create-account').hidden = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', run);
|
window.addEventListener('load', run);
|
||||||
|
|
14
index.html
14
index.html
|
@ -58,15 +58,26 @@
|
||||||
<label for="-acmeEmail">Email:</label>
|
<label for="-acmeEmail">Email:</label>
|
||||||
<input class="js-email" type="email" id="-acmeEmail">
|
<input class="js-email" type="email" id="-acmeEmail">
|
||||||
<br>
|
<br>
|
||||||
|
<button class="js-create-account" hidden>Create Account</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Certificate Signing Request</h2>
|
||||||
|
<form class="js-csr">
|
||||||
<label for="-acmeDomains">Domains:</label>
|
<label for="-acmeDomains">Domains:</label>
|
||||||
<input class="js-domains" type="text" id="-acmeDomains">
|
<input class="js-domains" type="text" id="-acmeDomains">
|
||||||
<br>
|
<br>
|
||||||
|
<button class="js-create-csr" hidden>Create CSR</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>ACME Certificate Order</h2>
|
||||||
|
<form class="js-acme-order">
|
||||||
|
Challenge type:
|
||||||
<label for="-http01"><input type="radio" id="-http01"
|
<label for="-http01"><input type="radio" id="-http01"
|
||||||
name="acme-challenge-type" value="http-01" checked>http-01</label>
|
name="acme-challenge-type" value="http-01" checked>http-01</label>
|
||||||
<label for="-dns01"><input type="radio" id="-dns01"
|
<label for="-dns01"><input type="radio" id="-dns01"
|
||||||
name="acme-challenge-type" value="dns-01">dns-01</label>
|
name="acme-challenge-type" value="dns-01">dns-01</label>
|
||||||
<br>
|
<br>
|
||||||
<button class="js-create-account" hidden>Create Account</button>
|
<button class="js-create-order" hidden>Create Order</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="js-loading" hidden>Loading</div>
|
<div class="js-loading" hidden>Loading</div>
|
||||||
|
@ -117,6 +128,7 @@
|
||||||
<script src="./lib/ecdsa.js"></script>
|
<script src="./lib/ecdsa.js"></script>
|
||||||
<script src="./lib/rsa.js"></script>
|
<script src="./lib/rsa.js"></script>
|
||||||
<script src="./lib/keypairs.js"></script>
|
<script src="./lib/keypairs.js"></script>
|
||||||
|
<script src="./lib/csr.js"></script>
|
||||||
<script src="./lib/acme.js"></script>
|
<script src="./lib/acme.js"></script>
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -110,6 +110,8 @@ Enc.binToHex = function (bin) {
|
||||||
return h;
|
return h;
|
||||||
}).join('');
|
}).join('');
|
||||||
};
|
};
|
||||||
|
// TODO are there any nuance differences here?
|
||||||
|
Enc.utf8ToHex = Enc.binToHex;
|
||||||
|
|
||||||
Enc.hexToBase64 = function (hex) {
|
Enc.hexToBase64 = function (hex) {
|
||||||
return btoa(Enc.hexToBin(hex));
|
return btoa(Enc.hexToBin(hex));
|
||||||
|
|
78
lib/csr.js
78
lib/csr.js
|
@ -1,14 +1,18 @@
|
||||||
|
// Copyright 2018-present AJ ONeal. All rights reserved
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
(function (exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var crypto = require('crypto');
|
var ASN1 = exports.ASN1;
|
||||||
var ASN1 = require('./asn1.js');
|
var Enc = exports.Enc;
|
||||||
var Enc = require('./encoding.js');
|
var PEM = exports.PEM;
|
||||||
var PEM = require('./pem.js');
|
var X509 = exports.x509;
|
||||||
var X509 = require('./x509.js');
|
var Keypairs = exports.Keypairs;
|
||||||
var RSA = {};
|
|
||||||
|
|
||||||
/*global Promise*/
|
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
|
||||||
var CSR = module.exports = function rsacsr(opts) {
|
var CSR = exports.CSR = function (opts) {
|
||||||
// We're using a Promise here to be compatible with the browser version
|
// We're using a Promise here to be compatible with the browser version
|
||||||
// which will probably use the webcrypto API for some of the conversions
|
// which will probably use the webcrypto API for some of the conversions
|
||||||
opts = CSR._prepare(opts);
|
opts = CSR._prepare(opts);
|
||||||
|
@ -69,11 +73,7 @@ CSR._prepare = function (opts) {
|
||||||
opts.jwk = jwk;
|
opts.jwk = jwk;
|
||||||
return opts;
|
return opts;
|
||||||
};
|
};
|
||||||
CSR.sync = function (opts) {
|
|
||||||
opts = CSR._prepare(opts);
|
|
||||||
var bytes = CSR.createSync(opts);
|
|
||||||
return CSR._encode(opts, bytes);
|
|
||||||
};
|
|
||||||
CSR._encode = function (opts, bytes) {
|
CSR._encode = function (opts, bytes) {
|
||||||
if ('der' === (opts.encoding||'').toLowerCase()) {
|
if ('der' === (opts.encoding||'').toLowerCase()) {
|
||||||
return bytes;
|
return bytes;
|
||||||
|
@ -84,11 +84,6 @@ CSR._encode = function (opts, bytes) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
CSR.createSync = function createCsr(opts) {
|
|
||||||
var hex = CSR.request(opts.jwk, opts.domains);
|
|
||||||
var csr = CSR.signSync(opts.jwk, hex);
|
|
||||||
return Enc.hexToBuf(csr);
|
|
||||||
};
|
|
||||||
CSR.create = function createCsr(opts) {
|
CSR.create = function createCsr(opts) {
|
||||||
var hex = CSR.request(opts.jwk, opts.domains);
|
var hex = CSR.request(opts.jwk, opts.domains);
|
||||||
return CSR.sign(opts.jwk, hex).then(function (csr) {
|
return CSR.sign(opts.jwk, hex).then(function (csr) {
|
||||||
|
@ -96,22 +91,25 @@ CSR.create = function createCsr(opts) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// RSA
|
||||||
|
//
|
||||||
|
//
|
||||||
CSR.request = function createCsrBodyEc(jwk, domains) {
|
CSR.request = function createCsrBodyEc(jwk, domains) {
|
||||||
var asn1pub = X509.packCsrPublicKey(jwk);
|
var asn1pub = X509.packCsrRsaPublicKey(jwk);
|
||||||
return X509.packCsr(asn1pub, domains);
|
return X509.packCsrRsa(asn1pub, domains);
|
||||||
};
|
};
|
||||||
|
|
||||||
CSR.signSync = function csrEcSig(jwk, request) {
|
|
||||||
var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) });
|
|
||||||
var sig = RSA.signSync(keypem, Enc.hexToBuf(request));
|
|
||||||
return CSR.toDer({ request: request, signature: sig });
|
|
||||||
};
|
|
||||||
CSR.sign = function csrEcSig(jwk, request) {
|
CSR.sign = function csrEcSig(jwk, request) {
|
||||||
var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) });
|
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
||||||
return RSA.sign(keypem, Enc.hexToBuf(request)).then(function (sig) {
|
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
|
||||||
|
// TODO have a consistent non-private way to sign
|
||||||
|
return Keypairs._sign({ jwk: jwk }, Enc.hexToBuf(request)).then(function (sig) {
|
||||||
return CSR.toDer({ request: request, signature: sig });
|
return CSR.toDer({ request: request, signature: sig });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
CSR.toDer = function encode(opts) {
|
CSR.toDer = function encode(opts) {
|
||||||
var sty = ASN1('30'
|
var sty = ASN1('30'
|
||||||
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
|
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
|
||||||
|
@ -128,30 +126,6 @@ CSR.toDer = function encode(opts) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
|
||||||
// RSA
|
|
||||||
//
|
|
||||||
|
|
||||||
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
|
||||||
RSA.signSync = function signRsaSync(keypem, ab) {
|
|
||||||
// Signer is a stream
|
|
||||||
var sign = crypto.createSign('SHA256');
|
|
||||||
sign.write(new Uint8Array(ab));
|
|
||||||
sign.end();
|
|
||||||
|
|
||||||
// The signature is ASN1 encoded, as it turns out
|
|
||||||
var sig = sign.sign(keypem);
|
|
||||||
|
|
||||||
// Convert to a JavaScript ArrayBuffer just because
|
|
||||||
return new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength));
|
|
||||||
};
|
|
||||||
RSA.sign = function signRsa(keypem, ab) {
|
|
||||||
return Promise.resolve().then(function () {
|
|
||||||
return RSA.signSync(keypem, ab);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
X509.packCsrRsa = function (asn1pubkey, domains) {
|
X509.packCsrRsa = function (asn1pubkey, domains) {
|
||||||
return ASN1('30'
|
return ASN1('30'
|
||||||
// Version (0)
|
// Version (0)
|
||||||
|
@ -211,3 +185,5 @@ X509.packCsrRsaPublicKey = function (jwk) {
|
||||||
// Add the CSR pub key header
|
// Add the CSR pub key header
|
||||||
return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub));
|
return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
}('undefined' === typeof window ? module.exports : window));
|
||||||
|
|
Loading…
Reference in New Issue