From 37f0bd7e3a3ff1705be81c4d938cafff9f8e36fe Mon Sep 17 00:00:00 2001 From: Sam Roberts Date: Fri, 9 Nov 2018 15:05:34 -0800 Subject: [PATCH] tls: include elliptic curve X.509 public key info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X.509 certs are provided to the user in a parsed object form by a number of TLS APIs. Include public key info for elliptic curves as well, not just RSA. - pubkey: the public key - bits: the strength of the curve - asn1Curve: the ASN.1 OID for the curve - nistCurve: the NIST nickname for the curve, if it has one PR-URL: https://github.com/nodejs/node/pull/24358 Reviewed-By: Ben Noordhuis Reviewed-By: Tobias Nießen --- doc/api/tls.md | 19 ++++++- src/env.h | 3 + src/node_crypto.cc | 65 +++++++++++++++++++++- src/node_crypto.h | 1 + test/parallel/test-tls-peer-certificate.js | 45 +++++++++++++++ 5 files changed, 130 insertions(+), 3 deletions(-) diff --git a/doc/api/tls.md b/doc/api/tls.md index 7af82eb5496a48..4c6d37224dbb62 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -649,6 +649,12 @@ If the full certificate chain was requested, each certificate will include an certificate. #### Certificate Object + 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 972e1d0f37cccd..41565da1563805 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; @@ -1629,8 +1638,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; @@ -1666,10 +1684,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(); +});