v0.7.0: it's working! it's working! (and tested)
This commit is contained in:
parent
b785b30623
commit
6b1860dfb7
@ -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
Normal file
17
fixtures/whatever.net-www-api.csr.pem
Normal file
@ -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
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
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
Normal file
122
lib/csr.js
Normal file
@ -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));
|
||||
});
|
||||
};
|
@ -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
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 + '-----'
|
||||
;
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function rsacsr(opts) {
|
||||
throw new Error("not implemented yet");
|
||||
};
|
66
lib/x509.js
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
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…
x
Reference in New Issue
Block a user