// 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 }; };