diff --git a/lib/csr-ec.js b/lib/csr-ec.js new file mode 100644 index 0000000..72ee072 --- /dev/null +++ b/lib/csr-ec.js @@ -0,0 +1,157 @@ +// 1.2.840.10045.3.1.7 +// prime256v1 (ANSI X9.62 named elliptic curve) +var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); +// 1.3.132.0.34 +// secp384r1 (SECG (Certicom) named elliptic curve) +var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); + +var ECDSACSR = {}; +var ECDSA = {}; +var DER = {}; +var PEM = {}; +var ASN1; +var Hex = {}; +var AB = {}; + +// +// CSR - the main event +// + +ECDSACSR.create = function createEcCsr(keypem, domains) { + var pemblock = PEM.parseBlock(keypem); + var ecpub = PEM.parseEcPubkey(pemblock.der); + var request = ECDSACSR.request(ecpub, domains); + return AB.fromHex(ECDSACSR.sign(keypem, request)); +}; + +ECDSACSR.request = function createCsrBodyEc(xy, domains) { + var publen = xy.x.byteLength; + var compression = '04'; + var hxy = ''; + // 04 == x+y, 02 == x-only + if (xy.y) { + publen += xy.y.byteLength; + } else { + // Note: I don't intend to support compression - it isn't used by most + // libraries and it requir more dependencies for bigint ops to deflate. + // This is more just a placeholder. It won't work right now anyway + // because compression requires an exta bit stored (odd vs even), which + // I haven't learned yet, and I'm not sure if it's allowed at all + compression = '02'; + } + hxy += Hex.fromAB(xy.x); + if (xy.y) { hxy += Hex.fromAB(xy.y); } + + // Sorry for the mess, but it is what it is + return ASN1('30' + + // Version (0) + , ASN1.UInt('00') + + // CN / Subject + , ASN1('30' + , ASN1('31' + , ASN1('30' + // object id (commonName) + , ASN1('06', '55 04 03') + , ASN1('0C', Hex.fromString(domains[0]))))) + + // EC P-256 Public Key + , ASN1('30' + , ASN1('30' + // 1.2.840.10045.2.1 ecPublicKey + // (ANSI X9.62 public key type) + , ASN1('06', '2A 86 48 CE 3D 02 01') + // 1.2.840.10045.3.1.7 prime256v1 + // (ANSI X9.62 named elliptic curve) + , ASN1('06', '2A 86 48 CE 3D 03 01 07') + ) + , ASN1.BitStr(compression + hxy)) + + // CSR Extension Subject Alternative Names + , ASN1('A0' + , ASN1('30' + // (extensionRequest (PKCS #9 via CRMF)) + , ASN1('06', '2A 86 48 86 F7 0D 01 09 0E') + , ASN1('31' + , ASN1('30' + , ASN1('30' + // (subjectAltName (X.509 extension)) + , ASN1('06', '55 1D 11') + , ASN1('04' + , ASN1('30', domains.map(function (d) { + return ASN1('82', Hex.fromString(d)); + }).join('')))))))) + ); +}; + +ECDSACSR.sign = function csrEcSig(keypem, request) { + var sig = ECDSA.sign(keypem, AB.fromHex(request)); + var rLen = sig.r.byteLength; + var rc = ''; + var sLen = sig.s.byteLength; + var sc = ''; + + if (0x80 & new Uint8Array(sig.r)[0]) { rc = '00'; rLen += 1; } + if (0x80 & new Uint8Array(sig.s)[0]) { sc = '00'; sLen += 1; } + + return ASN1('30' + // The Full CSR Request Body + , request + + // The Signature Type + , ASN1('30' + // 1.2.840.10045.4.3.2 ecdsaWithSHA256 + // (ANSI X9.62 ECDSA algorithm with SHA256) + , ASN1('06', '2A 86 48 CE 3D 04 03 02') + ) + + // The Signature, embedded in a Bit Stream + , ASN1.BitStr( + // As far as I can tell this is a completely separate ASN.1 structure + // that just so happens to be embedded in a Bit String of another ASN.1 + ASN1('30' + , ASN1.UInt(Hex.fromAB(sig.r)) + , ASN1.UInt(Hex.fromAB(sig.s)))) + ); +}; + +// +// ECDSA +// + +// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a +ECDSA.sign = function signEc(keypem, ab) { + // Signer is a stream + var sign = crypto.createSign('SHA256'); + sign.write(new Uint8Array(ab)); + sign.end(); + + // The signature is ASN1 encoded + var sig = sign.sign(keypem); + + // Convert to a JavaScript ArrayBuffer just because + sig = new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); + + // The first two bytes '30 xx' signify SEQUENCE and LENGTH + // The sequence length byte will be a single byte because the signature is less that 128 bytes (0x80, 1024-bit) + // (this would not be true for P-521, but I'm not supporting that yet) + // The 3rd byte will be '02', signifying INTEGER + // The 4th byte will tell us the length of 'r' (which, on occassion, will be less than the full 255 bytes) + var rIndex = 3; + var rLen = sig[rIndex]; + var rEnd = rIndex + 1 + rLen; + var sIndex = rEnd + 1; + var sLen = sig[sIndex]; + var sEnd = sIndex + 1 + sLen; + var r = sig.slice(rIndex + 1, rEnd); + var s = sig.slice(sIndex + 1, sEnd); // this should be end-of-file + + // ASN1 INTEGER types use the high-order bit to signify a negative number, + // hence a leading '00' is used for numbers that begin with '80' or greater + // which is why r length is sometimes a byte longer than its bit length + if (0 === s[0]) { s = s.slice(1); } + if (0 === r[0]) { r = r.slice(1); } + + return { raw: sig.buffer, r: r.buffer, s: s.buffer }; +}; diff --git a/lib/csr.js b/lib/csr.js new file mode 100644 index 0000000..aab4763 --- /dev/null +++ b/lib/csr.js @@ -0,0 +1,213 @@ +'use strict'; + +var crypto = require('crypto'); +var ASN1 = require('./asn1.js'); +var Enc = require('./encoding.js'); +var PEM = require('./pem.js'); +var X509 = require('./x509.js'); +var RSA = {}; + +/*global Promise*/ +var CSR = module.exports = function rsacsr(opts) { + // We're using a Promise here to be compatible with the browser version + // which will probably use the webcrypto API for some of the conversions + opts = CSR._prepare(opts); + + return CSR.create(opts).then(function (bytes) { + return CSR._encode(opts, bytes); + }); +}; + +CSR._prepare = function (opts) { + var Rasha; + opts = JSON.parse(JSON.stringify(opts)); + var pem, jwk; + + // We do a bit of extra error checking for user convenience + if (!opts) { throw new Error("You must pass options with key and domains to rsacsr"); } + if (!Array.isArray(opts.domains) || 0 === opts.domains.length) { + new Error("You must pass options.domains as a non-empty array"); + } + + // I need to check that 例.中国 is a valid domain name + if (!opts.domains.every(function (d) { + // allow punycode? xn-- + if ('string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/) { + return true; + } + })) { + throw new Error("You must pass options.domains as strings"); + } + + if (opts.pem) { + pem = opts.pem; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + if (!opts.key) { + throw new Error("You must pass options.key as a JSON web key"); + } else if (opts.key.kty) { + jwk = opts.key; + } else { + pem = opts.key; + } + } + + if (pem) { + try { + Rasha = require('rasha'); + } catch(e) { + throw new Error("Rasha.js is an optional dependency for PEM-to-JWK.\n" + + "Install it if you'd like to use it:\n" + + "\tnpm install --save rasha\n" + + "Otherwise supply a jwk as the private key." + ); + } + jwk = Rasha.importSync({ pem: pem }); + } + + opts.jwk = jwk; + return opts; +}; +CSR.sync = function (opts) { + opts = CSR._prepare(opts); + var bytes = CSR.createSync(opts); + return CSR._encode(opts, bytes); +}; +CSR._encode = function (opts, bytes) { + if ('der' === (opts.encoding||'').toLowerCase()) { + return bytes; + } + return PEM.packBlock({ + type: "CERTIFICATE REQUEST" + , bytes: bytes /* { jwk: jwk, domains: opts.domains } */ + }); +}; + +CSR.createSync = function createCsr(opts) { + var hex = CSR.request(opts.jwk, opts.domains); + var csr = CSR.signSync(opts.jwk, hex); + return Enc.hexToBuf(csr); +}; +CSR.create = function createCsr(opts) { + var hex = CSR.request(opts.jwk, opts.domains); + return CSR.sign(opts.jwk, hex).then(function (csr) { + return Enc.hexToBuf(csr); + }); +}; + +CSR.request = function createCsrBodyEc(jwk, domains) { + var asn1pub = X509.packCsrPublicKey(jwk); + return X509.packCsr(asn1pub, domains); +}; + +CSR.signSync = function csrEcSig(jwk, request) { + var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); + var sig = RSA.signSync(keypem, Enc.hexToBuf(request)); + return CSR.toDer({ request: request, signature: sig }); +}; +CSR.sign = function csrEcSig(jwk, request) { + var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); + return RSA.sign(keypem, Enc.hexToBuf(request)).then(function (sig) { + return CSR.toDer({ request: request, signature: sig }); + }); +}; +CSR.toDer = function encode(opts) { + var sty = ASN1('30' + // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1) + , ASN1('06', '2a864886f70d01010b') + , ASN1('05') + ); + return ASN1('30' + // The Full CSR Request Body + , opts.request + // The Signature Type + , sty + // The Signature + , ASN1.BitStr(Enc.bufToHex(opts.signature)) + ); +}; + +// +// RSA +// + +// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a +RSA.signSync = function signRsaSync(keypem, ab) { + // Signer is a stream + var sign = crypto.createSign('SHA256'); + sign.write(new Uint8Array(ab)); + sign.end(); + + // The signature is ASN1 encoded, as it turns out + var sig = sign.sign(keypem); + + // Convert to a JavaScript ArrayBuffer just because + return new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); +}; +RSA.sign = function signRsa(keypem, ab) { + return Promise.resolve().then(function () { + return RSA.signSync(keypem, ab); + }); +}; + + +X509.packCsrRsa = function (asn1pubkey, domains) { + return ASN1('30' + // Version (0) + , ASN1.UInt('00') + + // 2.5.4.3 commonName (X.520 DN component) + , ASN1('30', ASN1('31', ASN1('30', ASN1('06', '550403'), ASN1('0c', Enc.utf8ToHex(domains[0]))))) + + // Public Key (RSA or EC) + , asn1pubkey + + // Request Body + , ASN1('a0' + , ASN1('30' + // 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF) + , ASN1('06', '2a864886f70d01090e') + , ASN1('31' + , ASN1('30' + , ASN1('30' + // 2.5.29.17 subjectAltName (X.509 extension) + , ASN1('06', '551d11') + , ASN1('04' + , ASN1('30', domains.map(function (d) { + return ASN1('82', Enc.utf8ToHex(d)); + }).join('')))))))) + ); +}; + +X509.packPkcs1 = function (jwk) { + var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); + var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); + + if (!jwk.d) { + return Enc.hexToBuf(ASN1('30', n, e)); + } + + return Enc.hexToBuf(ASN1('30' + , ASN1.UInt('00') + , n + , e + , ASN1.UInt(Enc.base64ToHex(jwk.d)) + , ASN1.UInt(Enc.base64ToHex(jwk.p)) + , ASN1.UInt(Enc.base64ToHex(jwk.q)) + , ASN1.UInt(Enc.base64ToHex(jwk.dp)) + , ASN1.UInt(Enc.base64ToHex(jwk.dq)) + , ASN1.UInt(Enc.base64ToHex(jwk.qi)) + )); +}; + +X509.packCsrRsaPublicKey = function (jwk) { + // Sequence the key + var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); + var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); + var asn1pub = ASN1('30', n, e); + //var asn1pub = X509.packPkcs1({ kty: jwk.kty, n: jwk.n, e: jwk.e }); + + // Add the CSR pub key header + return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub)); +};