From 6b68d49abc7876d81cfa2f3947024f4a84c21a94 Mon Sep 17 00:00:00 2001 From: Alex Wilson Date: Tue, 9 Oct 2018 18:13:48 -0700 Subject: [PATCH] joyent/node-sshpk#54 want API for accessing x509 extensions Reviewed by: Cody Peter Mello Approved by: Cody Peter Mello --- README.md | 18 ++++++++++++ lib/certificate.js | 34 ++++++++++++++++++++- lib/formats/openssh-cert.js | 45 +++++++++++++++++++++++----- lib/formats/x509.js | 3 +- test/assets/openssh-exts.pub | 1 + test/assets/yubikey.pem | 16 ++++++++++ test/certs.js | 57 ++++++++++++++++++++++++++++++++++++ 7 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 test/assets/openssh-exts.pub create mode 100644 test/assets/yubikey.pem diff --git a/README.md b/README.md index e8d8c88..bd3d5c3 100644 --- a/README.md +++ b/README.md @@ -507,6 +507,24 @@ is valid for. The possible strings at the moment are: Authority) * `'crl'` -- key can be used to sign Certificate Revocation Lists (CRLs) +### `Certificate#getExtension(nameOrOid)` + +Retrieves information about a certificate extension, if present, or returns +`undefined` if not. The string argument `nameOrOid` should be either the OID +(for X509 extensions) or the name (for OpenSSH extensions) of the extension +to retrieve. + +The object returned will have the following properties: + + * `format` -- String, set to either `'x509'` or `'openssh'` + * `name` or `oid` -- String, only one set based on value of `format` + * `data` -- Buffer, the raw data inside the extension + +### `Certificate#getExtensions()` + +Returns an Array of all present certificate extensions, in the same manner and +format as `getExtension()`. + ### `Certificate#isExpired([when])` Tests whether the Certificate is currently expired (i.e. the `validFrom` and diff --git a/lib/certificate.js b/lib/certificate.js index f08d66a..6932357 100644 --- a/lib/certificate.js +++ b/lib/certificate.js @@ -120,6 +120,37 @@ Certificate.prototype.isSignedBy = function (issuerCert) { return (this.isSignedByKey(issuerCert.subjectKey)); }; +Certificate.prototype.getExtension = function (keyOrOid) { + assert.string(keyOrOid, 'keyOrOid'); + var ext = this.getExtensions().filter(function (maybeExt) { + if (maybeExt.format === 'x509') + return (maybeExt.oid === keyOrOid); + if (maybeExt.format === 'openssh') + return (maybeExt.name === keyOrOid); + return (false); + })[0]; + return (ext); +}; + +Certificate.prototype.getExtensions = function () { + var exts = []; + var x509 = this.signatures.x509; + if (x509 && x509.extras && x509.extras.exts) { + x509.extras.exts.forEach(function (ext) { + ext.format = 'x509'; + exts.push(ext); + }); + } + var openssh = this.signatures.openssh; + if (openssh && openssh.exts) { + openssh.exts.forEach(function (ext) { + ext.format = 'openssh'; + exts.push(ext); + }); + } + return (exts); +}; + Certificate.prototype.isSignedByKey = function (issuerKey) { utils.assertCompatible(issuerKey, Key, [1, 2], 'issuerKey'); @@ -370,8 +401,9 @@ Certificate.isCertificate = function (obj, ver) { /* * API versions for Certificate: * [1,0] -- initial ver + * [1,1] -- openssh format now unpacks extensions */ -Certificate.prototype._sshpkApiVersion = [1, 0]; +Certificate.prototype._sshpkApiVersion = [1, 1]; Certificate._oldVersionDetect = function (obj) { return ([1, 0]); diff --git a/lib/formats/openssh-cert.js b/lib/formats/openssh-cert.js index 0b95e89..766f3d3 100644 --- a/lib/formats/openssh-cert.js +++ b/lib/formats/openssh-cert.js @@ -122,8 +122,23 @@ function fromBuffer(data, algo, partial) { cert.validFrom = int64ToDate(sshbuf.readInt64()); cert.validUntil = int64ToDate(sshbuf.readInt64()); - cert.signatures.openssh.critical = sshbuf.readBuffer(); - cert.signatures.openssh.exts = sshbuf.readBuffer(); + var exts = []; + var extbuf = new SSHBuffer({ buffer: sshbuf.readBuffer() }); + var ext; + while (!extbuf.atEnd()) { + ext = { critical: true }; + ext.name = extbuf.readString(); + ext.data = extbuf.readBuffer(); + exts.push(ext); + } + extbuf = new SSHBuffer({ buffer: sshbuf.readBuffer() }); + while (!extbuf.atEnd()) { + ext = { critical: false }; + ext.name = extbuf.readString(); + ext.data = extbuf.readBuffer(); + exts.push(ext); + } + cert.signatures.openssh.exts = exts; /* reserved */ sshbuf.readBuffer(); @@ -278,13 +293,27 @@ function toBuffer(cert, noSig) { buf.writeInt64(dateToInt64(cert.validFrom)); buf.writeInt64(dateToInt64(cert.validUntil)); - if (sig.critical === undefined) - sig.critical = Buffer.alloc(0); - buf.writeBuffer(sig.critical); + var exts = sig.exts; + if (exts === undefined) + exts = []; - if (sig.exts === undefined) - sig.exts = Buffer.alloc(0); - buf.writeBuffer(sig.exts); + var extbuf = new SSHBuffer({}); + exts.forEach(function (ext) { + if (ext.critical !== true) + return; + extbuf.writeString(ext.name); + extbuf.writeBuffer(ext.data); + }); + buf.writeBuffer(extbuf.toBuffer()); + + extbuf = new SSHBuffer({}); + exts.forEach(function (ext) { + if (ext.critical === true) + return; + extbuf.writeString(ext.name); + extbuf.writeBuffer(ext.data); + }); + buf.writeBuffer(extbuf.toBuffer()); /* reserved */ buf.writeBuffer(Buffer.alloc(0)); diff --git a/lib/formats/x509.js b/lib/formats/x509.js index 219953c..cc0d2b3 100644 --- a/lib/formats/x509.js +++ b/lib/formats/x509.js @@ -242,7 +242,8 @@ function readExtension(cert, buf, der) { var extId = der.readOID(); var id; var sig = cert.signatures.x509; - sig.extras.exts = []; + if (!sig.extras.exts) + sig.extras.exts = []; var critical; if (der.peek() === asn1.Ber.Boolean) diff --git a/test/assets/openssh-exts.pub b/test/assets/openssh-exts.pub new file mode 100644 index 0000000..edc78f8 --- /dev/null +++ b/test/assets/openssh-exts.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgGJnHtSaSYoqtvjv5k2BeP4bKCQv3CVvjcG0EMbY/iToAAAAIbmlzdHAyNTYAAABBBK9+hFGVZ9RT61pg8t7EGgkvduhPr/CBYfx+5rQFEROj8EjkoGIH2xypHOHBz0WikK5hYcwTM5YMvnNxuU0h4+cAAAAAAAAAAAAAAAEAAAAIdXNlcl9mb28AAAAHAAAAA2ZvbwAAAABbvVB4AAAAAF2dMrUAAAAiAAAADWZvcmNlLWNvbW1hbmQAAAANAAAACWZvb2JhcmNtZAAAAHAAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAGEECFVIfBIMjMd5ibZFUmEIh/HhvrCvFr7fLOsva912h4J0TaGeHHL2OuHXFYHRRToZ9bZSI+5kGIdabZiCMXfI7aTYv7gT8uMNzZbw9qApwP91ZxJwTOkGikvhCvdhzMmDAAAAhAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAaQAAADEAwZVZk2dsVAy2w3dJMbMfNsP9sYEW5Qa5DRDAddpRV3yL9Sb318KwYzfeuRFoCl/HAAAAMCnLGQ23ZHJhxCVpmtlSeuAKC2lgoqK2UNsOPDUOFg2p74dqnWsBjaUi9Ddj0HfHkA== id_ecdsa2 diff --git a/test/assets/yubikey.pem b/test/assets/yubikey.pem new file mode 100644 index 0000000..f5757fe --- /dev/null +++ b/test/assets/yubikey.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICRDCCASygAwIBAgIRAO0gfWbAn5zoJg4edq5vmYcwDQYJKoZIhvcNAQELBQAw +ITEfMB0GA1UEAwwWWXViaWNvIFBJViBBdHRlc3RhdGlvbjAgFw0xNjAzMTQwMDAw +MDBaGA8yMDUyMDQxNzAwMDAwMFowJTEjMCEGA1UEAwwaWXViaUtleSBQSVYgQXR0 +ZXN0YXRpb24gOWUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASfpEFqoePaZIAs +L2xkEdVZ67pgQWIkgaKDxQr+QidA/j+5DjStb515FJZ8qYAF64mVrZjaJgD3Outp +9G5eJWvzozwwOjARBgorBgEEAYLECgMDBAMEAwMwEwYKKwYBBAGCxAoDBwQFAgNP +jfEwEAYKKwYBBAGCxAoDCAQCAQEwDQYJKoZIhvcNAQELBQADggEBAKa+hH9ExP4I +1g40Qzi6+xaB7K0nmlE4xXLAceVeBebIKMDFGdbJpcMGxw5K4GmcMlaYLxKUbUdX +uQBZ5LZSiHALxinF/0Lpn1I8SS4txgy5JvSJxMyaWCuupvQ0/zKk+eTryrsO52WX +RLYNdOwwlVHqhu2cNoZ7F0AynEfTwIksCcpiJX3UI8YmOnBUarJpWG2M98HbQUXx +0oLgy5I46xtY+vbMtw7tn42BjJF10RV+99VNsnUagbzCgULVLWnbaYUARA7k+Doc +MWNMYnZPS5ouvHnAJGFER9/v0YELGuyt/dDCU/qPUY95UW6lXJSO2Iy41E+libW7 +s4MpuZzf5rw= +-----END CERTIFICATE----- + diff --git a/test/certs.js b/test/certs.js index 0c10fef..edcf8ff 100644 --- a/test/certs.js +++ b/test/certs.js @@ -7,6 +7,8 @@ var fs = require('fs'); var path = require('path'); var crypto = require('crypto'); var sinon = require('sinon'); +var asn1 = require('asn1'); +var SSHBuffer = require('../lib/ssh-buffer'); var testDir = path.join(__dirname, 'assets'); @@ -250,6 +252,17 @@ test('example cert: digicert ca (x509)', function (t) { t.strictEqual(cert.subjects.length, 1); t.deepEqual(cert.purposes.sort(), ['ca', 'clientAuth', 'crl', 'serverAuth', 'signature']); + var exts = cert.getExtensions(); + t.strictEqual(exts.length, 8); + exts.forEach(function (ext) { + t.strictEqual(ext.format, 'x509'); + t.strictEqual(typeof (ext.oid), 'string'); + }); + var basicExt = cert.getExtension('2.5.29.19'); + t.strictEqual(basicExt.oid, '2.5.29.19'); + t.strictEqual(basicExt.critical, true); + t.strictEqual(basicExt.format, 'x509'); + t.strictEqual(basicExt.pathLen, 0); t.end(); }); @@ -386,3 +399,47 @@ test('cert with doubled-up DN attribute', function (t) { t.end(); }); + +test('example cert: yubikey attestation cert', function (t) { + var cert = sshpk.parseCertificate( + fs.readFileSync(path.join(testDir, 'yubikey.pem')), + 'pem'); + t.strictEqual(cert.subjectKey.type, 'ecdsa'); + t.strictEqual(cert.subjects[0].cn, 'YubiKey PIV Attestation 9e'); + + var serialExt = cert.getExtension('1.3.6.1.4.1.41482.3.7'); + t.ok(serialExt); + var der = new asn1.Ber.Reader(serialExt.data); + t.strictEqual(der.readInt(), 5213681); + + var policyExt = cert.getExtension('1.3.6.1.4.1.41482.3.8'); + t.ok(policyExt); + t.strictEqual(policyExt.data[0], 0x01); /* never require PIN */ + t.strictEqual(policyExt.data[1], 0x01); /* never require touch */ + + t.end(); +}); + +test('example cert: openssh extensions', function (t) { + var cert = sshpk.parseCertificate( + fs.readFileSync(path.join(testDir, 'openssh-exts.pub')), + 'openssh'); + t.strictEqual(cert.subjectKey.type, 'ecdsa'); + t.strictEqual(cert.subjects[0].uid, 'foo'); + + var forceCmdExt = cert.getExtension('force-command'); + t.ok(forceCmdExt); + t.strictEqual(forceCmdExt.name, 'force-command'); + t.strictEqual(forceCmdExt.critical, true); + + var cmdbuf = new SSHBuffer({ buffer: forceCmdExt.data }); + var cmd = cmdbuf.readString(); + t.strictEqual(cmd, 'foobarcmd'); + t.ok(cmdbuf.atEnd()); + + t.ok(cert.getExtension('permit-port-forwarding')); + t.notOk(cert.getExtension('source-address')); + t.notOk(cert.getExtension('permit-pty')); + + t.end(); +});