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 Eckles = window.Eckles;
|
||||
var x509 = window.x509;
|
||||
var CSR = window.CSR;
|
||||
var ACME = window.ACME;
|
||||
var accountStuff = {};
|
||||
|
||||
function $(sel) {
|
||||
return document.querySelector(sel);
|
||||
|
@ -15,6 +17,11 @@
|
|||
return Array.prototype.slice.call(document.querySelectorAll(sel));
|
||||
}
|
||||
|
||||
function checkTos(tos) {
|
||||
console.log("TODO checkbox for agree to terms");
|
||||
return tos;
|
||||
}
|
||||
|
||||
function run() {
|
||||
console.log('hello');
|
||||
|
||||
|
@ -101,6 +108,9 @@
|
|||
$$('input').map(function ($el) { $el.disabled = false; });
|
||||
$$('button').map(function ($el) { $el.disabled = 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);
|
||||
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||
var email = $('.js-email').value;
|
||||
function checkTos(tos) {
|
||||
console.log("TODO checkbox for agree to terms");
|
||||
return tos;
|
||||
}
|
||||
return acme.accounts.create({
|
||||
email: email
|
||||
, agreeToTerms: checkTos
|
||||
, accountKeypair: { privateKeyJwk: privJwk }
|
||||
}).then(function (account) {
|
||||
console.log("account created result:", account);
|
||||
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]
|
||||
});
|
||||
});
|
||||
accountStuff.account = account;
|
||||
accountStuff.privateJwk = privJwk;
|
||||
accountStuff.email = email;
|
||||
accountStuff.acme = acme;
|
||||
$('.js-create-order').hidden = false;
|
||||
}).catch(function (err) {
|
||||
console.error("A bad thing happened:");
|
||||
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-create-account').hidden = false;
|
||||
}
|
||||
|
||||
window.addEventListener('load', run);
|
||||
|
|
14
index.html
14
index.html
|
@ -58,15 +58,26 @@
|
|||
<label for="-acmeEmail">Email:</label>
|
||||
<input class="js-email" type="email" id="-acmeEmail">
|
||||
<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>
|
||||
<input class="js-domains" type="text" id="-acmeDomains">
|
||||
<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"
|
||||
name="acme-challenge-type" value="http-01" checked>http-01</label>
|
||||
<label for="-dns01"><input type="radio" id="-dns01"
|
||||
name="acme-challenge-type" value="dns-01">dns-01</label>
|
||||
<br>
|
||||
<button class="js-create-account" hidden>Create Account</button>
|
||||
<button class="js-create-order" hidden>Create Order</button>
|
||||
</form>
|
||||
|
||||
<div class="js-loading" hidden>Loading</div>
|
||||
|
@ -117,6 +128,7 @@
|
|||
<script src="./lib/ecdsa.js"></script>
|
||||
<script src="./lib/rsa.js"></script>
|
||||
<script src="./lib/keypairs.js"></script>
|
||||
<script src="./lib/csr.js"></script>
|
||||
<script src="./lib/acme.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
|
|
|
@ -110,6 +110,8 @@ Enc.binToHex = function (bin) {
|
|||
return h;
|
||||
}).join('');
|
||||
};
|
||||
// TODO are there any nuance differences here?
|
||||
Enc.utf8ToHex = Enc.binToHex;
|
||||
|
||||
Enc.hexToBase64 = function (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';
|
||||
|
||||
var crypto = require('crypto');
|
||||
var ASN1 = require('./asn1.js');
|
||||
var Enc = require('./encoding.js');
|
||||
var PEM = require('./pem.js');
|
||||
var X509 = require('./x509.js');
|
||||
var RSA = {};
|
||||
var ASN1 = exports.ASN1;
|
||||
var Enc = exports.Enc;
|
||||
var PEM = exports.PEM;
|
||||
var X509 = exports.x509;
|
||||
var Keypairs = exports.Keypairs;
|
||||
|
||||
/*global Promise*/
|
||||
var CSR = module.exports = function rsacsr(opts) {
|
||||
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
|
||||
var CSR = exports.CSR = function (opts) {
|
||||
// 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
|
||||
opts = CSR._prepare(opts);
|
||||
|
@ -69,11 +73,7 @@ CSR._prepare = function (opts) {
|
|||
opts.jwk = jwk;
|
||||
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) {
|
||||
if ('der' === (opts.encoding||'').toLowerCase()) {
|
||||
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) {
|
||||
var hex = CSR.request(opts.jwk, opts.domains);
|
||||
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) {
|
||||
var asn1pub = X509.packCsrPublicKey(jwk);
|
||||
return X509.packCsr(asn1pub, domains);
|
||||
var asn1pub = X509.packCsrRsaPublicKey(jwk);
|
||||
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) {
|
||||
var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) });
|
||||
return RSA.sign(keypem, Enc.hexToBuf(request)).then(function (sig) {
|
||||
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
||||
// 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 });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
CSR.toDer = function encode(opts) {
|
||||
var sty = ASN1('30'
|
||||
// 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) {
|
||||
return ASN1('30'
|
||||
// Version (0)
|
||||
|
@ -211,3 +185,5 @@ X509.packCsrRsaPublicKey = function (jwk) {
|
|||
// Add the CSR pub key header
|
||||
return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub));
|
||||
};
|
||||
|
||||
}('undefined' === typeof window ? module.exports : window));
|
||||
|
|
Loading…
Reference in New Issue