Browse Source

v0.7.0: it's working! it's working! (and tested)

master
AJ ONeal 6 years ago
parent
commit
6b1860dfb7
  1. 10
      bin/rsa-csr.js
  2. 17
      fixtures/whatever.net-www-api.csr.pem
  3. 2
      index.js
  4. 72
      lib/asn1.js
  5. 122
      lib/csr.js
  6. 34
      lib/encoding.js
  7. 12
      lib/pem.js
  8. 5
      lib/rsa-csr.js
  9. 66
      lib/x509.js
  10. 66
      test.sh

10
bin/rsa-csr.js

@ -7,9 +7,15 @@ var rsacsr = require('../index.js');
var keyname = process.argv[2];
var domains = process.argv[3].split(/,/);
var keypem = fs.readFileSync(keyname, 'ascii');
var key = fs.readFileSync(keyname, 'ascii');
rsacsr({ key: keypem, domains: domains }).then(function (csr) {
try {
key = JSON.parse(key);
} catch(e) {
// ignore
}
rsacsr({ key: key, domains: domains }).then(function (csr) {
// Using error so that we can redirect stdout to file
//console.error("CN=" + domains[0]);
//console.error("subjectAltName=" + domains.join(','));

17
fixtures/whatever.net-www-api.csr.pem

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICqjCCAZICAQAwFzEVMBMGA1UEAwwMd2hhdGV2ZXIubmV0MIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrD
wtCohHzLxGhDNzUJefLukC+xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLIC
OFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjs
kocn2BOnTB57qAZM6+I70on0/iDZm7+jcqOPgADAmbWHhy67BXkk4yy/YzD4yOGZ
FXZcNp915/TW5bRd//AKPHUHxJasPiyEFqlNKBR2DSD+LbX5eTmzCh2ikrwTMja7
mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQABoE4wTAYJ
KoZIhvcNAQkOMT8wPTA7BgNVHREENDAyggx3aGF0ZXZlci5uZXSCEHd3dy53aGF0
ZXZlci5uZXSCEGFwaS53aGF0ZXZlci5uZXQwDQYJKoZIhvcNAQELBQADggEBAB21
KZYjarfd8nUAbwhH8dWZOo4rFcdYFo3xcXPQ11b1Wa79dtG67cgD/dplKFis5qD3
6h4m818w9ESBA3Q1ZUy6HgDPMhCjg2fmCnSsZ5epo47wzvelYonfOX5DAwxgfYsa
335olrXJ0qsTiNmaS7RxDT53vfMOp41NyEAkFmpIAkaHgW/+xFPUSCBXIUWbaCG+
pK3FVNmK3VCVCAP6UvVKYQUWSC6FRG/Q8MHoecdo+bbMlr2s2GPxq9TKInwe8JqT
E9pD7QMsN7uWpMaXNKCje4+Q88Br4URNcGAiYoy4/6hcF2Ki1saTYVIk/DG1P4hX
G5f0ezDLtsC22xe6jHI=
-----END CERTIFICATE REQUEST-----

2
index.js

@ -1,2 +1,2 @@
'use strict';
module.exports = require('./lib/rsa-csr.js');
module.exports = require('./lib/csr.js');

72
lib/asn1.js

@ -0,0 +1,72 @@
'use strict';
//
// A dumbed-down, minimal ASN.1 parser / packer combo
//
// Note: generally I like to write congruent code
// (i.e. output can be used as input and vice-versa)
// However, this seemed to be more readable and easier
// to use written as-is, asymmetrically.
// (I also generally prefer to export objects rather
// functions but, yet again, asthetics one in this case)
var Enc = require('./encoding.js');
//
// Packer
//
// Almost every ASN.1 type that's important for CSR
// can be represented generically with only a few rules.
var ASN1 = module.exports = function ASN1(/*type, hexstrings...*/) {
var args = Array.prototype.slice.call(arguments);
var typ = args.shift();
var str = args.join('').replace(/\s+/g, '').toLowerCase();
var len = (str.length/2);
var lenlen = 0;
var hex = typ;
// We can't have an odd number of hex chars
if (len !== Math.round(len)) {
console.error(arguments);
throw new Error("invalid hex");
}
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
// The second byte is either the size of the value, or the size of its size
// 1. If the second byte is < 0x80 (128) it is considered the size
// 2. If it is > 0x80 then it describes the number of bytes of the size
// ex: 0x82 means the next 2 bytes describe the size of the value
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
if (len > 127) {
lenlen += 1;
while (len > 255) {
lenlen += 1;
len = len >> 8;
}
}
if (lenlen) { hex += Enc.numToHex(0x80 + lenlen); }
return hex + Enc.numToHex(str.length/2) + str;
};
// The Integer type has some special rules
ASN1.UInt = function UINT() {
var str = Array.prototype.slice.call(arguments).join('');
var first = parseInt(str.slice(0, 2), 16);
// If the first byte is 0x80 or greater, the number is considered negative
// Therefore we add a '00' prefix if the 0x80 bit is set
if (0x80 & first) { str = '00' + str; }
return ASN1('02', str);
};
// The Bit String type also has a special rule
ASN1.BitStr = function BITSTR() {
var str = Array.prototype.slice.call(arguments).join('');
// '00' is a mask of how many bits of the next byte to ignore
return ASN1('03', '00' + str);
};

122
lib/csr.js

@ -0,0 +1,122 @@
'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
return Promise.resolve().then(function () {
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 CSR.create(opts).then(function (bytes) {
return PEM.packBlock({
type: "CERTIFICATE REQUEST"
, bytes: bytes /* { jwk: jwk, domains: opts.domains } */
});
});
});
};
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.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) {
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
, request
// The Signature Type
, sty
// The Signature
, ASN1.BitStr(Enc.bufToHex(sig))
);
});
};
//
// RSA
//
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
RSA.sign = function signRsa(keypem, ab) {
return Promise.resolve().then(function () {
// 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));
});
};

