diff --git a/lib/encoding/asn1.js b/lib/encoding/asn1.js index 4a6996388..9920bb3de 100644 --- a/lib/encoding/asn1.js +++ b/lib/encoding/asn1.js @@ -117,12 +117,18 @@ const classesByVal = { 3: 'PRIVATE' }; +// ASN.1 type const TARGET = 0xff; + +// flags const OPTIONAL = 1 << 8; -const MODE = 0xff << 9; -const NORMAL = 0 << 9; -const EXPLICIT = 1 << 9; -const IMPLICIT = 2 << 9; +const FORCE = 1 << 9; + +// mode +const MODE = 0xff << 10; +const NORMAL = 0 << 10; +const EXPLICIT = 1 << 10; +const IMPLICIT = 1 << 11; /** * Node @@ -154,6 +160,17 @@ class Node extends bio.Struct { this.flags &= ~OPTIONAL; } + get force() { + return (this.flags & FORCE) !== 0; + } + + set force(value) { + if (value) + this.flags |= FORCE; + else + this.flags &= ~FORCE; + } + get target() { return this.flags & TARGET; } @@ -239,7 +256,7 @@ class Node extends bio.Struct { } getSize(extra) { - if (this.opt && this.clean()) + if (this.opt && this.clean() && !this.force) return 0; const body = this.getBodySize(extra); @@ -256,7 +273,7 @@ class Node extends bio.Struct { } write(bw, extra) { - if (this.opt && this.clean()) + if (this.opt && this.clean() && !this.force) return bw; const body = this.getBodySize(); @@ -490,6 +507,13 @@ class Any extends Node { const Node = typeToClass(hdr.type); this.node = new Node(); + + // If a Null node was present when reading, + // respect the preference and include the node + // when writing as well. + if (this.opt) + this.force = true; + this.node.flags = this.flags; this.node.read(br, extra); @@ -535,6 +559,32 @@ class Any extends Node { node: this.node }; } + + getJSON() { + if (this.opt && !this.force) + return null; + + return { + type: this.node.constructor.name, + node: this.node.getJSON() + }; + } + + fromJSON(json) { + if (!json) + return null; + + if (this.opt) + this.force = true; + + const type = types[json.type.toUpperCase()]; + const Node = typeToClass(type); + this.node = new Node(); + this.node.fromJSON(json.node); + this.node.flags = this.flags; + + return this.node; + } } /** @@ -547,6 +597,7 @@ class Choice extends Node { assert(node instanceof Node); this.node = node; this.from(...options); + this.types = types; } get type() { @@ -557,6 +608,10 @@ class Choice extends Node { throw new Error('Unimplemented.'); } + typeToClass(type) { + return typeToClass(type); + } + getSize(extra) { return this.node.getSize(extra); } @@ -584,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; @@ -621,6 +676,23 @@ class Choice extends Node { node: this.node }; } + + getJSON() { + return { + type: this.node.constructor.name, + node: this.node.getJSON() + }; + } + + fromJSON(json) { + 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; + + return this.node; + } } /** @@ -693,6 +765,17 @@ const Str = class String extends Node { format() { return `<${this.constructor.name}: ${this.value}>`; } + + getJSON() { + return this.value; + } + + fromJSON(json) { + assert(typeof json === 'string'); + this.value = json; + + return this; + } }; /** @@ -751,6 +834,17 @@ const Bool = class Boolean extends Node { format() { return `<${this.constructor.name}: ${this.value}>`; } + + getJSON() { + return this.value; + } + + fromJSON(json) { + assert(typeof json === 'boolean'); + this.value = json; + + return this; + } }; /** @@ -954,6 +1048,21 @@ class Integer extends Node { return `<${name}: ${sign}0x${hex}>`; } + + getJSON() { + return { + value: this.value.toString('hex'), + negative: this.negative + }; + } + + fromJSON(json) { + assert(typeof json.negative === 'boolean'); + this.value = Buffer.from(json.value, 'hex'); + this.negative = json.negative; + + return this; + } } /** @@ -995,6 +1104,14 @@ class Unsigned extends Integer { assert(!this.negative); return this; } + + getJSON() { + return this.toNumber(); + } + + fromJSON(json) { + return this.fromNumber(json); + } } /** @@ -1121,6 +1238,21 @@ class BitString extends Node { return `<${this.constructor.name}: ${this.bits}:${value.toString('hex')}>`; } + + getJSON() { + return { + bits: this.bits, + value: this.value.toString('hex') + }; + } + + fromJSON(json) { + assert(typeof json.bits === 'number'); + this.bits = json.bits; + this.value = Buffer.from(json.value, 'hex'); + + return this; + } } /** @@ -1175,6 +1307,16 @@ class OctString extends Node { return `<${this.constructor.name}: ${value.toString('hex')}>`; } + + getJSON() { + return this.value.toString('hex'); + } + + fromJSON(json) { + this.value = Buffer.from(json, 'hex'); + + return this; + } } /** @@ -1213,6 +1355,14 @@ class Null extends Node { format() { return `<${this.constructor.name}>`; } + + getJSON() { + return null; + } + + fromJSON(json) { + return this; + } } /** @@ -1368,6 +1518,10 @@ class OID extends Node { str = objects.hashes[str]; else if (objects.curves.hasOwnProperty(str)) str = objects.curves[str]; + else if (objects.sigAlgs.hasOwnProperty(str)) + str = objects.sigAlgs[str]; + else if (objects.extensions.hasOwnProperty(str)) + str = objects.extensions[str]; const parts = str.split('.'); const out = new Uint32Array(parts.length); @@ -1415,6 +1569,10 @@ class OID extends Node { return objects.curvesByVal[this.toString()] || null; } + getExtensionName() { + return objects.extensionsByVal[this.toString()] || null; + } + format() { const oid = this.toString(); const name = objects.attrsByVal[oid] @@ -1422,12 +1580,29 @@ class OID extends Node { || objects.keyAlgsByVal[oid] || objects.hashesByVal[oid] || objects.curvesByVal[oid] + || objects.extensionsByVal[oid] || 'UNKNOWN'; const str = `${oid} (${name})`; return `<${this.constructor.name}: ${str}>`; } + + getJSON() { + const oid = this.toString(); + const name = objects.attrsByVal[oid] + || objects.sigAlgsByVal[oid] + || objects.keyAlgsByVal[oid] + || objects.hashesByVal[oid] + || objects.curvesByVal[oid] + || objects.extensionsByVal[oid] + || this.toString(); + return name; + } + + fromJSON(json) { + return this.fromString(json); + } } /** @@ -1548,6 +1723,14 @@ class RawSequence extends Node { format() { return this.toArray(); } + + getJSON() { + throw new Error('Not implemented.'); + } + + fromJSON(json) { + throw new Error('Not implemented.'); + } } /** @@ -1695,6 +1878,14 @@ class Time extends Node { return `<${name}: ${value}${off} (${this.toString()})>`; } + + getJSON() { + return this.toString(); + } + + fromJSON(json) { + return this.fromString(json); + } } /** diff --git a/lib/encoding/x509.js b/lib/encoding/x509.js index 44ed1b4a8..e8ee16b9b 100644 --- a/lib/encoding/x509.js +++ b/lib/encoding/x509.js @@ -17,6 +17,10 @@ * https://github.com/indutny/asn1.js/blob/master/lib/asn1/base/node.js * https://github.com/indutny/asn1.js/blob/master/lib/asn1/encoders/der.js * https://github.com/indutny/asn1.js/blob/master/lib/asn1/decoders/der.js + * https://www.itu.int/rec/T-REC-X.509-201910-I + * https://www.openssl.org/docs/manmaster/man5/x509v3_config.html + * https://tools.ietf.org/html/rfc8017#appendix-A + * https://tools.ietf.org/html/rfc5280 */ 'use strict'; @@ -92,6 +96,26 @@ class Certificate extends asn1.Sequence { signature: this.signature }; } + + getJSON() { + return { + tbsCertificate: this.tbsCertificate.getJSON(), + signatureAlgorithm: this.signatureAlgorithm.getJSON(), + signature: this.signature.getJSON() + }; + } + + fromJSON(json) { + this.tbsCertificate.fromJSON(json.tbsCertificate); + this.signatureAlgorithm.fromJSON(json.signatureAlgorithm); + this.signature.fromJSON(json.signature); + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } } /** @@ -211,6 +235,43 @@ class TBSCertificate extends asn1.Sequence { extensions: this.extensions }; } + + getJSON() { + return { + version: this.version.getJSON(), + serialNumber: this.serialNumber.getJSON(), + signature: this.signature.getJSON(), + issuer: this.issuer.getJSON(), + validity: this.validity.getJSON(), + subject: this.subject.getJSON(), + subjectPublicKeyInfo: this.subjectPublicKeyInfo.getJSON(), + issuerUniqueID: this.issuerUniqueID.getJSON(), + subjectUniqueID: this.subjectUniqueID.getJSON(), + extensions: this.extensions.getJSON() + }; + } + + fromJSON(json) { + let sn = json.serialNumber; + if (typeof sn === 'string') + sn = {value: sn, negative: false}; + + this.version.fromJSON(json.version); + 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); + 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; + } } /** @@ -260,6 +321,20 @@ class AlgorithmIdentifier extends asn1.Sequence { parameters: this.parameters }; } + + getJSON() { + return { + algorithm: this.algorithm.getJSON(), + parameters: this.parameters.getJSON() + }; + } + + fromJSON(json) { + this.algorithm.fromJSON(json.algorithm); + this.parameters.fromJSON(json.parameters); + + return this; + } } /** @@ -311,6 +386,22 @@ class RDNSequence extends asn1.Sequence { names: this.names }; } + + getJSON() { + const names = []; + for (const name of this.names) + names.push(name.getJSON()); + + return names; + } + + fromJSON(json) { + assert(Array.isArray(json)); + for (const name of json) + this.names.push(RDN.fromJSON(name)); + + return this; + } } /** @@ -368,6 +459,28 @@ class RDN extends asn1.Set { attributes: this.attributes }; } + + getJSON() { + const attributes = []; + for (const attr of this.attributes) + attributes.push(attr.getJSON()); + + return attributes; + } + + fromJSON(json) { + assert(Array.isArray(json)); + this.attributes = []; + + for (const attr of json) + this.attributes.push(Attribute.fromJSON(attr)); + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } } /** @@ -421,6 +534,24 @@ class Attribute extends asn1.Sequence { value: this.value }; } + + getJSON() { + return { + id: this.id.getJSON(), + value: this.value.getJSON() + }; + } + + fromJSON(json) { + this.id.fromJSON(json.id); + this.value.fromJSON(json.value); + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } } /** @@ -469,6 +600,20 @@ class Validity extends asn1.Sequence { notAfter: this.notAfter }; } + + getJSON() { + return { + notBefore: this.notBefore.getJSON(), + notAfter: this.notAfter.getJSON() + }; + } + + fromJSON(json) { + this.notBefore.fromJSON(json.notBefore); + this.notAfter.fromJSON(json.notAfter); + + return this; + } } /** @@ -492,6 +637,10 @@ class Time extends asn1.Choice { } } +/** + * SubjectPublicKeyInfo + */ + // SubjectPublicKeyInfo ::= SEQUENCE { // algorithm AlgorithmIdentifier, // subjectPublicKey BIT STRING } @@ -547,6 +696,40 @@ class SubjectPublicKeyInfo extends asn1.Sequence { publicKey: this.publicKey }; } + + getJSON() { + let publicKey = this.publicKey; + const OBJ = + identifierToClass(this.algorithm.algorithm.getJSON()); + if (OBJ) { + publicKey = new OBJ(); + publicKey.decode(this.publicKey.value); + } + + return { + algorithm: this.algorithm.getJSON(), + publicKey: publicKey.getJSON() + }; + } + + fromJSON(json) { + this.algorithm.fromJSON(json.algorithm); + + const OBJ = + identifierToClass(this.algorithm.algorithm.getJSON()); + if (OBJ) { + const publicKey = new OBJ(); + publicKey.fromJSON(json.publicKey); + this.publicKey.fromJSON({ + bits: publicKey.encode().length * 8, + value: publicKey.encode().toString('hex') + }); + } else { + this.publicKey.fromJSON(json.publicKey); + } + + return this; + } } /** @@ -575,8 +758,10 @@ class Extensions extends asn1.Sequence { } readBody(br) { - for (const ext of this.extensions) - ext.read(br); + while(br.left()) { + const ext = Extension.read(br); + this.extensions.push(ext); + } return this; } @@ -590,6 +775,22 @@ class Extensions extends asn1.Sequence { extensions: this.extensions }; } + + getJSON() { + const extensions = []; + for (const ext of this.extensions) + extensions.push(ext.getJSON()); + + return extensions; + } + + fromJSON(json) { + assert(Array.isArray(json)); + for (const ext of json) + this.extensions.push(Extension.fromJSON(ext)); + + return this; + } } /** @@ -645,6 +846,41 @@ class Extension extends asn1.Sequence { extnValue: this.extnValue }; } + + getJSON() { + let val = this.extnValue; + const OBJ = identifierToClass(this.extnID.getJSON()); + if (OBJ) { + val = new OBJ(); + val.decode(this.extnValue.value); + } + + return { + extnID: this.extnID.getJSON(), + critical: this.critical.getJSON(), + extnValue: val.getJSON() + }; + } + + fromJSON(json) { + this.extnID.fromJSON(json.extnID); + this.critical.fromJSON(json.critical); + + const OBJ = identifierToClass(this.extnID.getJSON()); + if (OBJ) { + const val = new OBJ(); + val.fromJSON(json.extnValue); + this.extnValue.fromJSON(val.encode().toString('hex')); + } else { + this.extnValue.fromJSON(json.extnValue); + } + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } } /** @@ -698,6 +934,459 @@ class DigestInfo extends asn1.Sequence { digest: this.digest }; } + + getJSON() { + return { + algorithm: this.algorithm.getJSON(), + digest: this.digest.getJSON() + }; + } + + fromJSON(json) { + this.algorithm.fromJSON(json.algorithm); + this.digest.fromJSON(json.digest); + + return this; + } +} + +/** + * BasicConstraints + */ + +// basicConstraints EXTENSION ::= { +// SYNTAX BasicConstraintsSyntax +// IDENTIFIED BY id-ce-basicConstraints } +// BasicConstraintsSyntax ::= SEQUENCE { +// cA BOOLEAN DEFAULT FALSE, +// pathLenConstraint INTEGER(0..MAX) OPTIONAL, +// ... } + +class BasicConstraints extends asn1.Sequence { + constructor() { + super(); + this.cA = new asn1.Bool().optional(); + this.pathLenConstraint = new asn1.Integer().optional(); + } + + getBodySize() { + let size = 0; + size += this.cA.getSize(); + size += this.pathLenConstraint.getSize(); + return size; + } + + writeBody(bw) { + this.cA.write(bw); + this.pathLenConstraint.write(bw); + return bw; + } + + readBody(br) { + this.cA.read(br); + this.pathLenConstraint.read(br); + return this; + } + + clean() { + return this.cA.clean() + && this.pathLenConstraint.clean(); + } + + format() { + return { + cA: this.cA, + pathLenConstraint: this.pathLenConstraint + }; + } + + getJSON() { + return { + cA: this.cA.getJSON(), + pathLenConstraint: this.pathLenConstraint.toNumber() + }; + } + + fromJSON(json) { + this.cA.fromJSON(json.cA); + this.pathLenConstraint.fromNumber(json.pathLenConstraint); + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } +} + +/** + * RSAPublicKey + */ + +// RSAPublicKey ::= SEQUENCE { +// modulus INTEGER, -- n +// publicExponent INTEGER -- e +// } + +class RSAPublicKey extends asn1.Sequence { + constructor() { + super(); + this.modulus = new asn1.Integer(); + this.publicExponent = new asn1.Integer(); + } + + getBodySize() { + let size = 0; + size += this.modulus.getSize(); + size += this.publicExponent.getSize(); + return size; + } + + writeBody(bw) { + this.modulus.write(bw); + this.publicExponent.write(bw); + return bw; + } + + readBody(br) { + this.modulus.read(br); + this.publicExponent.read(br); + return this; + } + + clean() { + return this.modulus.clean() + && this.publicExponent.clean(); + } + + format() { + return { + modulus: this.modulus, + publicExponent: this.publicExponent + }; + } + + getJSON() { + return { + modulus: this.modulus.getJSON().value, + publicExponent: this.publicExponent.getJSON().value + }; + } + + fromJSON(json) { + this.modulus.fromJSON({value: json.modulus, negative: false}); + this.publicExponent.fromJSON({value: json.publicExponent, negative: false}); + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } +} + +/** + * SubjectAltName + */ + +// SubjectAltName ::= GeneralNames +// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName + +class SubjectAltName extends asn1.Sequence { + constructor() { + super(); + this.names = []; + } + + getBodySize() { + let size = 0; + + for (const name of this.names) + size += name.getSize(); + + return size; + } + + writeBody(bw) { + for (const name of this.names) + name.write(bw); + return bw; + } + + readBody(br) { + while (br.left()) { + const name = GeneralName.read(br); + this.names.push(name); + } + return this; + } + + clean() { + return this.names.length === 0; + } + + format() { + return { + type: this.constructor.name, + names: this.names + }; + } + + getJSON() { + const names = []; + for (const name of this.names) + names.push(name.getJSON()); + + return names; + } + + fromJSON(json) { + assert(Array.isArray(json)); + for (const name of json) + this.names.push(GeneralName.fromJSON(name)); + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } +} + +/** + * GeneralName + */ + +// GeneralName ::= CHOICE { +// otherName [0] OtherName, +// rfc822Name [1] IA5String, +// dNSName [2] IA5String, +// x400Address [3] ORAddress, +// directoryName [4] Name, +// ediPartyName [5] EDIPartyName, +// uniformResourceIdentifier [6] IA5String, +// iPAddress [7] OCTET STRING, +// registeredID [8] OBJECT IDENTIFIER } + +// OtherName ::= SEQUENCE { +// type-id OBJECT IDENTIFIER, +// value [0] EXPLICIT ANY DEFINED BY type-id } + +// ORAddress ::= SEQUENCE { +// built-in-standard-attributes BuiltInStandardAttributes, +// built-in-domain-defined-attributes +// BuiltInDomainDefinedAttributes OPTIONAL, +// -- see also teletex-domain-defined-attributes +// extension-attributes ExtensionAttributes OPTIONAL } + +// Name ::= CHOICE { -- only one possibility for now -- +// rdnSequence RDNSequence } + +// EDIPartyName ::= SEQUENCE { +// nameAssigner [0] DirectoryString OPTIONAL, +// partyName [1] DirectoryString } + +class GeneralName extends asn1.Choice { + constructor() { + super(new asn1.Node()); + this.names = []; + this.types = { + OTHERNAME: 0, + RFC822NAME: 1, + DNSNAME: 2, + X400ADDRESS: 3, + DIRECTORYNAME: 4, + EDIPARTYNAME: 5, + UNIFORMRESOURCEIDENTIFIER: 6, + IPADDRESS: 7, + REGISTEREDID: 8 + }; + } + + choices() { + return Object.values(this.types); + } + + typeToClass(type) { + assert((type >>> 0) === type); + + // See https://tools.ietf.org/html/rfc5280#appendix-A + // for a confusing list of explicit/implicit types. + // Also: https://serverfault.com/questions/1020712/ + // x509-asn1-are-subjectaltname-elements-explicit-or-implicit + + this.implicit(type); + + switch (type) { + case this.types.OTHERNAME: + return OtherName; + case this.types.RFC822NAME: + return RFC822Name; + case this.types.DNSNAME: + return DNSName; + case this.types.X400ADDRESS: + return X400Address; + case this.types.DIRECTORYNAME: + this.explicit(type); + return DirectoryName; + case this.types.EDIPARTYNAME: + return EDIPartyName; + case this.types.UNIFORMRESOURCEIDENTIFIER: + return UniformResourceIdentifier; + case this.types.IPADDRESS: + return IPAddress; + case this.types.REGISTEREDID: + return RegisteredID; + default: + throw new Error(`Unknown type: ${type}.`); + } + } +} + +/** + * GeneralName types + */ + +class OtherName extends asn1.Sequence {}; +class RFC822Name extends asn1.IA5String {}; +class DNSName extends asn1.IA5String {}; +class X400Address extends asn1.Sequence {}; +class DirectoryName extends RDNSequence {}; +class EDIPartyName extends asn1.Sequence {}; +class UniformResourceIdentifier extends asn1.IA5String {}; +class IPAddress extends asn1.OctString {}; +class RegisteredID extends asn1.OID {}; + +/** + * KeyUsage + */ + +// KeyUsage ::= BIT STRING { +// digitalSignature (0), +// nonRepudiation (1), -- recent editions of X.509 have +// -- renamed this bit to contentCommitment +// keyEncipherment (2), +// dataEncipherment (3), +// keyAgreement (4), +// keyCertSign (5), +// cRLSign (6), +// encipherOnly (7), +// decipherOnly (8) } + +class KeyUsage extends asn1.BitString { + constructor() { + super(); + this.value = Buffer.alloc(2); + } + + getBitByProperty(property) { + const properties = { + 'digitalSignature': 0, + 'nonRepudiation': 1, + 'keyEncipherment': 2, + 'dataEncipherment': 3, + 'keyAgreement': 4, + 'keyCertSign': 5, + 'cRLSign': 6, + 'encipherOnly': 7, + 'decipherOnly': 8 + }; + + return properties[property]; + } + + getPropertyByBit(bit) { + const bits = [ + 'digitalSignature', + 'nonRepudiation', + 'keyEncipherment', + 'dataEncipherment', + 'keyAgreement', + 'keyCertSign', + 'cRLSign', + 'encipherOnly', + 'decipherOnly' + ]; + + return bits[bit]; + } + + getJSON() { + const purpose = []; + for (let i = 0; i <= this.bits; i++) { + if (this.getBit(i)) + purpose.push(this.getPropertyByBit(i)); + } + + return purpose; + } + + fromJSON(json) { + assert(Array.isArray(json)); + for (const property of json) { + const bit = this.getBitByProperty(property); + + if (bit + 1 > this.bits) + this.bits = bit + 1; + + this.setBit(bit, true); + } + + if (this.bits < 9) + this.value = this.value.slice(0, -1); + + return this; + } + + static fromJSON(json) { + return new this().fromJSON(json); + } +} + +/** + * Entity + */ + +// Wrapper around RDNSequence for JSON construction +// of subject and issuer. Uses UTF8String for everything. + +class Entity { + static fromJSON(json) { + const names = []; + for (const key of Object.keys(json)) { + const string = json[key]; + const attr = [{ + id: key, + value: { + type: 'Utf8String', + node: string + } + }]; + names.push(attr); + } + + return new RDNSequence().fromJSON(names); + } +} +/** + * Helpers + */ + +function identifierToClass(oid) { + assert(typeof oid === 'string'); + + switch (oid) { + case 'BasicConstraints': + return BasicConstraints; + case 'RSAPublicKey': + return RSAPublicKey; + case 'SubjectAltName': + return SubjectAltName; + case 'KeyUsage': + return KeyUsage; + default: + return null; + } } /* @@ -716,3 +1405,8 @@ exports.SubjectPublicKeyInfo = SubjectPublicKeyInfo; exports.Extensions = Extensions; exports.Extension = Extension; exports.DigestInfo = DigestInfo; +exports.BasicConstraints = BasicConstraints; +exports.RSAPublicKey = RSAPublicKey; +exports.SubjectAltName = SubjectAltName; +exports.KeyUsage = KeyUsage; +exports.Entity = Entity; diff --git a/lib/internal/objects.js b/lib/internal/objects.js index f34395dc8..ce3e199b3 100644 --- a/lib/internal/objects.js +++ b/lib/internal/objects.js @@ -36,7 +36,8 @@ const attrs = { LOCALITY: '2.5.4.7', PROVINCE: '2.5.4.8', STREETADDRESS: '2.5.4.9', - POSTALCODE: '2.5.4.17' + POSTALCODE: '2.5.4.17', + EMAILADDRESS: '1.2.840.113549.1.9.1' }; const attrsByVal = { @@ -48,14 +49,15 @@ const attrsByVal = { [attrs.LOCALITY]: 'LOCALITY', [attrs.PROVINCE]: 'PROVINCE', [attrs.STREETADDRESS]: 'STREETADDRESS', - [attrs.POSTALCODE]: 'POSTALCODE' + [attrs.POSTALCODE]: 'POSTALCODE', + [attrs.EMAILADDRESS]: 'EMAILADDRESS' }; const keyAlgs = { DH: '1.2.840.113549.1.3.1', DSA: '1.2.840.10040.4.1', DSA_ALT: '1.2.840.10040.4.2', - RSA: '1.2.840.113549.1.1.1', + RSAPublicKey: '1.2.840.113549.1.1.1', ECDSA: '1.2.840.10045.2.1', EDDSA: '1.3.6.1.4.1.11591.4.12.1' }; @@ -64,7 +66,7 @@ const keyAlgsByVal = { [keyAlgs.DH]: 'DH', [keyAlgs.DSA]: 'DSA', [keyAlgs.DSA_ALT]: 'DSA', - [keyAlgs.RSA]: 'RSA', + [keyAlgs.RSAPublicKey]: 'RSAPublicKey', [keyAlgs.ECDSA]: 'ECDSA', [keyAlgs.EDDSA]: 'EDDSA' }; @@ -235,6 +237,7 @@ const sigAlgsByVal = { [sigAlgs.ECDSASHA1]: 'ECDSASHA1', [sigAlgs.ECDSASHA224]: 'ECDSASHA224', [sigAlgs.ECDSASHA384]: 'ECDSASHA384', + [sigAlgs.ECDSASHA256]: 'ECDSASHA256', [sigAlgs.ECDSASHA512]: 'ECDSASHA512', [sigAlgs.EDDSA]: 'EDDSA' }; @@ -261,6 +264,42 @@ const sigToHash = { [sigAlgs.EDDSA]: null }; +const extensions = { + SubjectKeyIdentifier: '2.5.29.14', + KeyUsage: '2.5.29.15', + AuthorityKeyIdentifier: '2.5.29.35', + ExtendedKeyUsage: '2.5.29.37', + AuthorityInfoAccessKeyId: '2.5.29.35', + BasicConstraints: '2.5.29.19', + SubjectAltName: '2.5.29.17', + PrivateKeyUsagePeriod: '2.5.29.16', + CertificatePolicies: '2.5.29.32', + NameConstraints: '2.5.29.30', + CRLDistributionPoints: '2.5.29.31', + CRLNumber: '2.5.29.20', + AuthorityInfoAccess: '1.3.6.1.5.5.7.1.1', + EntrustVersInfo: '1.2.840.113533.7.65.0', + HashedRootKey: '2.23.42.7.0' +}; + +const extensionsByVal = { + [extensions.SubjectKeyIdentifier]: 'SubjectKeyIdentifier', + [extensions.KeyUsage]: 'KeyUsage', + [extensions.AuthorityKeyIdentifier]: 'AuthorityKeyIdentifier', + [extensions.ExtendedKeyUsage]: 'ExtendedKeyUsage', + [extensions.AuthorityKeyId]: 'AuthorityKeyId', + [extensions.BasicConstraints]: 'BasicConstraints', + [extensions.SubjectAltName]: 'SubjectAltName', + [extensions.PrivateKeyUsagePeriod]: 'PrivateKeyUsagePeriod', + [extensions.CertificatePolicies]: 'CertificatePolicies', + [extensions.NameConstraints]: 'NameConstraints', + [extensions.CRLDistributionPoints]: 'CRLDistributionPoints', + [extensions.CRLNumber]: 'CRLNumber', + [extensions.AuthorityInfoAccess]: 'AuthorityInfoAccess', + [extensions.EntrustVersInfo]: 'EntrustVersInfo', + [extensions.HashedRootKey]: 'HashedRootKey' +}; + /* * Expose */ @@ -277,3 +316,5 @@ exports.curvesByVal = curvesByVal; exports.sigAlgs = sigAlgs; exports.sigAlgsByVal = sigAlgsByVal; exports.sigToHash = sigToHash; +exports.extensions = extensions; +exports.extensionsByVal = extensionsByVal; diff --git a/test/data/x509/README.md b/test/data/x509/README.md new file mode 100644 index 000000000..6b7d66d1c --- /dev/null +++ b/test/data/x509/README.md @@ -0,0 +1,27 @@ +### Generate self-signed x509 Certificate from new private key + +``` +$ openssl version +LibreSSL 2.6.5 + +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout privateKey.key -out certificate.crt +Generating a 2048 bit RSA private key +.......................+++ +........+++ +writing new private key to 'privateKey.key' +----- +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) []:US +State or Province Name (full name) []:CA +Locality Name (eg, city) []:San Francisco +Organization Name (eg, company) []:bcrypto +Organizational Unit Name (eg, section) []:encodings +Common Name (eg, fully qualified host name) []:https://bcoin.io +Email Address []:satoshi@bcoin.io +``` diff --git a/test/data/x509/certificate.crt b/test/data/x509/certificate.crt new file mode 100644 index 000000000..e50b42c12 --- /dev/null +++ b/test/data/x509/certificate.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDpjCCAo4CCQDb/+svSNnR4TANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC +VVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRAwDgYDVQQK +DAdiY3J5cHRvMRIwEAYDVQQLDAllbmNvZGluZ3MxGTAXBgNVBAMMEGh0dHBzOi8v +YmNvaW4uaW8xHzAdBgkqhkiG9w0BCQEWEHNhdG9zaGlAYmNvaW4uaW8wHhcNMjAw +NDIwMTg1MzI1WhcNMjEwNDIwMTg1MzI1WjCBlDELMAkGA1UEBhMCVVMxCzAJBgNV +BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKDAdiY3J5cHRv +MRIwEAYDVQQLDAllbmNvZGluZ3MxGTAXBgNVBAMMEGh0dHBzOi8vYmNvaW4uaW8x +HzAdBgkqhkiG9w0BCQEWEHNhdG9zaGlAYmNvaW4uaW8wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCkPFZqd/yIa2iXezC6ZzheEdGsHqbeFPcGJSCxbLB7 +G5Bwv2hHtMi5AapWEIupeeluSdh1kRvmD/lNaa/UIxaVV77jH+EzLuLxuPrMq0yF +DiwBRhMg00nShSzdbOtTwWqEeQczISyw2LBkUDrHX9Ybhi2DDFPUdX7od+iYLpG+ +KriWajvgPLO1P5jso77gYmpAPsjxH+ro44nnYC2r1AC5tGRBfYzhbwdkeTq9JsPT +brpMCJVHaheeq9DDwidWqH9bAw1uNp1a/JwxuanVdkCoMgPZiLT5pKTLb0inVzzv +VAXH+fi1JKoWcOPM7f7H8wKxKJScylxKREHzKd6JKMBdAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAJI2colRuRTNx4r71pOJrkVzoRx240JVopQn3/6l5+LFDEjMq7ak +z1DDUoGfQYqkGR/WIIpLfuNI7mPsUsdk2oNHKI1dVc9doaGqXC0O9cStLFum5Xt8 +wKmpQnZtgHIQeUguKcLrIyCVcofORmcCpWLcLjxwu0XJvpJIPQ/EduPftyUH3SjL +SSR1Uoz1/FIzH8TxiRYu4p5F9y56YkGkuoFpFmSUMItUp8ZPSgQ5CbZa+HPvTYny +tDLRKJoA/voxgCL+GKycAMFLAh4YJ4u8ladMzKjOQVbjiAsqhwBDOqYU2U+iJyqD +nGjiYXuscrQ8YGv1sUKR9BsNVOWLJZfdTzM= +-----END CERTIFICATE----- diff --git a/test/data/x509/privateKey.key b/test/data/x509/privateKey.key new file mode 100644 index 000000000..b70d6d93d --- /dev/null +++ b/test/data/x509/privateKey.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCkPFZqd/yIa2iX +ezC6ZzheEdGsHqbeFPcGJSCxbLB7G5Bwv2hHtMi5AapWEIupeeluSdh1kRvmD/lN +aa/UIxaVV77jH+EzLuLxuPrMq0yFDiwBRhMg00nShSzdbOtTwWqEeQczISyw2LBk +UDrHX9Ybhi2DDFPUdX7od+iYLpG+KriWajvgPLO1P5jso77gYmpAPsjxH+ro44nn +YC2r1AC5tGRBfYzhbwdkeTq9JsPTbrpMCJVHaheeq9DDwidWqH9bAw1uNp1a/Jwx +uanVdkCoMgPZiLT5pKTLb0inVzzvVAXH+fi1JKoWcOPM7f7H8wKxKJScylxKREHz +Kd6JKMBdAgMBAAECggEAGYIxndnDBGFCXnzIxbrwe1PjMAuWuVmiQHcVy9jo1EYk +ug5QoQqIPovuq+1n88u2oOWhVClUnvgOLaLjp1xHzqxRPq+d57g5kXe8pHtaqDPa +S9xZbjjC94qtvDqFln2tEKRHpP5bFaCiTQJVDtBozd+aRAdvIcmaC3tMSg65nkJ+ +tKq/UvW2Sp9T2QSckTOki9zQTrJRVpK58YHkvLtt8HKqWxdjRbozfCAAjIvK0HXc ++xwznme5dgakIV9Ymq+fbv4TuPW+kz274upLKF6v/dZS6VGbKLJRW82u+0D9aOUV +JZ14jEGJCLsTYo3jl3SY8jguQOn/74bFP6oisiDU1QKBgQDOTFx3CVSISaYOGBMH +gdZPGykUFisEU/qIkD02PJyhqhcJgYtna31jdfDsIN3zr+37TDtGixY0y2jr1OIk +qeohnhv27awM8hMgqYR34t7VF95xefFaOstVbVo5fq7LZY6CtZ427VVqpnrHTxY9 +2VQtLN/RbukwtoARX0/3cpaslwKBgQDLzboSDomTJ6eQZ6ewQbH2fSaAlMXeoxfS +Oph8SQJwcU525gd217bpApb6RPSnFnvBXS29IMrl+amaKZAPTJcUQoLFvsALV604 +aoBXR0ZabG4Cs5Qfj3Rh5l45tsFTPEw2n7XDmSEg2TXp2WGYm7jNwAMJBJPDSvcM +YEJdsCW1KwKBgQDEqa8MG2zmb69JiFG7ul6fEvlaGLfxbE1NmpN//NAknY1Qlppa +gPILuwdtUvfDs1dfVRC88yK8OZ1QFKVw5jo4yg1GnNSDktIBNRj/YNuksRhxfRpE +NgBY8+IrJUVkyO/OU2z8V8wx23r4PcCqPWAtoLXNZboPoir8ZKxK7IYPCQKBgQC6 +PJT8SyyMvH/zBcXG20G07Uhx14G5oW/zPHh7mnwQJHp/TFUl1Jng8+zjZn/q8DDG +0k8ptP20iiDiL3jlgifM67p02YrE0qoIE8qT9x3jI5KkBVYmQQEpNUqFkuu3FDLQ +98ExrI0JZ3RM7cixnBuUaRJc+0HMBIUdWhlLY9wRYQKBgQC+NQSlaV1HSWU+Zud7 +ZOcKapMtuiD0RvcDNLo/2pvwUytbloogTBj91GcoTK4T936fiypAVYD+JFWHa+HZ +Yp5Z04kgIMwSz0IhVT3GD9BJmdwmgwwF0w1SHzQf+ydYzzo1XAEGj5LISnlO6UFj +2h+zpgN5ASq/xiwi8rGT/WjBaw== +-----END PRIVATE KEY----- diff --git a/test/x509-test.js b/test/x509-test.js index b201880d8..ae960b45d 100644 --- a/test/x509-test.js +++ b/test/x509-test.js @@ -5,34 +5,209 @@ const fs = require('fs'); const Path = require('path'); const x509 = require('../lib/encoding/x509'); const pem = require('../lib/encoding/pem'); +const rsa = require('../lib/rsa'); +const sha256 = require('../lib/sha256'); -const file = Path.resolve(__dirname, 'data', 'certs.pem'); -const data = fs.readFileSync(file, 'utf8'); +const certs = Path.resolve(__dirname, 'data', 'certs.pem'); +const certsData = fs.readFileSync(certs, 'utf8'); -function clear(crt) { - crt.raw = null; - crt.tbsCertificate.raw = null; - crt.tbsCertificate.subjectPublicKeyInfo.raw = null; -} +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(); let i = 0; - - for (const block of pem.decode(data)) { + for (const block of pem.decode(certsData)) { it(`should deserialize and reserialize certificate (${i++})`, () => { const crt1 = x509.Certificate.decode(block.data); const raw1 = crt1.encode(); const crt2 = x509.Certificate.decode(raw1); const raw2 = crt2.encode(); - clear(crt1); - clear(crt2); - assert.deepStrictEqual(crt1, crt2); assert.bufferEqual(raw1, raw2); + assert.bufferEqual(raw1, block.data); + }); + + it(`should read JSON and write JSON (${i})`, () => { + const crt1 = x509.Certificate.decode(block.data); + const json1 = crt1.getJSON(); + + const crt2 = x509.Certificate.fromJSON(json1); + const raw2 = crt2.encode(); + const json2 = crt1.getJSON(); + + assert.deepStrictEqual(json1, json2); + assert.bufferEqual(raw2, block.data); + }); + } + + i = 0; + for (const block of pem.decode(certificateData)) { + it(`should verify self-signed certificate from JSON (${i++})`, () => { + const crt1 = x509.Certificate.decode(block.data); + const json1 = crt1.getJSON(); + + const keyInfo = json1.tbsCertificate.subjectPublicKeyInfo; + if (keyInfo.algorithm.algorithm !== 'RSAPublicKey') + this.skip(); + + const key = rsa.publicKeyImport({ + n: Buffer.from(keyInfo.publicKey.modulus, 'hex'), + e: Buffer.from(keyInfo.publicKey.publicExponent, 'hex') + }); + + const sigAlg = json1.signatureAlgorithm.algorithm; + if (sigAlg !== 'RSASHA256') + this.skip(); + + const sig = Buffer.from(json1.signature.value, 'hex'); + + const r = rsa.verify( + 'SHA256', + sha256.digest(crt1.tbsCertificate.encode()), + sig, + key); + 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', + parameters: { + type: 'NULL', + node: null + } + }, + 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', + parameters: { + type: 'NULL', + node: null + } + }, + 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} + }, + { + extnID: 'KeyUsage', + critical: false, + extnValue: [ + 'digitalSignature', + 'nonRepudiation', + 'keyEncipherment', + 'dataEncipherment' + ] + } + ] + }; + + // 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', + parameters: { + type: 'NULL', + node: null + }}); + 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)); + }); + }); });