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 keyname = process.argv[2];
|
||||||
var domains = process.argv[3].split(/,/);
|
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
|
// Using error so that we can redirect stdout to file
|
||||||
//console.error("CN=" + domains[0]);
|
//console.error("CN=" + domains[0]);
|
||||||
//console.error("subjectAltName=" + domains.join(','));
|
//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';
|
'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
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
gencsr() {
|
gencsr2() {
|
||||||
keyfile=$1
|
keyfile=$1
|
||||||
domain=$2
|
domain=$2
|
||||||
csrfile=$3
|
csrfile=$3
|
||||||
@ -22,4 +23,65 @@ DNS.2 = www.$domain") \
|
|||||||
-out $csrfile
|
-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