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