'use strict'; var crypto = require('crypto'); var fs = require('fs'); var pubkey = require('./pubkey.js'); // these are static ASN.1 segments // The head specifies that there will be 3 segments and a content length // (those segments will be content, signature header, and signature) var csrHead = '30 82 {0seq0len}'.replace(/\s+/g, ''); // The tail specifies the RSA256 signature header (and is followed by the signature var csrRsaFoot = ( '30 0D' // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1) + '06 09 2A 86 48 86 F7 0D 01 01 0B' + '05 00' + '03 82 01 01 00' // preamble to sig ).replace(/\s+/g, ''); var csrEcFoot = ( '30 0A' // 1.2.840.10045.4.3.2 ecdsaWithSHA256 // (ANSI X9.62 ECDSA algorithm with SHA256) + '06 08 2A 86 48 CE 3D 04 03 02' + '03 49 00' + '30 46' + '02 21 00 {s}' // 32 bytes of sig + '02 21 00 {r}' // 32 bytes of sig ).replace(/\s+/g, ''); var csrDomains = '82 {dlen} {domain.tld}'; // 2+n bytes (type 82?) /* var csrRsaContent = '30 82 {1.1.0seqlen}' // 7+n 4 bytes, sequence + '02 01 00' // 3 3 bytes, int 0 + '30 {3.2.0seqlen}' // 13+n 2 bytes, sequence + '31 {4.3.0setlen}' // 11+n 2 bytes, set + '30 {5.4.0seqlen}' // 9+n 2 bytes, sequence + '06 03 55 04 03' // 7+n 5 bytes, object id (commonName) + '0C {dlen} {domain.tld}' // 2+n 2+n bytes, utf8string + '30 82 {8.2.2seqlen}' // 19 4 bytes, sequence + '30 0D' // 15 2 bytes, sequence + '06 09 2A 86 48 86 F7 0D 01 01 01' // 13 11 bytes, rsaEncryption (PKCS #1) + '05 00' // 2 2 bytes, null (why?) + '03 82 {12.3.0bslen} 00' // +18+m 5 bytes, bit string [01 0F] + '30 82 {13.4.0seqlen}' // +13+m 4 bytes, sequence + '02 82 {klen} 00 {key}' // +9+m 4+1+n bytes, int (RSA Pub Key) + '02 03 {mod}' // +5 5 bytes, key and modules [01 00 01] + 'A0 {16.2.3ellen}' // 30+n 2 bytes, ?? [4B] + '30 {17.3.9seqlen}' // 28+n 2 bytes, sequence + '06 09 2A 86 48 86 F7 0D 01 09 0E' // 26+n 11 bytes, object id (extensionRequest (PKCS #9 via CRMF)) + '31 {19.5.0setlen}' // 15+n 2 bytes, set + '30 {20.6.0seqlen}' // 13+n 2 bytes, sequence + '30 {21.7.0seqlen}' // 11+n 2 bytes, sequence + '06 03 55 1D 11' // 9+n 5 bytes, object id (subjectAltName (X.509 extension)) + '04 {23.8.0octlen}' // 4+n 2 bytes, octet string + '30 {24.9.0seqlen}' // 2+n 2 bytes, sequence + '{altnames}' // n n bytes ; */ function privateToPub(pem) { var pubbuf; var pubxy; var key = pubkey.parsePem(pem); var pubder; if ('RSA' === key.typ) { if (key.pub) { pubbuf = pubkey.readPubkey(key.der); } else { pubbuf = pubkey.readPrivkey(key.der); } pubder = pubkey.toRsaPub(pubbuf); } else if ('EC' === key.typ) { if (key.crv && 'P-256' !== key.crv) { throw new Error("unsupported curve type"); } if (key.pub) { pubxy = pubkey.readEcPubkey(key.der); } else { pubxy = pubkey.readEcPrivkey(key.der); } pubder = pubkey.toEcPub(pubxy.x, pubxy.y); } //console.log(pubbuf.byteLength, pubkey.toHex(pubbuf)); var b64 = pubkey.toBase64(pubder); return pubkey.formatAsPublicPem(b64); } function strToHex(str) { return str.split('').map(function (ch) { var h = ch.charCodeAt(0).toString(16); if (2 === h.length) { return h; } return '0' + h; }).join(''); } function pubToPem(pubbuf) { var der = pubkey.toRsaPub(pubbuf); var b64 = pubkey.toBase64(der); return pubkey.formatAsPublicPem(b64); } function h(d) { d = d.toString(16); if (d.length % 2) { return '0' + d; } return d; } function fromHex(hex) { if ('undefined' !== typeof Buffer) { return Buffer.from(hex, 'hex'); } var ab = new ArrayBuffer(hex.length/2); var i; var j; ab = new Uint8Array(ab); for (i = 0, j = 0; i < (hex.length/2); i += 1) { ab[i] = parseInt(hex.slice(j, j+1), 16); j += 2; } return ab.buffer; } function createCsrBodyRsa(domains, csrpub) { var altnames = domains.map(function (d) { return csrDomains.replace(/{dlen}/, h(d.length)).replace(/{domain\.tld}/, strToHex(d)); }).join('').replace(/\s+/g, ''); var publen = csrpub.byteLength; var sublen = domains[0].length; var sanlen = (altnames.length/2); var body = [ '30 82 {1.1.0seqlen}' // 4 bytes, sequence .replace(/{[^}]+}/, h( 3 + 13 + sublen + 38 + publen // Length for RSA-related stuff + 30 + sanlen )) // #0 Total 3 , '02 01 00' // 3 bytes, int 0 // Subject // #1 Total 2+11+n , '30 {3.2.0seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(2+2+5+2+sublen)) , '31 {4.3.0setlen}' // 2 bytes, set .replace(/{[^}]+}/, h(2+5+2+sublen)) , '30 {5.4.0seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(5+2+sublen)) , '06 03 55 04 03' // 5 bytes, object id (commonName) , '0C {dlen} {domain.tld}' // 2+n bytes, utf8string .replace(/{dlen}/, h(sublen)) .replace(/{domain\.tld}/, strToHex(domains[0])) // Public Key // #2 Total 4+28+n+1+5 , '30 82 {8.2.2seqlen}' // 4 bytes, sequence .replace(/{[^}]+}/, h(2+11+2+4+1+4+4+publen+1+5)) , '30 0D' // 2 bytes, sequence , '06 09 2A 86 48 86 F7 0D 01 01 01' // 11 bytes, rsaEncryption (PKCS #1) , '05 00' // 2 bytes, null (why?) , '03 82 {12.3.0bslen} 00' // 4+1 bytes, bit string [01 0F] .replace(/{[^}]+}/, h(1+4+4+publen+1+5)) , '30 82 {13.4.0seqlen}' // 4 bytes, sequence .replace(/{[^}]+}/, h(4+publen+1+5)) , '02 82 {klen} 00 {key}' // 4+n bytes, int (RSA Pub Key) .replace(/{klen}/, h(publen+1)) .replace(/{key}/, pubkey.toHex(csrpub)) , '02 03 {mod}' // 5 bytes, key and modules [01 00 01] .replace(/{mod}/, '01 00 01') // AltNames // #3 Total 2+28+n , 'A0 {16.2.3ellen}' // 2 bytes, ?? [4B] .replace(/{[^}]+}/, h(2+11+2+2+2+5+2+2+sanlen)) , '30 {17.3.9seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(11+2+2+2+5+2+2+sanlen)) , '06 09 2A 86 48 86 F7 0D 01 09 0E' // 11 bytes, object id (extensionRequest (PKCS #9 via CRMF)) , '31 {19.5.0setlen}' // 2 bytes, set .replace(/{[^}]+}/, h(2+2+5+2+2+sanlen)) , '30 {20.6.0seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(2+5+2+2+sanlen)) , '30 {21.7.0seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(5+2+2+sanlen)) , '06 03 55 1D 11' // 5 bytes, object id (subjectAltName (X.509 extension)) , '04 {23.8.0octlen}' // 2 bytes, octet string .replace(/{[^}]+}/, h(2+sanlen)) , '30 {24.9.0seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(sanlen)) , '{altnames}' // n (elements of sequence) .replace(/{altnames}/, altnames) ]; body = body.join('').replace(/\s+/g, ''); return fromHex(body); } function createCsrBodyEc(domains, xy) { var altnames = domains.map(function (d) { return csrDomains.replace(/{dlen}/, h(d.length)).replace(/{domain\.tld}/, strToHex(d)); }).join('').replace(/\s+/g, ''); var sublen = domains[0].length; var sanlen = (altnames.length/2); 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 += pubkey.toHex(xy.x); if (xy.y) { hxy += pubkey.toHex(xy.y); } console.log('hxy:', hxy); var body = [ '30 81 {+85+n}' // 4 bytes, sequence .replace(/{[^}]+}/, h( 3 + 13 + sublen + 27 + publen // Length for EC-related P-256 stuff + 30 + sanlen )) // #0 Total 3 , '02 01 00' // 3 bytes, int 0 // Subject // #1 Total 2+11+n , '30 {3.2.0seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(2+2+5+2+sublen)) , '31 {4.3.0setlen}' // 2 bytes, set .replace(/{[^}]+}/, h(2+5+2+sublen)) , '30 {5.4.0seqlen}' // 2 bytes, sequence .replace(/{[^}]+}/, h(5+2+sublen)) , '06 03 55 04 03' // 5 bytes, object id (commonName) , '0C {dlen} {domain.tld}' // 2+n bytes, utf8string .replace(/{dlen}/, h(sublen)) .replace(/{domain\.tld}/, strToHex(domains[0])) // P-256 Public Key // #2 Total 2+25+xy , '30 {+25+xy}' // 2 bytes, sequence .replace(/{[^}]+}/, h(2+9+10+3+1+publen)) , '30 13' // 2 bytes, sequence // 1.2.840.10045.2.1 ecPublicKey // (ANSI X9.62 public key type) , '06 07 2A 86 48 CE 3D 02 01' // 9 bytes, object id // 1.2.840.10045.3.1.7 prime256v1 // (ANSI X9.62 named elliptic curve) , '06 08 2A 86 48 CE 3D 03 01 07' // 10 bytes, object id , '03 {xylen} 00 {xy}' // 3+1+n bytes .replace(/{xylen}/, h(publen+2)) .replace(/{xy}/, compression + hxy) // Altnames // #3 Total 2+28+n , 'A0 {+28}' // 2 bytes, ?? [4B] .replace(/{[^}]+}/, h(2+11+2+2+2+5+2+2+sanlen)) , '30 {+26}' // 2 bytes, sequence .replace(/{[^}]+}/, h(11+2+2+2+5+2+2+sanlen)) // (extensionRequest (PKCS #9 via CRMF)) , '06 09 2A 86 48 86 F7 0D 01 09 0E' // 11 bytes, object id , '31 {+13}' // 2 bytes, set .replace(/{[^}]+}/, h(2+2+5+2+2+sanlen)) , '30 {+11}' // 2 bytes, sequence .replace(/{[^}]+}/, h(2+5+2+2+sanlen)) , '30 {+9}' // 2 bytes, sequence .replace(/{[^}]+}/, h(5+2+2+sanlen)) // (subjectAltName (X.509 extension)) , '06 03 55 1D 11' // 5 bytes, object id , '04 {+2}' // 2 bytes, octet string .replace(/{[^}]+}/, h(2+sanlen)) , '30 {+n}' // 2 bytes, sequence .replace(/{[^}]+}/, h(sanlen)) , '{altnames}' // n (elements of sequence) .replace(/{altnames}/, altnames) ]; body = body.join('').replace(/\s+/g, ''); return fromHex(body); } // https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a function signEc(keypem, ab) { var sign = crypto.createSign('SHA256'); sign.write(new Uint8Array(ab)); sign.end(); var sig = sign.sign(keypem); console.log(""); console.log("SIGNING!!"); console.log('csr body:', pubkey.toHex(new Uint8Array(ab))); console.log('ec sig:', pubkey.toHex(sig)); console.log(""); sig = new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); var rlen = new Uint8Array(sig)[3]; var r = sig.slice(4, 4 + rlen); var slen = sig.slice[5 + rlen]; var s = sig.slice(6 + rlen, 6 + rlen + slen); // not sure what this is all about... while(r[0] === 0) { r = r.slice(1); } while(s[0] === 0) { s = s.slice(1); } return { r: r, s: s }; } function createRsaCsr(domains, keypem, csrpub) { // TODO get pub from priv var body = createCsrBodyRsa(domains, csrpub); var sign = crypto.createSign('SHA256'); sign.write(new Uint8Array(body)); sign.end(); var sig = sign.sign(keypem); var len = body.byteLength + (csrRsaFoot.length/2) + sig.byteLength; console.log('headlen', h(len)); var head = csrHead.replace(/{[^}]+}/, h(len)); var ab = new Uint8Array(new ArrayBuffer(4 + len)); var i = 0; fromHex(head).forEach(function (b) { ab[i] = b; i += 1; }); body.forEach(function (b) { ab[i] = b; i += 1; }); fromHex(csrRsaFoot).forEach(function (b) { ab[i] = b; i += 1; }); new Uint8Array(sig).forEach(function (b) { ab[i] = b; i += 1; }); var pem = pubkey.formatAsPem(pubkey.toBase64(ab)); return pem; } function createEcCsr(domains, keypem, pubj) { // TODO get pub from priv console.log('[TRACE] createEcCsr'); var body = createCsrBodyEc(domains, pubj); console.log('csr body:', pubkey.toHex(body)) var sig = signEc(keypem, body); //var siglen = sig.s.byteLength + sig.r.byteLength; console.log('r:', pubkey.toHex(sig.r)); console.log('s:', pubkey.toHex(sig.s)); // XXX TODO this is junk, not real var foot = csrEcFoot .replace(/{s}/, pubkey.toHex(sig.s)) .replace(/{r}/, pubkey.toHex(sig.r)) ; var len = body.byteLength + (foot.length/2) + sig.r.byteLength + sig.s.byteLength; console.log('headlen', h(len)); var head = csrHead.replace(/{[^}]+}/, h(len)); var ab = new Uint8Array(new ArrayBuffer(4 + len)); var i = 0; fromHex(head).forEach(function (b) { ab[i] = b; i += 1; }); body.forEach(function (b) { ab[i] = b; i += 1; }); fromHex(foot).forEach(function (b) { ab[i] = b; i += 1; }); new Uint8Array(sig).forEach(function (b) { ab[i] = b; i += 1; }); var pem = pubkey.formatAsPem(pubkey.toBase64(ab)); return pem; } /* function signagain(csrbody) { var longEnough = csrbody.byteLength > 256; if (!longEnough) { return false; } var domains = [ 'example.com', 'www.example.com', 'api.example.com' ]; var body = createCsrBodyRsa(domains); var sign = crypto.createSign('SHA256'); sign.write(new Uint8Array(body)); sign.end(); var sig2 = sign.sign(keypem); var hexsig = pubkey.toHex(sig); var hexsig2 = pubkey.toHex(sig2); console.log('hexsig:'); console.log(hexsig); if (hexsig2 !== hexsig) { throw new Error("sigs didn't match"); } console.log('Winner winner!', csrbody.byteLength); console.log(pubkey.toHex(csrbody)); console.log(); console.log('Test:', body.byteLength); console.log(pubkey.toHex(body)); console.log(); var len = body.byteLength + (csrRsaFoot.length/2) + sig.byteLength; console.log('headlen', h(len)); var head = csrHead.replace(/{[^}]+}/, h(len)); var ab = new Uint8Array(new ArrayBuffer(4 + len)); var i = 0; fromHex(head).forEach(function (b) { ab[i] = b; i += 1; }); body.forEach(function (b) { ab[i] = b; i += 1; }); fromHex(csrRsaFoot).forEach(function (b) { ab[i] = b; i += 1; }); new Uint8Array(sig2).forEach(function (b) { ab[i] = b; i += 1; }); console.log("Whole enchilada:", pubkey.toHex(ab.buffer) === pubkey.toHex(csrFull)); console.log(pubkey.toHex(ab.buffer)); console.log(); // subject + 2-byte altname headers + altnames themselves + public key //var extralen = domains[0].length + (domains.length * 2) + domains.join('').length + csrpub.byteLength; //var foot = csrRsaFoot.replace(/xxxx/g, (csrpub.byteLength + 1)); //var head = csrHead.replace(/xxxx/); return false; } */ function check(domains, keypem, csrFull) { var pemblock = pubkey.parsePem(keypem); if ('EC' === pemblock.typ) { console.log("EC TIME:"); console.log(pemblock); checkEc(domains, keypem, pemblock, csrFull); } else if ('RSA' === pemblock.typ) { console.log("RSA TIME!"); checkRsa(domains, keypem, pemblock, csrFull); } else { // TODO try throw new Error("unknown key type"); } } // TODO function checkEc(domains, keypem, pemblock, csrFull) { var bodyend = (csrFull.byteLength - (3+2+2+32+2+32)); var sig = csrFull.slice(bodyend); console.log(); console.log(); console.log('csr (' + csrFull.byteLength + ')'); console.log(pubkey.toHex(csrFull)); console.log(); // First 4 bytes define Segment, segment length, and content length var csrbody = csrFull.slice(4, bodyend); console.log(); console.log('csr body (' + csrbody.byteLength + ')'); console.log(pubkey.toHex(csrbody)); console.log(); var cnend = 21 + 2 + domains[0].length + 2 + 2 + 7 + 8; var csrpub = csrFull.slice(cnend, cnend+2+89); console.log('csr pub (' + csrpub.byteLength + ')'); console.log(pubkey.toHex(csrpub)); console.log(); console.log('sig (' + sig.byteLength + ')'); console.log(pubkey.toHex(sig)); console.log(); /* var csrpem = pubToPem(csrpub); console.log(csrpem); console.log(); var prvpem = privateToPub(keypem); console.log(prvpem); console.log(); if (csrpem === prvpem) { console.log("Public Keys Match"); } else { throw new Error("public key read from keyfile doesn't match public key read from CSR"); } */ var pubj = pubkey.readEcPubkey(pemblock.der); console.log('pubj:', pubj); var pem = createEcCsr(domains, keypem, pubj, csrpub); console.log('EC CSR PEM:'); console.log(pem); return; } function checkRsa(domains, keypem, pubj, csrFull) { var sigend = (csrFull.byteLength - (2048 / 8)); var sig = csrFull.slice(sigend); console.log(); console.log(); console.log('csr (' + csrFull.byteLength + ')'); console.log(pubkey.toHex(csrFull)); console.log(); // First 4 bytes define Segment, segment length, and content length console.log(sigend, csrRsaFoot, csrRsaFoot.length/2); var csrbody = csrFull.slice(4, sigend - (csrRsaFoot.length/2)); console.log(pubkey.toHex(csrbody.slice(0, csrbody.byteLength - sig.byteLength))); console.log(); console.log('csr body (' + csrbody.byteLength + ')'); console.log(pubkey.toHex(csrbody)); console.log(); var csrpub = csrFull.slice(63 + 5, 63 + 5 + 256); console.log('csr pub (' + csrpub.byteLength + ')'); console.log(pubkey.toHex(csrpub)); console.log(); console.log('sig (' + sig.byteLength + ')'); console.log(pubkey.toHex(sig)); console.log(); var csrpem = pubToPem(csrpub); console.log(csrpem); console.log(); var prvpem = privateToPub(keypem); console.log(prvpem); console.log(); if (csrpem === prvpem) { console.log("Public Keys Match"); } else { throw new Error("public key read from keyfile doesn't match public key read from CSR"); } var pem = createRsaCsr(domains, keypem, csrpub); console.log('RSA CSR PEM:'); console.log(pem); return; } var keyname = process.argv[2]; var dername = process.argv[3]; var keypem = fs.readFileSync(keyname, 'ascii'); var csrFull = fs.readFileSync(dername); var csrFull = csrFull.buffer.slice(csrFull.byteOffset, csrFull.byteOffset + csrFull.byteLength); console.log(); console.log("CSR"); console.log(pubkey.toHex(csrFull)); console.log(); //check([ 'whatever.net', 'api.whatever.net' ], keypem, csrFull); check([ 'example.com', 'www.example.com', 'api.example.com' ], keypem, csrFull);