WIP Building out all features necessary for Let's Encrypt #6

Closed
coolaj86 wants to merge 17 commits from more-acme into master
4 changed files with 137 additions and 115 deletions
Showing only changes of commit 07f75f8e43 - Show all commits

158
app.js
View File

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

View File

@ -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>

View File

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

View File

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