Browse Source

v0.8.0: generate CSR with native JavaScript

master v0.8.0
AJ ONeal 5 years ago
parent
commit
22b752507f
  1. 47
      README.md
  2. 335
      csr.js
  3. 73
      package-lock.json
  4. 15
      package.json
  5. 32
      tests/index.js

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

@ -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

@ -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

@ -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

@ -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…
Cancel
Save