34
lib/encoding.js

@ -0,0 +1,34 @@
'use strict';
var Enc = module.exports;
Enc.base64ToHex = function base64ToHex(b64) {
return Buffer.from(b64, 'base64').toString('hex').toLowerCase();
};
Enc.bufToBase64 = function bufToBase64(u8) {
// we want to maintain api compatability with browser APIs,
// so we assume that this could be a Uint8Array
return Buffer.from(u8).toString('base64');
};
Enc.bufToHex = function toHex(u8) {
return Buffer.from(u8).toString('hex').toLowerCase();
};
Enc.hexToBuf = function (hex) {
return Buffer.from(hex, 'hex');
};
Enc.numToHex = function numToHex(d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
}
return d;
};
Enc.utf8ToHex = function utf8ToHex(str) {
// node can properly handle utf-8 strings
return Buffer.from(str).toString('hex').toLowerCase();
};

12
lib/pem.js

@ -0,0 +1,12 @@
'use strict';
var Enc = require('./encoding.js')
var PEM = module.exports;
PEM.packBlock = function (opts) {
// TODO allow for headers?
return '-----BEGIN ' + opts.type + '-----\n'
+ Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----'
;
};

5
lib/rsa-csr.js

@ -1,5 +0,0 @@
'use strict';
module.exports = function rsacsr(opts) {
throw new Error("not implemented yet");
};

66
lib/x509.js

@ -0,0 +1,66 @@
'use strict';
var ASN1 = require('./asn1.js');
var Enc = require('./encoding.js');
var X509 = module.exports;
X509.packCsr = 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.packCsrPublicKey = 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));
};

66
test.sh

@ -1,6 +1,7 @@
#!/bin/bash
set -e
gencsr() {
gencsr2() {
keyfile=$1
domain=$2
csrfile=$3
@ -22,4 +23,65 @@ DNS.2 = www.$domain") \
-out $csrfile
}
gencsr fixtures/privkey-rsa-2048.pkcs1.pem example.com fixtures/example.com-www.csr.pem
gencsr3() {
keyfile=$1
domain=$2
csrfile=$3
openssl req -key $keyfile -new -nodes \
-config <(printf "[req]
prompt = no
req_extensions = req_ext
distinguished_name = dn
[ dn ]
CN = $domain
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = $domain
DNS.2 = www.$domain
DNS.3 = api.$domain") \
-out $csrfile
}
rndcsr() {
keysize=$1
openssl genrsa -out fixtures/valid.pkcs1.1.pem $keysize
rasha fixtures/valid.pkcs1.1.pem > fixtures/test.jwk.1.json
gencsr3 fixtures/valid.pkcs1.1.pem whatever.net fixtures/valid.csr.1.pem
node bin/rsa-csr.js fixtures/test.jwk.1.json whatever.net,www.whatever.net,api.whatever.net \
> fixtures/test.csr.1.pem
diff fixtures/valid.csr.1.pem fixtures/test.csr.1.pem
}
echo ""
echo "Generating CSR for example.com,www.example.com"
gencsr2 fixtures/privkey-rsa-2048.pkcs1.pem example.com fixtures/example.com-www.csr.pem
node bin/rsa-csr.js fixtures/privkey-rsa-2048.jwk.json example.com,www.example.com \
> fixtures/example.com-www.csr.1.pem
diff fixtures/example.com-www.csr.pem fixtures/example.com-www.csr.1.pem
echo "Pass"
echo ""
echo "Generating CSR for whatever.net,www.whatever.net,api.whatever.net"
gencsr3 fixtures/privkey-rsa-2048.pkcs1.pem whatever.net fixtures/whatever.net-www-api.csr.pem
node bin/rsa-csr.js fixtures/privkey-rsa-2048.jwk.json whatever.net,www.whatever.net,api.whatever.net \
> fixtures/whatever.net-www-api.csr.1.pem
diff fixtures/whatever.net-www-api.csr.pem fixtures/whatever.net-www-api.csr.1.pem
echo "Pass"
echo ""
echo "Generating random keys of various lengths and re-running tests for each"
rndcsr 3072
rndcsr 1024
rndcsr 512 # minimum size that can reasonably work
echo "Pass"
rm fixtures/*.1.*
echo ""
echo "All tests passed!"
echo " • Fixture CSRs built and do not differ from OpenSSL-generated CSRs"
echo " • Random keys and CSRs are also correct"

Loading…
Cancel
Save