diff --git a/app.js b/app.js index 1185815..c861b5b 100644 --- a/app.js +++ b/app.js @@ -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)); }); }); diff --git a/index.html b/index.html index 4379cdb..5978ffd 100644 --- a/index.html +++ b/index.html @@ -128,6 +128,7 @@ + diff --git a/lib/acme.js b/lib/acme.js index 87668c4..ea7113a 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -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) { diff --git a/lib/asn1-parser.js b/lib/asn1-parser.js index 82f7cd0..9314aa3 100644 --- a/lib/asn1-parser.js +++ b/lib/asn1-parser.js @@ -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) { diff --git a/lib/csr.js b/lib/csr.js index 3eb75cd..4f6d61b 100644 --- a/lib/csr.js +++ b/lib/csr.js @@ -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));