WIP Building out all features necessary for Let's Encrypt #6
9
app.js
9
app.js
|
@ -151,10 +151,13 @@
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
||||||
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
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
|
// Verify with https://www.sslshopper.com/csr-decoder.html
|
||||||
console.log('urlBase64 CSR:');
|
console.log('CSR:');
|
||||||
console.log(web64);
|
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/ecdsa.js"></script>
|
||||||
<script src="./lib/rsa.js"></script>
|
<script src="./lib/rsa.js"></script>
|
||||||
<script src="./lib/keypairs.js"></script>
|
<script src="./lib/keypairs.js"></script>
|
||||||
|
<script src="./lib/asn1-parser.js"></script>
|
||||||
<script src="./lib/csr.js"></script>
|
<script src="./lib/csr.js"></script>
|
||||||
<script src="./lib/acme.js"></script>
|
<script src="./lib/acme.js"></script>
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
|
|
14
lib/acme.js
14
lib/acme.js
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
var ACME = exports.ACME = {};
|
var ACME = exports.ACME = {};
|
||||||
//var Keypairs = exports.Keypairs || {};
|
//var Keypairs = exports.Keypairs || {};
|
||||||
|
//var CSR = exports.CSR;
|
||||||
var Enc = exports.Enc || {};
|
var Enc = exports.Enc || {};
|
||||||
var Crypto = exports.Crypto || {};
|
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"
|
return Promise.reject(new Error("options.challengeTypes (string array) must be specified"
|
||||||
+ " (and in order of preferential priority)."));
|
+ " (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)) {
|
if (!(options.domains && options.domains.length)) {
|
||||||
return Promise.reject(new Error("options.domains must be a list of string domain names,"
|
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)."));
|
+ " 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) {
|
ACME._generateCsrWeb64 = function (me, options, validatedDomains) {
|
||||||
return ACME._importKeypair(me, options.domainKeypair).then(function (/*pair*/) {
|
return ACME._importKeypair(me, options.domainKeypair).then(function (pair) {
|
||||||
return me.Keypairs.generateCsr(options.domainKeypair, validatedDomains).then(function (der) {
|
return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) {
|
||||||
return Enc.bufToUrlBase64(der);
|
return Enc.bufToUrlBase64(der);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -830,6 +839,7 @@ ACME.create = function create(me) {
|
||||||
// me.debug = true;
|
// me.debug = true;
|
||||||
me.challengePrefixes = ACME.challengePrefixes;
|
me.challengePrefixes = ACME.challengePrefixes;
|
||||||
me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA;
|
me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA;
|
||||||
|
me.CSR = me.CSR || require('CSR').CSR;
|
||||||
me._nonces = [];
|
me._nonces = [];
|
||||||
me._canCheck = {};
|
me._canCheck = {};
|
||||||
if (!me._baseUrl) {
|
if (!me._baseUrl) {
|
||||||
|
|
|
@ -125,7 +125,7 @@ PEM.parseBlock = PEM.parseBlock || function (str) {
|
||||||
var der = str.split(/\n/).filter(function (line) {
|
var der = str.split(/\n/).filter(function (line) {
|
||||||
return !/-----/.test(line);
|
return !/-----/.test(line);
|
||||||
}).join('');
|
}).join('');
|
||||||
return { der: Enc.base64ToBuf(der) };
|
return { bytes: Enc.base64ToBuf(der) };
|
||||||
};
|
};
|
||||||
|
|
||||||
Enc.base64ToBuf = function (b64) {
|
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) {
|
X509.packCsrRsaPublicKey = function (jwk) {
|
||||||
// Sequence the key
|
// Sequence the key
|
||||||
var n = ASN1.UInt(Enc.base64ToHex(jwk.n));
|
var n = ASN1.UInt(Enc.base64ToHex(jwk.n));
|
||||||
|
@ -193,4 +287,12 @@ X509._oids = {
|
||||||
//, 'P-521': '2B 81 04 00 23'
|
//, '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));
|
}('undefined' === typeof window ? module.exports : window));
|
||||||
|
|
Loading…
Reference in New Issue