mirror of
https://git.coolaj86.com/coolaj86/csr.js.git
synced 2025-03-13 04:00:46 +00:00
v0.8.0: generate CSR with native JavaScript
This commit is contained in:
parent
609ba9a62f
commit
22b752507f
47
README.md
47
README.md
@ -1,3 +1,46 @@
|
||||
# csr.js
|
||||
# @root/csr
|
||||
|
||||
Lightweight, Zero-Dependency CSR (Certificate Signing Request) generator and parser for Node.js and Browsers
|
||||
Lightweight, Zero-Dependency CSR (Certificate Signing Request) generator and parser for Node.js and Browsers
|
||||
|
||||
# Usage
|
||||
|
||||
```js
|
||||
var CSR = require('@root/csr');
|
||||
var PEM = require('@root/pem/packer');
|
||||
|
||||
CSR.csr({
|
||||
jwk: jwk,
|
||||
domains: ['example.com', '*.example.com', 'foo.bar.example.com'],
|
||||
encoding: 'pem'
|
||||
}).then(function(der) {
|
||||
var csr = PEM.packBlock({ type: 'CERTIFICATE REQUEST', bytes: der });
|
||||
console.log(csr);
|
||||
});
|
||||
```
|
||||
|
||||
```txt
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBHjCBxQIBADAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
|
||||
CCqGSM49AwEHA0IABFL897BlwE6Tmco/r7LpwVL2BdDx12zZr+BnA/0/PjkI0lsu
|
||||
013u1+X5fe6vKnOIjcb5obaFnSQixuMGu3qcVnmgTTBLBgkqhkiG9w0BCQ4xPjA8
|
||||
MDoGA1UdEQQzMDGCC2V4YW1wbGUuY29tgg0qLmV4YW1wbGUuY29tghNmb28uYmFy
|
||||
LmV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0gAMEUCIADRCWsMYBjm70Hqi08QrOcR
|
||||
Gcz8uJTe7vZwqOGtykWiAiEA1FTbMskZR9w2ugFWXkWfBdb1W6cD2v6nK+J0wj2r
|
||||
Q48=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
```
|
||||
|
||||
# Advanced Usage
|
||||
|
||||
Create an unsigned request
|
||||
|
||||
```
|
||||
var CSR = require('@root/csr');
|
||||
|
||||
// Note: this requires the public key to embed it in the request
|
||||
var hex = CSR.request({
|
||||
jwk: jwk,
|
||||
domains: ['example.com', '*.example.com', 'foo.bar.example.com'],
|
||||
encoding: 'hex'
|
||||
})
|
||||
```
|
||||
|
335
csr.js
335
csr.js
@ -1,3 +1,336 @@
|
||||
// Copyright 2018-present AJ ONeal. All rights reserved
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use strict';
|
||||
/*global Promise*/
|
||||
|
||||
module.exports = require('@root/acme/csr');
|
||||
var Enc = require('@root/encoding');
|
||||
|
||||
var ASN1 = require('@root/asn1/packer'); // DER, actually
|
||||
var Asn1 = ASN1.Any;
|
||||
var BitStr = ASN1.BitStr;
|
||||
var UInt = ASN1.UInt;
|
||||
var Asn1Parser = require('@root/asn1/parser');
|
||||
var PEM = require('@root/pem');
|
||||
var X509 = require('@root/x509');
|
||||
// TODO @root/keypairs/sign
|
||||
var Keypairs = require('@root/keypairs');
|
||||
|
||||
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
|
||||
var CSR = module.exports;
|
||||
|
||||
// { jwk, domains }
|
||||
CSR.csr = function(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 CSR._prepare(opts).then(function(opts) {
|
||||
return CSR.create(opts).then(function(bytes) {
|
||||
return CSR._encode(opts, bytes);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
CSR._prepare = function(opts) {
|
||||
return Promise.resolve().then(function() {
|
||||
opts = JSON.parse(JSON.stringify(opts));
|
||||
|
||||
// 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.jwk) {
|
||||
return opts;
|
||||
}
|
||||
if (opts.key && opts.key.kty) {
|
||||
opts.jwk = opts.key;
|
||||
return opts;
|
||||
}
|
||||
if (!opts.pem && !opts.key) {
|
||||
throw new Error('You must pass options.key as a JSON web key');
|
||||
}
|
||||
|
||||
return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
|
||||
pair
|
||||
) {
|
||||
opts.jwk = pair.private;
|
||||
return opts;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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 } */
|
||||
});
|
||||
};
|
||||
|
||||
// { jwk, domains }
|
||||
CSR.create = function createCsr(opts) {
|
||||
var hex = CSR.request({
|
||||
jwk: opts.jwk,
|
||||
domains: opts.domains,
|
||||
encoding: 'hex'
|
||||
});
|
||||
return CSR._sign(opts.jwk, hex).then(function(csr) {
|
||||
return Enc.hexToBuf(csr);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// EC / RSA
|
||||
//
|
||||
// { jwk, domains }
|
||||
CSR.request = function createCsrBody(opts) {
|
||||
var asn1pub;
|
||||
if (/^EC/i.test(opts.jwk.kty)) {
|
||||
asn1pub = X509.packCsrEcPublicKey(opts.jwk);
|
||||
} else {
|
||||
asn1pub = X509.packCsrRsaPublicKey(opts.jwk);
|
||||
}
|
||||
var hex = X509.packCsr(asn1pub, opts.domains);
|
||||
if ('hex' === opts.encoding) {
|
||||
return hex;
|
||||
}
|
||||
// der
|
||||
return Enc.hexToBuf(hex);
|
||||
};
|
||||
|
||||
CSR._sign = function csrEcSig(jwk, request) {
|
||||
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
||||
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
|
||||
// TODO have a consistent non-private way to sign
|
||||
return Keypairs.sign(
|
||||
{ jwk: jwk, format: 'x509' },
|
||||
Enc.hexToBuf(request)
|
||||
).then(function(sig) {
|
||||
return CSR._toDer({
|
||||
request: request,
|
||||
signature: sig,
|
||||
kty: jwk.kty
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
CSR._toDer = function encode(opts) {
|
||||
var sty;
|
||||
if (/^EC/i.test(opts.kty)) {
|
||||
// 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
|
||||
sty = Asn1('30', Asn1('06', '2a8648ce3d040302'));
|
||||
} else {
|
||||
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
|
||||
sty = Asn1('30', Asn1('06', '2a864886f70d01010b'), Asn1('05'));
|
||||
}
|
||||
return Asn1(
|
||||
'30',
|
||||
// The Full CSR Request Body
|
||||
opts.request,
|
||||
// The Signature Type
|
||||
sty,
|
||||
// The Signature
|
||||
BitStr(Enc.bufToHex(opts.signature))
|
||||
);
|
||||
};
|
||||
|
||||
X509.packCsr = function(asn1pubkey, domains) {
|
||||
return Asn1(
|
||||
'30',
|
||||
// Version (0)
|
||||
UInt('00'),
|
||||
|
||||
// 2.5.4.3 commonName (X.520 DN component)
|
||||
Asn1(
|
||||
'30',
|
||||
Asn1(
|
||||
'31',
|
||||
Asn1(
|
||||
'30',
|
||||
Asn1('06', '550403'),
|
||||
// TODO utf8 => punycode
|
||||
Asn1('0c', Enc.strToHex(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) {
|
||||
// TODO utf8 => punycode
|
||||
return Asn1('82', Enc.strToHex(d));
|
||||
})
|
||||
.join('')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// TODO finish this later
|
||||
// we want to parse the domains, the public key, and verify the signature
|
||||
CSR._info = function(der) {
|
||||
// standard base64 PEM
|
||||
if ('string' === typeof der && '-' === der[0]) {
|
||||
der = PEM.parseBlock(der).bytes;
|
||||
}
|
||||
// jose urlBase64 not-PEM
|
||||
if ('string' === typeof der) {
|
||||
der = Enc.base64ToBuf(der);
|
||||
}
|
||||
// not supporting binary-encoded base64
|
||||
var c = Asn1Parser.parse({ der: der, verbose: true, json: false });
|
||||
var kty;
|
||||
// A cert has 3 parts: cert, signature meta, signature
|
||||
if (c.children.length !== 3) {
|
||||
throw new Error(
|
||||
"doesn't look like a certificate request: expected 3 parts of header"
|
||||
);
|
||||
}
|
||||
var sig = c.children[2];
|
||||
if (sig.children.length) {
|
||||
// ASN1/X509 EC
|
||||
sig = sig.children[0];
|
||||
sig = Asn1(
|
||||
'30',
|
||||
UInt(Enc.bufToHex(sig.children[0].value)),
|
||||
UInt(Enc.bufToHex(sig.children[1].value))
|
||||
);
|
||||
sig = Enc.hexToBuf(sig);
|
||||
kty = 'EC';
|
||||
} else {
|
||||
// Raw RSA Sig
|
||||
sig = sig.value;
|
||||
kty = 'RSA';
|
||||
}
|
||||
//c.children[1]; // signature type
|
||||
var req = c.children[0];
|
||||
if (4 !== req.children.length) {
|
||||
throw new Error(
|
||||
"doesn't look like a certificate request: expected 4 parts to request"
|
||||
);
|
||||
}
|
||||
// 0 null
|
||||
// 1 commonName / subject
|
||||
var sub = Enc.bufToStr(
|
||||
req.children[1].children[0].children[0].children[1].value
|
||||
);
|
||||
// 3 public key (type, key)
|
||||
//console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value));
|
||||
var pub;
|
||||
// TODO reuse ASN1 parser for these?
|
||||
if ('EC' === kty) {
|
||||
// throw away compression byte
|
||||
pub = req.children[2].children[1].value.slice(1);
|
||||
pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) };
|
||||
while (0 === pub.x[0]) {
|
||||
pub.x = pub.x.slice(1);
|
||||
}
|
||||
while (0 === pub.y[0]) {
|
||||
pub.y = pub.y.slice(1);
|
||||
}
|
||||
if ((pub.x.length || pub.x.byteLength) > 48) {
|
||||
pub.crv = 'P-521';
|
||||
} else if ((pub.x.length || pub.x.byteLength) > 32) {
|
||||
pub.crv = 'P-384';
|
||||
} else {
|
||||
pub.crv = 'P-256';
|
||||
}
|
||||
pub.x = Enc.bufToUrlBase64(pub.x);
|
||||
pub.y = Enc.bufToUrlBase64(pub.y);
|
||||
} else {
|
||||
pub = req.children[2].children[1].children[0];
|
||||
pub = {
|
||||
kty: kty,
|
||||
n: pub.children[0].value,
|
||||
e: pub.children[1].value
|
||||
};
|
||||
while (0 === pub.n[0]) {
|
||||
pub.n = pub.n.slice(1);
|
||||
}
|
||||
while (0 === pub.e[0]) {
|
||||
pub.e = pub.e.slice(1);
|
||||
}
|
||||
pub.n = Enc.bufToUrlBase64(pub.n);
|
||||
pub.e = Enc.bufToUrlBase64(pub.e);
|
||||
}
|
||||
// 4 extensions
|
||||
var domains = req.children[3].children
|
||||
.filter(function(seq) {
|
||||
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
|
||||
if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.map(function(seq) {
|
||||
return seq.children[1].children[0].children
|
||||
.filter(function(seq2) {
|
||||
// subjectAltName (X.509 extension)
|
||||
if ('551d11' === Enc.bufToHex(seq2.children[0].value)) {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.map(function(seq2) {
|
||||
return seq2.children[1].children[0].children.map(function(
|
||||
name
|
||||
) {
|
||||
// TODO utf8 => punycode
|
||||
return Enc.bufToStr(name.value);
|
||||
});
|
||||
})[0];
|
||||
})[0];
|
||||
|
||||
return {
|
||||
subject: sub,
|
||||
altnames: domains,
|
||||
jwk: pub,
|
||||
signature: sig
|
||||
};
|
||||
};
|
||||
|
73
package-lock.json
generated
73
package-lock.json
generated
@ -1,14 +1,75 @@
|
||||
{
|
||||
"name": "@root/csr",
|
||||
"version": "1.0.0",
|
||||
"version": "0.8.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"acme-dns-01-digitalocean": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/acme-dns-01-digitalocean/-/acme-dns-01-digitalocean-3.0.1.tgz",
|
||||
"integrity": "sha512-LUdOGluDERQWJG4CwlC9HbzUai4mtKzCz8nzpVTirXup2WwH60iRFAcd81hRGaoWbd0Bc0m6RVjN9YFkXB84yA==",
|
||||
"dev": true
|
||||
"@root/acme": {
|
||||
"version": "3.0.0-wip.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.0-wip.0.tgz",
|
||||
"integrity": "sha512-IwnG3ZFt1fl81O1M+FFV91b5Kpw7GYAD1jXwvOWnq9KF50AVO6+L7MUQIAFCK1q/u/weC73DCFrw/6kFN+Vi9A==",
|
||||
"requires": {
|
||||
"@root/csr": "^1.0.0-wip.0",
|
||||
"@root/encoding": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@root/asn1": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
|
||||
"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
|
||||
"requires": {
|
||||
"@root/encoding": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@root/csr": {
|
||||
"version": "1.0.0-wip.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-1.0.0-wip.0.tgz",
|
||||
"integrity": "sha512-ZrZeGgf/hvfIyMDAZXfD45rYriaZF6LJu7+l0ioPPKgLWVEUAUBkV53z7JbzlcPvXXr6/ZjECzWQ7MYQfMBUAg==",
|
||||
"requires": {
|
||||
"@root/acme": "^3.0.0-wip.0"
|
||||
}
|
||||
},
|
||||
"@root/encoding": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
|
||||
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
|
||||
},
|
||||
"@root/keypairs": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
|
||||
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@root/encoding": "^1.0.1",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/x509": "^0.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@root/x509": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
|
||||
"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@root/asn1": "^1.0.0",
|
||||
"@root/encoding": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@root/pem": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz",
|
||||
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
|
||||
},
|
||||
"@root/x509": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
|
||||
"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==",
|
||||
"requires": {
|
||||
"@root/asn1": "^1.0.0",
|
||||
"@root/encoding": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
package.json
15
package.json
@ -1,8 +1,13 @@
|
||||
{
|
||||
"name": "@root/csr",
|
||||
"version": "1.0.0-wip.0",
|
||||
"version": "0.8.0",
|
||||
"description": "Lightweight, Zero-Dependency CSR (Certificate Signing Request) generator and parser for Node.js and Browsers",
|
||||
"main": "csr.js",
|
||||
"files": [
|
||||
"*.js",
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node tests"
|
||||
},
|
||||
@ -26,6 +31,12 @@
|
||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@root/acme": "^3.0.0-wip.0"
|
||||
"@root/acme": "^3.0.0-wip.0",
|
||||
"@root/asn1": "^1.0.0",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/x509": "^0.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@root/keypairs": "^0.9.0"
|
||||
}
|
||||
}
|
||||
|
32
tests/index.js
Normal file
32
tests/index.js
Normal file
@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
var Keypairs = require('@root/keypairs');
|
||||
//var CSR = require('@root/csr');
|
||||
var CSR = require('../csr.js');
|
||||
//var PEM = require('@root/pem/packer');
|
||||
|
||||
async function run() {
|
||||
var pair = await Keypairs.generate();
|
||||
|
||||
var hex = CSR.request({
|
||||
jwk: pair.public,
|
||||
domains: ['example.com', '*.example.com', 'foo.bar.example.com'],
|
||||
encoding: 'hex'
|
||||
});
|
||||
//console.log(hex);
|
||||
|
||||
CSR.csr({
|
||||
jwk: pair.private,
|
||||
domains: ['example.com', '*.example.com', 'foo.bar.example.com'],
|
||||
encoding: 'pem'
|
||||
}).then(function(csr) {
|
||||
//var csr = PEM.packBlock({ type: 'CERTIFICATE REQUEST', bytes: der });
|
||||
console.log(csr);
|
||||
if (!/^-----BEGIN CERTIFICATE REQUEST-----\s*MIIB/m.test(csr)) {
|
||||
throw new Error("invalid CSR PEM");
|
||||
}
|
||||
console.info('PASS: (if it looks right)');
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
Loading…
x
Reference in New Issue
Block a user