AJ ONeal
6 years ago
10 changed files with 396 additions and 10 deletions
@ -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----- |
@ -1,2 +1,2 @@ |
|||
'use strict'; |
|||
module.exports = require('./lib/rsa-csr.js'); |
|||
module.exports = require('./lib/csr.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); |
|||
}; |
@ -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(); |
|||
}; |
@ -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"); |
|||
}; |
@ -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)); |
|||
}; |
Loading…
Reference in new issue