From 22ed4c9d1328024929259b25ceccf851a8d486de Mon Sep 17 00:00:00 2001 From: Sam Roberts Date: Tue, 13 Nov 2018 16:10:31 -0800 Subject: [PATCH 1/3] doc: describe certificate object properties --- doc/api/tls.md | 128 ++++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/doc/api/tls.md b/doc/api/tls.md index 5655f21bd6f0cc..3f52b7872cbbd1 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -638,44 +638,84 @@ added: v0.11.4 * `detailed` {boolean} Include the full certificate chain if `true`, otherwise include just the peer's certificate. -* Returns: {Object} +* Returns: {Object} A certificate object. -Returns an object representing the peer's certificate. The returned object has -some properties corresponding to the fields of the certificate. +Returns an object representing the peer's certificate. If the peer does not +provide a certificate, an empty object will be returned. If the socket has been +destroyed, `null` will be returned. If the full certificate chain was requested, each certificate will include an `issuerCertificate` property containing an object representing its issuer's certificate. +#### Certificate Object + +A certificate object has properties corresponding to the fields of the +certificate. + +* `raw` {Buffer} The DER encoded X.509 certificate data. +* `subject` {Object} The certificate subject, described in terms of + Country (`C:`), StateOrProvince (`ST`), Locality (`L`), Organization (`O`), + OrganizationalUnit (`OU`), and CommonName (`CN`). The CommonName is typically + a DNS name with TLS certificates. Example: + `{C: 'UK', ST: 'BC', L: 'Metro', O: 'Node Fans', OU: 'Docs', CN: 'example.com'}`. +* `issuer` {Object} The certificate issuer, described in the same terms as the + `subject`. +* `valid_from` {string} The date-time the certificate is valid from. +* `valid_to` {string} The date-time the certificate is valid to. +* `serialNumber` {string} The certificate serial number, as a hex string. + Example: `'B9B0D332A1AA5635'`. +* `fingerprint` {string} The SHA-1 digest of the DER encoded certificate. It is + returned as a `:` separated hexadecimal string. Example: `'2A:7A:C2:DD:...'`. +* `fingerprint256` {string} The SHA-256 digest of the DER encoded certificate. + It is returned as a `:` separated hexadecimal string. Example: + `'2A:7A:C2:DD:...'`. +* `ext_key_usage` {Array} (Optional) The extended key usage, a set of OIDs. +* `subjectaltname` {Array} (Optional) An array of names for the subject, an + alternative to the `subject` names. +* `infoAccess` {Array} (Optional) An array describing the AuthorityInfoAccess, + used with OCSP. +* `issuerCert` {Object} (Optional) The issuer certificate object. For + self-signed certificates, this may be a circular reference. + +The certificate may contain information about the public key, depending on +the key type. + +For RSA keys, the following properties may be defined: +* `exponent` {string} The RSA exponent, as a string in hexadecimal number + notation. Example: `'0x010001'`. +* `modulus` {string} The RSA modulus, as a hexadecimal string. Example: + `'B56CE45CB7...'`. +* `pubkey` {Buffer} The public key. + + ```text { subject: - { C: 'UK', - ST: 'Acknack Ltd', - L: 'Rhys Jones', - O: 'node.js', - OU: 'Test TLS Certificate', - CN: 'localhost' }, + { OU: [ 'Domain Control Validated', 'PositiveSSL Wildcard' ], + CN: '*.nodejs.org' }, issuer: - { C: 'UK', - ST: 'Acknack Ltd', - L: 'Rhys Jones', - O: 'node.js', - OU: 'Test TLS Certificate', - CN: 'localhost' }, - issuerCertificate: - { ... another certificate, possibly with an .issuerCertificate ... }, - raw: < RAW DER buffer >, - pubkey: < RAW DER buffer >, - valid_from: 'Nov 11 09:52:22 2009 GMT', - valid_to: 'Nov 6 09:52:22 2029 GMT', - fingerprint: '2A:7A:C2:DD:E5:F9:CC:53:72:35:99:7A:02:5A:71:38:52:EC:8A:DF', - fingerprint256: '2A:7A:C2:DD:E5:F9:CC:53:72:35:99:7A:02:5A:71:38:52:EC:8A:DF:00:11:22:33:44:55:66:77:88:99:AA:BB', - serialNumber: 'B9B0D332A1AA5635' } + { C: 'GB', + ST: 'Greater Manchester', + L: 'Salford', + O: 'COMODO CA Limited', + CN: 'COMODO RSA Domain Validation Secure Server CA' }, + subjectaltname: 'DNS:*.nodejs.org, DNS:nodejs.org', + infoAccess: + { 'CA Issuers - URI': + [ 'http://crt.comodoca.com/COMODORSADomainValidationSecureServerCA.crt' ], + 'OCSP - URI': [ 'http://ocsp.comodoca.com' ] }, + modulus: 'B56CE45CB740B09A13F64AC543B712FF9EE8E4C284B542A1708A27E82A8D151CA178153E12E6DDA15BF70FFD96CB8A88618641BDFCCA03527E665B70D779C8A349A6F88FD4EF6557180BD4C98192872BCFE3AF56E863C09DDD8BC1EC58DF9D94F914F0369102B2870BECFA1348A0838C9C49BD1C20124B442477572347047506B1FCD658A80D0C44BCC16BC5C5496CFE6E4A8428EF654CD3D8972BF6E5BFAD59C93006830B5EB1056BBB38B53D1464FA6E02BFDF2FF66CD949486F0775EC43034EC2602AEFBF1703AD221DAA2A88353C3B6A688EFE8387811F645CEED7B3FE46E1F8B9F59FAD028F349B9BC14211D5830994D055EEA3D547911E07A0ADDEB8A82B9188E58720D95CD478EEC9AF1F17BE8141BE80906F1A339445A7EB5B285F68039B0F294598A7D1C0005FC22B5271B0752F58CCDEF8C8FD856FB7AE21C80B8A2CE983AE94046E53EDE4CB89F42502D31B5360771C01C80155918637490550E3F555E2EE75CC8C636DDE3633CFEDD62E91BF0F7688273694EEEBA20C2FC9F14A2A435517BC1D7373922463409AB603295CEB0BB53787A334C9CA3CA8B30005C5A62FC0715083462E00719A8FA3ED0A9828C3871360A73F8B04A4FC1E71302844E9BB9940B77E745C9D91F226D71AFCAD4B113AAF68D92B24DDB4A2136B55A1CD1ADF39605B63CB639038ED0F4C987689866743A68769CC55847E4A06D6E2E3F1', + exponent: '0x10001', + pubkey: , + valid_from: 'Aug 14 00:00:00 2017 GMT', + valid_to: 'Nov 20 23:59:59 2019 GMT', + fingerprint: '01:02:59:D9:C3:D2:0D:08:F7:82:4E:44:A4:B4:53:C5:E2:3A:87:4D', + fingerprint256: '69:AE:1A:6A:D4:3D:C6:C1:1B:EA:C6:23:DE:BA:2A:14:62:62:93:5C:7A:EA:06:41:9B:0B:BC:87:CE:48:4E:02', + ext_key_usage: [ '1.3.6.1.5.5.7.3.1', '1.3.6.1.5.5.7.3.2' ], + serialNumber: '66593D57F20CBC573E433381B5FEC280', + raw: } ``` -If the peer does not provide a certificate, an empty object will be returned. -If the socket has been destroyed, `null` will be returned. - ### tlsSocket.getPeerFinished() A certificate object has properties corresponding to the fields of the certificate. @@ -688,7 +694,18 @@ For RSA keys, the following properties may be defined: `'B56CE45CB7...'`. * `pubkey` {Buffer} The public key. - +For EC keys, the following properties may be defined: +* `pubkey` {Buffer} The public key. +* `bits` {number} The key size in bits. Example: `256`. +* `asn1Curve` {string} (Optional) The ASN.1 name of the OID of the elliptic + curve. Well-known curves are identified by an OID. While it is unusual, it is + possible that the curve is identified by its mathematical properties, in which + case it will not have an OID. Example: `'prime256v1'`. +* `nistCurve` {string} (Optional) The NIST name for the elliptic curve, if it + has one (not all well-known curves have been assigned names by NIST). Example: + `'P-256'`. + +Example certificate: ```text { subject: { OU: [ 'Domain Control Validated', 'PositiveSSL Wildcard' ], diff --git a/src/env.h b/src/env.h index 6bed104dbb4f31..c99f1d68301b6f 100644 --- a/src/env.h +++ b/src/env.h @@ -124,7 +124,9 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(address_string, "address") \ V(aliases_string, "aliases") \ V(args_string, "args") \ + V(asn1curve_string, "asn1Curve") \ V(async_ids_stack_string, "async_ids_stack") \ + V(bits_string, "bits") \ V(buffer_string, "buffer") \ V(bytes_parsed_string, "bytesParsed") \ V(bytes_read_string, "bytesRead") \ @@ -207,6 +209,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(modulus_string, "modulus") \ V(name_string, "name") \ V(netmask_string, "netmask") \ + V(nistcurve_string, "nistCurve") \ V(nsname_string, "nsname") \ V(ocsp_request_string, "OCSPRequest") \ V(onaltsvc_string, "onaltsvc") \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index cf508213142cb9..91583c18d97549 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -52,6 +52,15 @@ static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL | XN_FLAG_FN_SN; namespace node { +namespace Buffer { +// OpenSSL uses `unsigned char*` for raw data, make this easier for us. +v8::MaybeLocal New(Environment* env, unsigned char* udata, + size_t length) { + char* data = reinterpret_cast(udata); + return Buffer::New(env, data, length); +} +} // namespace Buffer + namespace crypto { using v8::Array; @@ -1652,8 +1661,17 @@ static Local X509ToObject(Environment* env, X509* cert) { EVPKeyPointer pkey(X509_get_pubkey(cert)); RSAPointer rsa; - if (pkey) - rsa.reset(EVP_PKEY_get1_RSA(pkey.get())); + ECPointer ec; + if (pkey) { + switch (EVP_PKEY_id(pkey.get())) { + case EVP_PKEY_RSA: + rsa.reset(EVP_PKEY_get1_RSA(pkey.get())); + break; + case EVP_PKEY_EC: + ec.reset(EVP_PKEY_get1_EC_KEY(pkey.get())); + break; + } + } if (rsa) { const BIGNUM* n; @@ -1689,10 +1707,53 @@ static Local X509ToObject(Environment* env, X509* cert) { reinterpret_cast(Buffer::Data(pubbuff)); i2d_RSA_PUBKEY(rsa.get(), &pubserialized); info->Set(env->context(), env->pubkey_string(), pubbuff).FromJust(); + } else if (ec) { + const EC_GROUP* group = EC_KEY_get0_group(ec.get()); + if (group != nullptr) { + int bits = EC_GROUP_order_bits(group); + if (bits > 0) { + info->Set(context, env->bits_string(), + Integer::New(env->isolate(), bits)).FromJust(); + } + } + + unsigned char* pub = nullptr; + size_t publen = EC_KEY_key2buf(ec.get(), EC_KEY_get_conv_form(ec.get()), + &pub, nullptr); + if (publen > 0) { + Local buf = Buffer::New(env, pub, publen).ToLocalChecked(); + // Ownership of pub pointer accepted by Buffer. + pub = nullptr; + info->Set(context, env->pubkey_string(), buf).FromJust(); + } else { + CHECK_NULL(pub); + } + + if (EC_GROUP_get_asn1_flag(group) != 0) { + // Curve is well-known, get its OID and NIST nick-name (if it has one). + + int nid = EC_GROUP_get_curve_name(group); + if (nid != 0) { + if (const char* sn = OBJ_nid2sn(nid)) { + info->Set(context, env->asn1curve_string(), + OneByteString(env->isolate(), sn)).FromJust(); + } + } + if (nid != 0) { + if (const char* nist = EC_curve_nid2nist(nid)) { + info->Set(context, env->nistcurve_string(), + OneByteString(env->isolate(), nist)).FromJust(); + } + } + } else { + // Unnamed curves can be described by their mathematical properties, + // but aren't used much (at all?) with X.509/TLS. Support later if needed. + } } pkey.reset(); rsa.reset(); + ec.reset(); ASN1_TIME_print(bio.get(), X509_get_notBefore(cert)); BIO_get_mem_ptr(bio.get(), &mem); diff --git a/src/node_crypto.h b/src/node_crypto.h index f85cdd32081895..5f98af754ea727 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -81,6 +81,7 @@ using EVPKeyPointer = DeleteFnPtr; using EVPKeyCtxPointer = DeleteFnPtr; using EVPMDPointer = DeleteFnPtr; using RSAPointer = DeleteFnPtr; +using ECPointer = DeleteFnPtr; using BignumPointer = DeleteFnPtr; using NetscapeSPKIPointer = DeleteFnPtr; using ECGroupPointer = DeleteFnPtr; diff --git a/test/parallel/test-tls-peer-certificate.js b/test/parallel/test-tls-peer-certificate.js index 0b820b93eb2ada..2a48665e4d9357 100644 --- a/test/parallel/test-tls-peer-certificate.js +++ b/test/parallel/test-tls-peer-certificate.js @@ -86,3 +86,48 @@ connect({ return cleanup(); }); + +connect({ + client: { rejectUnauthorized: false }, + server: keys.ec, +}, function(err, pair, cleanup) { + assert.ifError(err); + const socket = pair.client.conn; + let peerCert = socket.getPeerCertificate(true); + assert.ok(peerCert.issuerCertificate); + + peerCert = socket.getPeerCertificate(true); + debug('peerCert:\n', peerCert); + + assert.ok(peerCert.issuerCertificate); + assert.strictEqual(peerCert.subject.emailAddress, 'ry@tinyclouds.org'); + assert.strictEqual(peerCert.serialNumber, 'C1EA7B03D5956D52'); + assert.strictEqual(peerCert.exponent, undefined); + assert.strictEqual(peerCert.pubKey, undefined); + assert.strictEqual(peerCert.modulus, undefined); + assert.strictEqual( + peerCert.fingerprint, + 'DF:F0:D3:6B:C3:E7:74:7C:C7:F3:FB:1E:33:12:AE:6C:8D:53:5F:74' + ); + assert.strictEqual( + peerCert.fingerprint256, + 'AB:08:3C:40:C7:07:D7:D1:79:32:92:3B:96:52:D0:38:4C:22:ED:CD:23:51:D0:A1:' + + '67:AA:33:A0:D5:26:5C:41' + ); + + assert.strictEqual( + sha256(peerCert.pubkey).digest('hex'), + 'ec68fc7d5e32cd4e1da5a7b59c0a2229be6f82fcc9bf8c8691a2262aacb14f53' + ); + assert.strictEqual(peerCert.asn1Curve, 'prime256v1'); + assert.strictEqual(peerCert.nistCurve, 'P-256'); + assert.strictEqual(peerCert.bits, 256); + + assert.deepStrictEqual(peerCert.infoAccess, undefined); + + const issuer = peerCert.issuerCertificate; + assert.strictEqual(issuer.issuerCertificate, issuer); + assert.strictEqual(issuer.serialNumber, 'C1EA7B03D5956D52'); + + return cleanup(); +}); From 51f4e0b5d85810fbce9a3333de3bfcc5edc3dc69 Mon Sep 17 00:00:00 2001 From: Sam Roberts Date: Thu, 15 Nov 2018 10:11:29 -0800 Subject: [PATCH 3/3] tls: include RSA bit size in X.509 public key info For symmetricality with the EC public key info, and because its useful. --- doc/api/tls.md | 1 + src/node_crypto.cc | 4 ++++ test/parallel/test-tls-peer-certificate.js | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/doc/api/tls.md b/doc/api/tls.md index 877aee128ec14a..69cfae438b4470 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -688,6 +688,7 @@ The certificate may contain information about the public key, depending on the key type. For RSA keys, the following properties may be defined: +* `bits` {number} The RSA bit size. Example: `1024`. * `exponent` {string} The RSA exponent, as a string in hexadecimal number notation. Example: `'0x010001'`. * `modulus` {string} The RSA modulus, as a hexadecimal string. Example: diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 91583c18d97549..7e168c6fa4272b 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -1685,6 +1685,10 @@ static Local X509ToObject(Environment* env, X509* cert) { mem->length).ToLocalChecked()).FromJust(); USE(BIO_reset(bio.get())); + int bits = BN_num_bits(n); + info->Set(context, env->bits_string(), + Integer::New(env->isolate(), bits)).FromJust(); + uint64_t exponent_word = static_cast(BN_get_word(e)); uint32_t lo = static_cast(exponent_word); uint32_t hi = static_cast(exponent_word >> 32); diff --git a/test/parallel/test-tls-peer-certificate.js b/test/parallel/test-tls-peer-certificate.js index 2a48665e4d9357..523638d4a39649 100644 --- a/test/parallel/test-tls-peer-certificate.js +++ b/test/parallel/test-tls-peer-certificate.js @@ -55,6 +55,11 @@ connect({ assert.strictEqual(peerCert.subject.emailAddress, 'ry@tinyclouds.org'); assert.strictEqual(peerCert.serialNumber, 'ECC9B856270DA9A8'); assert.strictEqual(peerCert.exponent, '0x10001'); + assert.strictEqual(peerCert.bits, 1024); + // The conversion to bits is odd because modulus isn't a buffer, its a hex + // string. There are two hex chars for every byte of modulus, and 8 bits per + // byte. + assert.strictEqual(peerCert.modulus.length / 2 * 8, peerCert.bits); assert.strictEqual( peerCert.fingerprint, 'D7:FD:F6:42:92:A8:83:51:8E:80:48:62:66:DA:85:C2:EE:A6:A1:CD'