From 0416948f28186cea3d33e642b25ac7c7d1938695 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 10 Jun 2020 10:15:55 -0400 Subject: [PATCH] x509: finish JSON interface with extensions --- lib/encoding/asn1.js | 11 ++-- lib/encoding/x509.js | 15 ++++-- test/x509-test.js | 116 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 8 deletions(-) diff --git a/lib/encoding/asn1.js b/lib/encoding/asn1.js index 4e858b63c..9920bb3de 100644 --- a/lib/encoding/asn1.js +++ b/lib/encoding/asn1.js @@ -597,6 +597,7 @@ class Choice extends Node { assert(node instanceof Node); this.node = node; this.from(...options); + this.types = types; } get type() { @@ -607,6 +608,10 @@ class Choice extends Node { throw new Error('Unimplemented.'); } + typeToClass(type) { + return typeToClass(type); + } + getSize(extra) { return this.node.getSize(extra); } @@ -634,7 +639,7 @@ class Choice extends Node { if (choices.indexOf(hdr.type) === -1) throw new Error(`Could not satisfy choice for: ${hdr.type}.`); - const Node = typeToClass(hdr.type); + const Node = this.typeToClass(hdr.type); const el = new Node(); el.flags = this.flags; @@ -680,8 +685,8 @@ class Choice extends Node { } fromJSON(json) { - const type = types[json.type.toUpperCase()]; - const Node = typeToClass(type); + const type = this.types[json.type.toUpperCase()]; + const Node = this.typeToClass(type); this.node = new Node(); this.node.fromJSON(json.node); this.node.flags = this.flags; diff --git a/lib/encoding/x509.js b/lib/encoding/x509.js index 8ffa33b9d..53316af18 100644 --- a/lib/encoding/x509.js +++ b/lib/encoding/x509.js @@ -252,16 +252,23 @@ class TBSCertificate extends asn1.Sequence { } fromJSON(json) { + let sn = json.serialNumber; + if (typeof sn === 'string') + sn = {value: sn, negative: false}; + this.version.fromJSON(json.version); - this.serialNumber.fromJSON(json.serialNumber); + this.serialNumber.fromJSON(sn); this.signature.fromJSON(json.signature); this.issuer.fromJSON(json.issuer); this.validity.fromJSON(json.validity); this.subject.fromJSON(json.subject); this.subjectPublicKeyInfo.fromJSON(json.subjectPublicKeyInfo); - this.issuerUniqueID.fromJSON(json.issuerUniqueID); - this.subjectUniqueID.fromJSON(json.subjectUniqueID); - this.extensions.fromJSON(json.extensions); + if (json.issuerUniqueID) + this.issuerUniqueID.fromJSON(json.issuerUniqueID); + if (json.subjectUniqueID) + this.subjectUniqueID.fromJSON(json.subjectUniqueID); + if (json.extensions) + this.extensions.fromJSON(json.extensions); return this; } diff --git a/test/x509-test.js b/test/x509-test.js index 4a57bf2f0..7831d1a66 100644 --- a/test/x509-test.js +++ b/test/x509-test.js @@ -14,6 +14,8 @@ const certsData = fs.readFileSync(certs, 'utf8'); const certificate = Path.resolve(__dirname, 'data', 'x509', 'certificate.crt'); const certificateData = fs.readFileSync(certificate, 'utf8'); +let certFromJSON; + describe('X509', function() { if (process.env.BMOCHA_VALGRIND) this.skip(); @@ -31,7 +33,7 @@ describe('X509', function() { assert.bufferEqual(raw1, block.data); }); - it(`should read JSON and write JSON (${i++})`, () => { + it(`should read JSON and write JSON (${i})`, () => { const crt1 = x509.Certificate.decode(block.data); const json1 = crt1.getJSON(); @@ -73,4 +75,116 @@ describe('X509', function() { assert(r); }); } + + it('should create a self-signed certificate using JSON', () => { + // Create key pair and get JSON for pubkey + const priv = rsa.privateKeyGenerate(2048); + const pub = rsa.publicKeyCreate(priv); + const pubJSON = rsa.publicKeyExport(pub); + + // Basic details, leave out optional and more complex stuff + const json = { + version: 2, + serialNumber: 'deadbeef0101', + signature: { + algorithm: 'RSASHA256' + }, + issuer: [], + validity: { + notBefore: { type: 'UTCTime', node: '2020-04-20T18:53:25Z' }, + notAfter: { type: 'UTCTime', node: '2021-04-20T18:53:25Z' } + }, + subject: [], + subjectPublicKeyInfo: { + algorithm: { + algorithm: 'RSAPublicKey' + }, + publicKey: { + modulus: pubJSON.n, + publicExponent: pubJSON.e + } + }, + extensions: [ + { + extnID: 'SubjectAltName', + critical: false, + extnValue: [ + { type: 'DNSName', node: '*.bcoin.io' }, + { type: 'DNSName', node: 'bcoin.io' } + ] + }, + { + extnID: 'BasicConstraints', + critical: false, + extnValue: {cA: false, pathLenConstraint: 0} + } + ] + }; + + // Create to-be-signed certificate object + const tbs = x509.TBSCertificate.fromJSON(json); + + // Use helper functions for the complicated details + tbs.issuer = x509.Entity.fromJSON({ + COUNTRY: 'US', + PROVINCE: 'CA', + LOCALITY: 'San Francisco', + ORGANIZATION: 'bcrypto', + ORGANIZATIONALUNIT: 'encodings', + COMMONNAME: 'bcoin.io', + EMAILADDRESS: 'satoshi@bcoin.io' + }); + tbs.subject = x509.Entity.fromJSON({ + COUNTRY: 'US', + PROVINCE: 'CA', + LOCALITY: 'San Francisco', + ORGANIZATION: 'bcrypto', + ORGANIZATIONALUNIT: 'encodings', + COMMONNAME: 'bcoin.io', + EMAILADDRESS: 'satoshi@bcoin.io' + }); + + // Serialize + const msg = sha256.digest(tbs.encode()); + + // Sign + const sig = rsa.sign('SHA256', msg, priv); + + // Complete + certFromJSON = new x509.Certificate(); + certFromJSON.tbsCertificate = tbs; + certFromJSON.signatureAlgorithm.fromJSON({algorithm: 'RSASHA256'}); + certFromJSON.signature.fromJSON({bits: sig.length * 8, value: sig.toString('hex')}); + }); + + it.skip('should verify with openssl', () => { + const os = require('os'); + const {exec} = require('child_process'); + + // Write file + let tmp = Path.join(os.tmpdir(), 'bcrypto-test.crt'); + fs.writeFileSync(tmp, certFromJSON.toPEM()); + + // Test + exec(`openssl verify -check_ss_sig ${tmp}`, (error, stdout, stderr) => { + assert(!error); + assert.strictEqual('OK\n', stdout.slice(-3)); + }); + + // Sanity check 1: certificate produced by openssl + exec(`openssl verify -check_ss_sig ${certificate}`, (error, stdout, stderr) => { + assert(!error); + assert.strictEqual('OK\n', stdout.slice(-3)); + }); + + // Sanity check 2: malleated signature fails verification + certFromJSON.signature.value[100]++; + tmp = Path.join(os.tmpdir(), 'bcrypto-test2.crt'); + fs.writeFileSync(tmp, certFromJSON.toPEM()); + exec(`openssl verify -check_ss_sig ${tmp}`, (error, stdout, stderr) => { + assert(error); + const msg = 'certificate signature failure\n'; + assert.strictEqual(msg, stdout.slice(-1 * msg.length)); + }); + }); });