WIP Building out all features necessary for Let's Encrypt #6
9
app.js
9
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));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
14
lib/acme.js
14
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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
102
lib/csr.js
102
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));
|
||||
|
|
Loading…
Reference in New Issue