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

Closed
coolaj86 wants to merge 17 commits from more-acme into master
5 changed files with 122 additions and 6 deletions
Showing only changes of commit 266d8a0ba0 - Show all commits

9
app.js
View File

@ -151,10 +151,13 @@
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) {
return CSR({ jwk: privJwk, domains: domains }).then(function (pem) {
// Verify with https://www.sslshopper.com/csr-decoder.html
console.log('urlBase64 CSR:');
console.log(web64);
console.log('CSR:');
console.log(pem);
console.log('CSR info:');
console.log(CSR._info(pem));
});
});

View File

@ -128,6 +128,7 @@
<script src="./lib/ecdsa.js"></script>
<script src="./lib/rsa.js"></script>
<script src="./lib/keypairs.js"></script>
<script src="./lib/asn1-parser.js"></script>
<script src="./lib/csr.js"></script>
<script src="./lib/acme.js"></script>
<script src="./app.js"></script>

View File

@ -8,6 +8,7 @@
var ACME = exports.ACME = {};
//var Keypairs = exports.Keypairs || {};
//var CSR = exports.CSR;
var Enc = exports.Enc || {};
var Crypto = exports.Crypto || {};
@ -670,6 +671,14 @@ ACME._getCertificate = function (me, options) {
return Promise.reject(new Error("options.challengeTypes (string array) must be specified"
+ " (and in order of preferential priority)."));
}
if (options.csr) {
// TODO validate csr signature
options._csr = me.CSR._info(options.csr);
options.domains = options._csr.altnames;
if (options._csr.subject !== options.domains[0]) {
return Promise.reject(new Error("certificate subject (commonName) does not match first altname (SAN)"));
}
}
if (!(options.domains && options.domains.length)) {
return Promise.reject(new Error("options.domains must be a list of string domain names,"
+ " with the first being the subject of the certificate (or options.subject must specified)."));
@ -818,8 +827,8 @@ ACME._getCertificate = function (me, options) {
});
};
ACME._generateCsrWeb64 = function (me, options, validatedDomains) {
return ACME._importKeypair(me, options.domainKeypair).then(function (/*pair*/) {
return me.Keypairs.generateCsr(options.domainKeypair, validatedDomains).then(function (der) {
return ACME._importKeypair(me, options.domainKeypair).then(function (pair) {
return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) {
return Enc.bufToUrlBase64(der);
});
});
@ -830,6 +839,7 @@ ACME.create = function create(me) {
// me.debug = true;
me.challengePrefixes = ACME.challengePrefixes;
me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA;
me.CSR = me.CSR || require('CSR').CSR;
me._nonces = [];
me._canCheck = {};
if (!me._baseUrl) {

View File

@ -125,7 +125,7 @@ PEM.parseBlock = PEM.parseBlock || function (str) {
var der = str.split(/\n/).filter(function (line) {
return !/-----/.test(line);
}).join('');
return { der: Enc.base64ToBuf(der) };
return { bytes: Enc.base64ToBuf(der) };
};
Enc.base64ToBuf = function (b64) {

View File

@ -155,6 +155,100 @@ X509.packCsr = function (asn1pubkey, domains) {
);
};
// TODO finish this later
// we want to parse the domains, the public key, and verify the signature
CSR._info = function (der) {
// standard base64 PEM
if ('string' === typeof der && '-' === der[0]) {
der = PEM.parseBlock(der).bytes;
}
// jose urlBase64 not-PEM
if ('string' === typeof der) {
der = Enc.base64ToBuf(der);
}
// not supporting binary-encoded bas64
var c = ASN1.parse(der);
var kty;
// A cert has 3 parts: cert, signature meta, signature
if (c.children.length !== 3) {
throw new Error("doesn't look like a certificate request: expected 3 parts of header");
}
var sig = c.children[2];
if (sig.children.length) {
// ASN1/X509 EC
sig = sig.children[0];
sig = ASN1('30', ASN1.UInt(Enc.bufToHex(sig.children[0].value)), ASN1.UInt(Enc.bufToHex(sig.children[1].value)));
sig = Enc.hexToBuf(sig);
kty = 'EC';
} else {
// Raw RSA Sig
sig = sig.value;
kty = 'RSA';
}
//c.children[1]; // signature type
var req = c.children[0];
// TODO utf8
if (4 !== req.children.length) {
throw new Error("doesn't look like a certificate request: expected 4 parts to request");
}
// 0 null
// 1 commonName / subject
var sub = Enc.bufToBin(req.children[1].children[0].children[0].children[1].value);
// 3 public key (type, key)
//console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value));
var pub;
// TODO reuse ASN1 parser for these?
if ('EC' === kty) {
// throw away compression byte
pub = req.children[2].children[1].value.slice(1);
pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) };
while (0 === pub.x[0]) { pub.x = pub.x.slice(1); }
while (0 === pub.y[0]) { pub.y = pub.y.slice(1); }
if ((pub.x.length || pub.x.byteLength) > 48) {
pub.crv = 'P-521';
} else if ((pub.x.length || pub.x.byteLength) > 32) {
pub.crv = 'P-384';
} else {
pub.crv = 'P-256';
}
pub.x = Enc.bufToUrlBase64(pub.x);
pub.y = Enc.bufToUrlBase64(pub.y);
} else {
pub = req.children[2].children[1].children[0];
pub = { kty: kty, n: pub.children[0].value, e: pub.children[1].value };
while (0 === pub.n[0]) { pub.n = pub.n.slice(1); }
while (0 === pub.e[0]) { pub.e = pub.e.slice(1); }
pub.n = Enc.bufToUrlBase64(pub.n);
pub.e = Enc.bufToUrlBase64(pub.e);
}
// 4 extensions
var domains = req.children[3].children.filter(function (seq) {
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) {
return true;
}
}).map(function (seq) {
return seq.children[1].children[0].children.filter(function (seq2) {
// subjectAltName (X.509 extension)
if ('551d11' === Enc.bufToHex(seq2.children[0].value)) {
return true;
}
}).map(function (seq2) {
return seq2.children[1].children[0].children.map(function (name) {
// TODO utf8
return Enc.bufToBin(name.value);
});
});
})[0];
return {
subject: sub
, altnames: domains
, jwk: pub
, signature: sig
};
};
X509.packCsrRsaPublicKey = function (jwk) {
// Sequence the key
var n = ASN1.UInt(Enc.base64ToHex(jwk.n));
@ -193,4 +287,12 @@ X509._oids = {
//, 'P-521': '2B 81 04 00 23'
};
// don't replace the full parseBlock, if it exists
PEM.parseBlock = PEM.parseBlock || function (str) {
var der = str.split(/\n/).filter(function (line) {
return !/-----/.test(line);
}).join('');
return { bytes: Enc.base64ToBuf(der) };
};
}('undefined' === typeof window ? module.exports : window));