WIP Building out all features necessary for Let's Encrypt #6
|
@ -0,0 +1,157 @@
|
||||||
|
// 1.2.840.10045.3.1.7
|
||||||
|
// prime256v1 (ANSI X9.62 named elliptic curve)
|
||||||
|
var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase();
|
||||||
|
// 1.3.132.0.34
|
||||||
|
// secp384r1 (SECG (Certicom) named elliptic curve)
|
||||||
|
var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase();
|
||||||
|
|
||||||
|
var ECDSACSR = {};
|
||||||
|
var ECDSA = {};
|
||||||
|
var DER = {};
|
||||||
|
var PEM = {};
|
||||||
|
var ASN1;
|
||||||
|
var Hex = {};
|
||||||
|
var AB = {};
|
||||||
|
|
||||||
|
//
|
||||||
|
// CSR - the main event
|
||||||
|
//
|
||||||
|
|
||||||
|
ECDSACSR.create = function createEcCsr(keypem, domains) {
|
||||||
|
var pemblock = PEM.parseBlock(keypem);
|
||||||
|
var ecpub = PEM.parseEcPubkey(pemblock.der);
|
||||||
|
var request = ECDSACSR.request(ecpub, domains);
|
||||||
|
return AB.fromHex(ECDSACSR.sign(keypem, request));
|
||||||
|
};
|
||||||
|
|
||||||
|
ECDSACSR.request = function createCsrBodyEc(xy, domains) {
|
||||||
|
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 += Hex.fromAB(xy.x);
|
||||||
|
if (xy.y) { hxy += Hex.fromAB(xy.y); }
|
||||||
|
|
||||||
|
// Sorry for the mess, but it is what it is
|
||||||
|
return ASN1('30'
|
||||||
|
|
||||||
|
// Version (0)
|
||||||
|
, ASN1.UInt('00')
|
||||||
|
|
||||||
|
// CN / Subject
|
||||||
|
, ASN1('30'
|
||||||
|
, ASN1('31'
|
||||||
|
, ASN1('30'
|
||||||
|
// object id (commonName)
|
||||||
|
, ASN1('06', '55 04 03')
|
||||||
|
, ASN1('0C', Hex.fromString(domains[0])))))
|
||||||
|
|
||||||
|
// EC P-256 Public Key
|
||||||
|
, ASN1('30'
|
||||||
|
, ASN1('30'
|
||||||
|
// 1.2.840.10045.2.1 ecPublicKey
|
||||||
|
// (ANSI X9.62 public key type)
|
||||||
|
, ASN1('06', '2A 86 48 CE 3D 02 01')
|
||||||
|
// 1.2.840.10045.3.1.7 prime256v1
|
||||||
|
// (ANSI X9.62 named elliptic curve)
|
||||||
|
, ASN1('06', '2A 86 48 CE 3D 03 01 07')
|
||||||
|
)
|
||||||
|
, ASN1.BitStr(compression + hxy))
|
||||||
|
|
||||||
|
// CSR Extension Subject Alternative Names
|
||||||
|
, ASN1('A0'
|
||||||
|
, ASN1('30'
|
||||||
|
// (extensionRequest (PKCS #9 via CRMF))
|
||||||
|
, ASN1('06', '2A 86 48 86 F7 0D 01 09 0E')
|
||||||
|
, ASN1('31'
|
||||||
|
, ASN1('30'
|
||||||
|
, ASN1('30'
|
||||||
|
// (subjectAltName (X.509 extension))
|
||||||
|
, ASN1('06', '55 1D 11')
|
||||||
|
, ASN1('04'
|
||||||
|
, ASN1('30', domains.map(function (d) {
|
||||||
|
return ASN1('82', Hex.fromString(d));
|
||||||
|
}).join(''))))))))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ECDSACSR.sign = function csrEcSig(keypem, request) {
|
||||||
|
var sig = ECDSA.sign(keypem, AB.fromHex(request));
|
||||||
|
var rLen = sig.r.byteLength;
|
||||||
|
var rc = '';
|
||||||
|
var sLen = sig.s.byteLength;
|
||||||
|
var sc = '';
|
||||||
|
|
||||||
|
if (0x80 & new Uint8Array(sig.r)[0]) { rc = '00'; rLen += 1; }
|
||||||
|
if (0x80 & new Uint8Array(sig.s)[0]) { sc = '00'; sLen += 1; }
|
||||||
|
|
||||||
|
return ASN1('30'
|
||||||
|
// The Full CSR Request Body
|
||||||
|
, request
|
||||||
|
|
||||||
|
// The Signature Type
|
||||||
|
, ASN1('30'
|
||||||
|
// 1.2.840.10045.4.3.2 ecdsaWithSHA256
|
||||||
|
// (ANSI X9.62 ECDSA algorithm with SHA256)
|
||||||
|
, ASN1('06', '2A 86 48 CE 3D 04 03 02')
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Signature, embedded in a Bit Stream
|
||||||
|
, ASN1.BitStr(
|
||||||
|
// As far as I can tell this is a completely separate ASN.1 structure
|
||||||
|
// that just so happens to be embedded in a Bit String of another ASN.1
|
||||||
|
ASN1('30'
|
||||||
|
, ASN1.UInt(Hex.fromAB(sig.r))
|
||||||
|
, ASN1.UInt(Hex.fromAB(sig.s))))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// ECDSA
|
||||||
|
//
|
||||||
|
|
||||||
|
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
||||||
|
ECDSA.sign = function signEc(keypem, ab) {
|
||||||
|
// Signer is a stream
|
||||||
|
var sign = crypto.createSign('SHA256');
|
||||||
|
sign.write(new Uint8Array(ab));
|
||||||
|
sign.end();
|
||||||
|
|
||||||
|
// The signature is ASN1 encoded
|
||||||
|
var sig = sign.sign(keypem);
|
||||||
|
|
||||||
|
// Convert to a JavaScript ArrayBuffer just because
|
||||||
|
sig = new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength));
|
||||||
|
|
||||||
|
// The first two bytes '30 xx' signify SEQUENCE and LENGTH
|
||||||
|
// The sequence length byte will be a single byte because the signature is less that 128 bytes (0x80, 1024-bit)
|
||||||
|
// (this would not be true for P-521, but I'm not supporting that yet)
|
||||||
|
// The 3rd byte will be '02', signifying INTEGER
|
||||||
|
// The 4th byte will tell us the length of 'r' (which, on occassion, will be less than the full 255 bytes)
|
||||||
|
var rIndex = 3;
|
||||||
|
var rLen = sig[rIndex];
|
||||||
|
var rEnd = rIndex + 1 + rLen;
|
||||||
|
var sIndex = rEnd + 1;
|
||||||
|
var sLen = sig[sIndex];
|
||||||
|
var sEnd = sIndex + 1 + sLen;
|
||||||
|
var r = sig.slice(rIndex + 1, rEnd);
|
||||||
|
var s = sig.slice(sIndex + 1, sEnd); // this should be end-of-file
|
||||||
|
|
||||||
|
// ASN1 INTEGER types use the high-order bit to signify a negative number,
|
||||||
|
// hence a leading '00' is used for numbers that begin with '80' or greater
|
||||||
|
// which is why r length is sometimes a byte longer than its bit length
|
||||||
|
if (0 === s[0]) { s = s.slice(1); }
|
||||||
|
if (0 === r[0]) { r = r.slice(1); }
|
||||||
|
|
||||||
|
return { raw: sig.buffer, r: r.buffer, s: s.buffer };
|
||||||
|
};
|
|
@ -0,0 +1,213 @@
|
||||||
|
'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
|
||||||
|
opts = CSR._prepare(opts);
|
||||||
|
|
||||||
|
return CSR.create(opts).then(function (bytes) {
|
||||||
|
return CSR._encode(opts, bytes);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
CSR._prepare = function (opts) {
|
||||||
|
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 opts;
|
||||||
|
};
|
||||||
|
CSR.sync = function (opts) {
|
||||||
|
opts = CSR._prepare(opts);
|
||||||
|
var bytes = CSR.createSync(opts);
|
||||||
|
return CSR._encode(opts, bytes);
|
||||||
|
};
|
||||||
|
CSR._encode = function (opts, bytes) {
|
||||||
|
if ('der' === (opts.encoding||'').toLowerCase()) {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
return PEM.packBlock({
|
||||||
|
type: "CERTIFICATE REQUEST"
|
||||||
|
, bytes: bytes /* { jwk: jwk, domains: opts.domains } */
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
CSR.createSync = function createCsr(opts) {
|
||||||
|
var hex = CSR.request(opts.jwk, opts.domains);
|
||||||
|
var csr = CSR.signSync(opts.jwk, hex);
|
||||||
|
return Enc.hexToBuf(csr);
|
||||||
|
};
|
||||||
|
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.signSync = function csrEcSig(jwk, request) {
|
||||||
|
var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) });
|
||||||
|
var sig = RSA.signSync(keypem, Enc.hexToBuf(request));
|
||||||
|
return CSR.toDer({ request: request, signature: sig });
|
||||||
|
};
|
||||||
|
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) {
|
||||||
|
return CSR.toDer({ request: request, signature: sig });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
CSR.toDer = function encode(opts) {
|
||||||
|
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
|
||||||
|
, opts.request
|
||||||
|
// The Signature Type
|
||||||
|
, sty
|
||||||
|
// The Signature
|
||||||
|
, ASN1.BitStr(Enc.bufToHex(opts.signature))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// RSA
|
||||||
|
//
|
||||||
|
|
||||||
|
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
||||||
|
RSA.signSync = function signRsaSync(keypem, ab) {
|
||||||
|
// 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));
|
||||||
|
};
|
||||||
|
RSA.sign = function signRsa(keypem, ab) {
|
||||||
|
return Promise.resolve().then(function () {
|
||||||
|
return RSA.signSync(keypem, ab);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
X509.packCsrRsa = 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.packCsrRsaPublicKey = 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