diff --git a/node.gyp b/node.gyp index 23134d1056..62b393b7d2 100644 --- a/node.gyp +++ b/node.gyp @@ -801,12 +801,14 @@ [ 'node_use_openssl=="true"', { 'sources': [ 'src/node_crypto.cc', + 'src/node_crypto_common.cc', 'src/node_crypto_bio.cc', 'src/node_crypto_clienthello.cc', 'src/node_crypto.h', 'src/node_crypto_bio.h', 'src/node_crypto_clienthello.h', 'src/node_crypto_clienthello-inl.h', + 'src/node_crypto_common.h', 'src/node_crypto_groups.h', 'src/tls_wrap.cc', 'src/tls_wrap.h', diff --git a/src/node_crypto.cc b/src/node_crypto.cc index bae09d65d6..6d91b57c2e 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -24,6 +24,7 @@ #include "node_crypto_bio.h" #include "node_crypto_clienthello-inl.h" #include "node_crypto_groups.h" +#include "node_crypto_common.h" #include "node_errors.h" #include "node_mutex.h" #include "node_process.h" @@ -2270,46 +2271,12 @@ void SSLWrap::GetPeerCertificate( const FunctionCallbackInfo& args) { Base* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - ClearErrorOnReturn clear_error_on_return; - - Local result; - // Used to build the issuer certificate chain. - Local issuer_chain; - - // NOTE: This is because of the odd OpenSSL behavior. On client `cert_chain` - // contains the `peer_certificate`, but on server it doesn't. - X509Pointer cert( - w->is_server() ? SSL_get_peer_certificate(w->ssl_.get()) : nullptr); - STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(w->ssl_.get()); - if (!cert && (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0)) - goto done; - - // Short result requested. - if (args.Length() < 1 || !args[0]->IsTrue()) { - result = X509ToObject(env, cert ? cert.get() : sk_X509_value(ssl_certs, 0)); - goto done; - } - - if (auto peer_certs = CloneSSLCerts(std::move(cert), ssl_certs)) { - // First and main certificate. - X509Pointer cert(sk_X509_value(peer_certs.get(), 0)); - CHECK(cert); - result = X509ToObject(env, cert.release()); - - issuer_chain = - AddIssuerChainToObject(&cert, result, std::move(peer_certs), env); - issuer_chain = GetLastIssuedCert(&cert, w->ssl_.get(), issuer_chain, env); - // Last certificate should be self-signed. - if (X509_check_issued(cert.get(), cert.get()) == X509_V_OK) - issuer_chain->Set(env->context(), - env->issuercert_string(), - issuer_chain).Check(); - } - - done: - args.GetReturnValue().Set(result); + args.GetReturnValue().Set( + GetPeerCert( + w->ssl_env(), + w->ssl_.get(), + args.Length() < 1 || !args[0]->IsTrue(), + w->is_server())); } @@ -2318,18 +2285,7 @@ void SSLWrap::GetCertificate( const FunctionCallbackInfo& args) { Base* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - ClearErrorOnReturn clear_error_on_return; - - Local result; - - X509* cert = SSL_get_certificate(w->ssl_.get()); - - if (cert != nullptr) - result = X509ToObject(env, cert); - - args.GetReturnValue().Set(result); + args.GetReturnValue().Set(GetCert(w->ssl_env(), w->ssl_.get())); } @@ -2416,14 +2372,8 @@ void SSLWrap::SetSession(const FunctionCallbackInfo& args) { ArrayBufferViewContents sbuf(args[0].As()); const unsigned char* p = sbuf.data(); - SSLSessionPointer sess(d2i_SSL_SESSION(nullptr, &p, sbuf.length())); - - if (sess == nullptr) - return; - - int r = SSL_set_session(w->ssl_.get(), sess.get()); - if (!r) + if (!SetTLSSession(w->ssl_.get(), p, sbuf.length())) return env->ThrowError("SSL_set_session error"); } @@ -2547,51 +2497,8 @@ void SSLWrap::GetEphemeralKeyInfo( if (w->is_server()) return args.GetReturnValue().SetNull(); - Local info = Object::New(env->isolate()); - - EVP_PKEY* raw_key; - if (SSL_get_server_tmp_key(w->ssl_.get(), &raw_key)) { - EVPKeyPointer key(raw_key); - int kid = EVP_PKEY_id(key.get()); - switch (kid) { - case EVP_PKEY_DH: - info->Set(context, env->type_string(), - FIXED_ONE_BYTE_STRING(env->isolate(), "DH")).Check(); - info->Set(context, env->size_string(), - Integer::New(env->isolate(), EVP_PKEY_bits(key.get()))) - .Check(); - break; - case EVP_PKEY_EC: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: - { - const char* curve_name; - if (kid == EVP_PKEY_EC) { - EC_KEY* ec = EVP_PKEY_get1_EC_KEY(key.get()); - int nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); - curve_name = OBJ_nid2sn(nid); - EC_KEY_free(ec); - } else { - curve_name = OBJ_nid2sn(kid); - } - info->Set(context, env->type_string(), - FIXED_ONE_BYTE_STRING(env->isolate(), "ECDH")).Check(); - info->Set(context, env->name_string(), - OneByteString(args.GetIsolate(), - curve_name)).Check(); - info->Set(context, env->size_string(), - Integer::New(env->isolate(), - EVP_PKEY_bits(key.get()))).Check(); - } - break; - default: - break; - } - } - // TODO(@sam-github) semver-major: else return ThrowCryptoError(env, - // ERR_get_error()) - - return args.GetReturnValue().Set(info); + return args.GetReturnValue().Set( + crypto::GetEphemeralKey(env, w->ssl_.get())); } @@ -2621,11 +2528,8 @@ void SSLWrap::VerifyError(const FunctionCallbackInfo& args) { // peer certificate is questionable but it's compatible with what was // here before. long x509_verify_error = // NOLINT(runtime/int) - X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT; - if (X509* peer_cert = SSL_get_peer_certificate(w->ssl_.get())) { - X509_free(peer_cert); - x509_verify_error = SSL_get_verify_result(w->ssl_.get()); - } + VerifyPeerCertificate(w->ssl_.get(), + X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT); if (x509_verify_error == X509_V_OK) return args.GetReturnValue().SetNull(); @@ -2687,12 +2591,10 @@ void SSLWrap::GetCipher(const FunctionCallbackInfo& args) { return; Local info = Object::New(env->isolate()); - const char* cipher_name = SSL_CIPHER_get_name(c); info->Set(context, env->name_string(), - OneByteString(args.GetIsolate(), cipher_name)).Check(); - const char* cipher_version = SSL_CIPHER_get_version(c); + GetCipherName(env, w->ssl_.get())).Check(); info->Set(context, env->version_string(), - OneByteString(args.GetIsolate(), cipher_version)).Check(); + GetCipherVersion(env, w->ssl_.get())).Check(); args.GetReturnValue().Set(info); } diff --git a/src/node_crypto_common.cc b/src/node_crypto_common.cc new file mode 100644 index 0000000000..7ad1337ae8 --- /dev/null +++ b/src/node_crypto_common.cc @@ -0,0 +1,417 @@ +#include "env-inl.h" +#include "node_crypto.h" +#include "node_crypto_common.h" +#include "node.h" +#include "node_internals.h" +#include "node_url.h" +#include "string_bytes.h" +#include "v8.h" + +#include +#include + +#include +#include + +namespace node { + +using v8::Array; +using v8::Context; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace crypto { + +void LogSecret( + SSL* ssl, + const char* name, + const unsigned char* secret, + size_t secretlen) { + if (auto keylog_cb = SSL_CTX_get_keylog_callback(SSL_get_SSL_CTX(ssl))) { + unsigned char crandom[32]; + if (SSL_get_client_random(ssl, crandom, 32) != 32) + return; + std::string line = name; + line += " " + StringBytes::hex_encode( + reinterpret_cast(crandom), 32); + line += " " + StringBytes::hex_encode( + reinterpret_cast(secret), secretlen); + keylog_cb(ssl, line.c_str()); + } +} + +void SetALPN(SSL* ssl, const std::string& alpn) { + CHECK_EQ(SSL_set_alpn_protos( + ssl, + reinterpret_cast(alpn.c_str()), + alpn.length()), 0); +} + +std::string GetSSLOCSPResponse(SSL* ssl) { + const unsigned char* resp; + int len = SSL_get_tlsext_status_ocsp_resp(ssl, &resp); + if (len < 0) len = 0; + return std::string(reinterpret_cast(resp), len); +} + +bool SetTLSSession(SSL* ssl, const unsigned char* buf, size_t length) { + SSLSessionPointer s(d2i_SSL_SESSION(nullptr, &buf, length)); + return s != nullptr && SSL_set_session(ssl, s.get()) == 1; +} + +std::unordered_multimap +GetCertificateAltNames( + X509* cert) { + std::unordered_multimap map; + crypto::BIOPointer bio(BIO_new(BIO_s_mem())); + BUF_MEM* mem; + int idx = X509_get_ext_by_NID(cert, NID_subject_alt_name, -1); + if (idx < 0) // There is no subject alt name + return map; + + X509_EXTENSION* ext = X509_get_ext(cert, idx); + CHECK_NOT_NULL(ext); + const X509V3_EXT_METHOD* method = X509V3_EXT_get(ext); + CHECK_EQ(method, X509V3_EXT_get_nid(NID_subject_alt_name)); + + GENERAL_NAMES* names = static_cast(X509V3_EXT_d2i(ext)); + if (names == nullptr) // There are no names + return map; + + for (int i = 0; i < sk_GENERAL_NAME_num(names); i++) { + USE(BIO_reset(bio.get())); + GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i); + if (gen->type == GEN_DNS) { + ASN1_IA5STRING* name = gen->d.dNSName; + BIO_write(bio.get(), name->data, name->length); + BIO_get_mem_ptr(bio.get(), &mem); + map.emplace("dns", std::string(mem->data, mem->length)); + } else { + STACK_OF(CONF_VALUE)* nval = i2v_GENERAL_NAME( + const_cast(method), gen, nullptr); + if (nval == nullptr) + continue; + X509V3_EXT_val_prn(bio.get(), nval, 0, 0); + sk_CONF_VALUE_pop_free(nval, X509V3_conf_free); + BIO_get_mem_ptr(bio.get(), &mem); + std::string value(mem->data, mem->length); + if (value.compare(0, 11, "IP Address:") == 0) { + map.emplace("ip", value.substr(11)); + } else if (value.compare(0, 4, "URI:") == 0) { + url::URL url(value.substr(4)); + if (url.flags() & url::URL_FLAGS_CANNOT_BE_BASE || + url.flags() & url::URL_FLAGS_FAILED) { + continue; // Skip this one + } + map.emplace("uri", url.host()); + } + } + } + sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free); + return map; +} + +std::string GetCertificateCN(X509* cert) { + X509_NAME* subject = X509_get_subject_name(cert); + if (subject != nullptr) { + int nid = OBJ_txt2nid("CN"); + int idx = X509_NAME_get_index_by_NID(subject, nid, -1); + if (idx != -1) { + X509_NAME_ENTRY* cn = X509_NAME_get_entry(subject, idx); + if (cn != nullptr) { + ASN1_STRING* cn_str = X509_NAME_ENTRY_get_data(cn); + if (cn_str != nullptr) { + return std::string(reinterpret_cast( + ASN1_STRING_get0_data(cn_str))); + } + } + } + } + return std::string(); +} + +int VerifyPeerCertificate(SSL* ssl, int def) { + int err = def; + if (X509* peer_cert = SSL_get_peer_certificate(ssl)) { + X509_free(peer_cert); + err = SSL_get_verify_result(ssl); + } + return err; +} + +int UseSNIContext(SSL* ssl, SecureContext* context) { + SSL_CTX* ctx = context->ctx_.get(); + X509* x509 = SSL_CTX_get0_certificate(ctx); + EVP_PKEY* pkey = SSL_CTX_get0_privatekey(ctx); + STACK_OF(X509)* chain; + + int err = SSL_CTX_get0_chain_certs(ctx, &chain); + if (err) + err = SSL_use_certificate(ssl, x509); + if (err) + err = SSL_use_PrivateKey(ssl, pkey); + if (err && chain != nullptr) + err = SSL_set1_chain(ssl, chain); + return err; +} + +const char* GetClientHelloALPN(SSL* ssl) { + const unsigned char* buf; + size_t len; + size_t rem; + + if (!SSL_client_hello_get0_ext( + ssl, + TLSEXT_TYPE_application_layer_protocol_negotiation, + &buf, &rem) || rem < 2) { + return nullptr; + } + + len = (buf[0] << 8) | buf[1]; + if (len + 2 != rem) + return nullptr; + buf += 3; + return reinterpret_cast(buf); +} + +const char* GetClientHelloServerName(SSL* ssl) { + const unsigned char* buf; + size_t len; + size_t rem; + + if (!SSL_client_hello_get0_ext( + ssl, + TLSEXT_TYPE_server_name, + &buf, + &rem) || rem <= 2) { + return nullptr; + } + + len = *(buf++) << 8; + len += *(buf++); + if (len + 2 != rem) + return nullptr; + rem = len; + + if (rem == 0 || *buf++ != TLSEXT_NAMETYPE_host_name) + return nullptr; + rem--; + if (rem <= 2) + return nullptr; + len = *(buf++) << 8; + len += *(buf++); + if (len + 2 > rem) + return nullptr; + rem = len; + return reinterpret_cast(buf); +} + +const char* GetServerName(SSL* ssl) { + return SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); +} + +Local GetClientHelloCiphers(Environment* env, SSL* ssl) { + const unsigned char* buf; + size_t len = SSL_client_hello_get0_ciphers(ssl, &buf); + std::vector> ciphers_array; + for (size_t n = 0; n < len; n += 2) { + const SSL_CIPHER* cipher = SSL_CIPHER_find(ssl, buf); + buf += 2; + const char* cipher_name = SSL_CIPHER_get_name(cipher); + const char* cipher_version = SSL_CIPHER_get_version(cipher); + Local obj = Object::New(env->isolate()); + obj->Set( + env->context(), + env->name_string(), + OneByteString(env->isolate(), cipher_name)).FromJust(); + obj->Set( + env->context(), + env->version_string(), + OneByteString(env->isolate(), cipher_version)).FromJust(); + ciphers_array.push_back(obj); + } + return Array::New(env->isolate(), ciphers_array.data(), ciphers_array.size()); +} + +bool SetGroups(SecureContext* sc, const char* groups) { + return SSL_CTX_set1_groups_list(**sc, groups) == 1; +} + +const char* X509ErrorCode(int err) { + const char* code = "UNSPECIFIED"; +#define CASE_X509_ERR(CODE) case X509_V_ERR_##CODE: code = #CODE; break; + switch (err) { + CASE_X509_ERR(UNABLE_TO_GET_ISSUER_CERT) + CASE_X509_ERR(UNABLE_TO_GET_CRL) + CASE_X509_ERR(UNABLE_TO_DECRYPT_CERT_SIGNATURE) + CASE_X509_ERR(UNABLE_TO_DECRYPT_CRL_SIGNATURE) + CASE_X509_ERR(UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY) + CASE_X509_ERR(CERT_SIGNATURE_FAILURE) + CASE_X509_ERR(CRL_SIGNATURE_FAILURE) + CASE_X509_ERR(CERT_NOT_YET_VALID) + CASE_X509_ERR(CERT_HAS_EXPIRED) + CASE_X509_ERR(CRL_NOT_YET_VALID) + CASE_X509_ERR(CRL_HAS_EXPIRED) + CASE_X509_ERR(ERROR_IN_CERT_NOT_BEFORE_FIELD) + CASE_X509_ERR(ERROR_IN_CERT_NOT_AFTER_FIELD) + CASE_X509_ERR(ERROR_IN_CRL_LAST_UPDATE_FIELD) + CASE_X509_ERR(ERROR_IN_CRL_NEXT_UPDATE_FIELD) + CASE_X509_ERR(OUT_OF_MEM) + CASE_X509_ERR(DEPTH_ZERO_SELF_SIGNED_CERT) + CASE_X509_ERR(SELF_SIGNED_CERT_IN_CHAIN) + CASE_X509_ERR(UNABLE_TO_GET_ISSUER_CERT_LOCALLY) + CASE_X509_ERR(UNABLE_TO_VERIFY_LEAF_SIGNATURE) + CASE_X509_ERR(CERT_CHAIN_TOO_LONG) + CASE_X509_ERR(CERT_REVOKED) + CASE_X509_ERR(INVALID_CA) + CASE_X509_ERR(PATH_LENGTH_EXCEEDED) + CASE_X509_ERR(INVALID_PURPOSE) + CASE_X509_ERR(CERT_UNTRUSTED) + CASE_X509_ERR(CERT_REJECTED) + CASE_X509_ERR(HOSTNAME_MISMATCH) + } +#undef CASE_X509_ERR + return code; +} + +Local GetValidationErrorReason(Environment* env, int err) { + const char* reason = X509_verify_cert_error_string(err); + return OneByteString(env->isolate(), reason); +} + +Local GetValidationErrorCode(Environment* env, int err) { + return OneByteString(env->isolate(), X509ErrorCode(err)); +} + +Local GetCert(Environment* env, SSL* ssl) { + ClearErrorOnReturn clear_error_on_return; + Local value = v8::Undefined(env->isolate()); + X509* cert = SSL_get_certificate(ssl); + if (cert != nullptr) + value = X509ToObject(env, cert); + return value; +} + +Local GetCipherName(Environment* env, SSL* ssl) { + Local cipher; + const SSL_CIPHER* c = SSL_get_current_cipher(ssl); + if (c != nullptr) { + const char* cipher_name = SSL_CIPHER_get_name(c); + cipher = OneByteString(env->isolate(), cipher_name); + } + return cipher; +} + +Local GetCipherVersion(Environment* env, SSL* ssl) { + Local version; + const SSL_CIPHER* c = SSL_get_current_cipher(ssl); + if (c != nullptr) { + const char* cipher_version = SSL_CIPHER_get_version(c); + version = OneByteString(env->isolate(), cipher_version); + } + return version; +} + +Local GetEphemeralKey(Environment* env, SSL* ssl) { + Local context = env->context(); + + Local info = Object::New(env->isolate()); + + EVP_PKEY* raw_key; + if (SSL_get_server_tmp_key(ssl, &raw_key)) { + crypto::EVPKeyPointer key(raw_key); + int kid = EVP_PKEY_id(key.get()); + switch (kid) { + case EVP_PKEY_DH: + info->Set(context, env->type_string(), + FIXED_ONE_BYTE_STRING(env->isolate(), "DH")).FromJust(); + info->Set(context, env->size_string(), + Integer::New(env->isolate(), EVP_PKEY_bits(key.get()))) + .FromJust(); + break; + case EVP_PKEY_EC: + case EVP_PKEY_X25519: + case EVP_PKEY_X448: + { + const char* curve_name; + if (kid == EVP_PKEY_EC) { + EC_KEY* ec = EVP_PKEY_get1_EC_KEY(key.get()); + int nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); + curve_name = OBJ_nid2sn(nid); + EC_KEY_free(ec); + } else { + curve_name = OBJ_nid2sn(kid); + } + info->Set(context, env->type_string(), + FIXED_ONE_BYTE_STRING( + env->isolate(), + "ECDH")).FromJust(); + info->Set(context, env->name_string(), + OneByteString( + env->isolate(), + curve_name)).FromJust(); + info->Set(context, env->size_string(), + Integer::New( + env->isolate(), + EVP_PKEY_bits(key.get()))).FromJust(); + } + break; + default: + break; + } + } + return info; +} + +Local GetPeerCert( + Environment* env, + SSL* ssl, + bool abbreviated, + bool is_server) { + ClearErrorOnReturn clear_error_on_return; + + Local result = v8::Undefined(env->isolate()); + Local issuer_chain; + + // NOTE: This is because of the odd OpenSSL behavior. On client `cert_chain` + // contains the `peer_certificate`, but on server it doesn't. + X509Pointer cert(is_server ? SSL_get_peer_certificate(ssl) : nullptr); + STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(ssl); + if (!cert && (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0)) + return result; + + // Short result requested. + if (abbreviated) + return X509ToObject(env, cert ? cert.get() : sk_X509_value(ssl_certs, 0)); + + if (auto peer_certs = CloneSSLCerts(std::move(cert), ssl_certs)) { + // First and main certificate. + X509Pointer cert(sk_X509_value(peer_certs.get(), 0)); + CHECK(cert); + result = X509ToObject(env, cert.release()); + + Local issuer_chain = + GetLastIssuedCert( + &cert, + ssl, + AddIssuerChainToObject( + &cert, + result.As(), + std::move(peer_certs), + env), + env); + // Last certificate should be self-signed. + if (X509_check_issued(cert.get(), cert.get()) == X509_V_OK) + USE(issuer_chain->Set( + env->context(), + env->issuercert_string(), + issuer_chain)); + } + return result; +} + +} // namespace crypto +} // namespace node diff --git a/src/node_crypto_common.h b/src/node_crypto_common.h new file mode 100644 index 0000000000..cf77b1eeb3 --- /dev/null +++ b/src/node_crypto_common.h @@ -0,0 +1,73 @@ +#ifndef SRC_NODE_CRYPTO_COMMON_H_ +#define SRC_NODE_CRYPTO_COMMON_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "env.h" +#include "node_crypto.h" +#include "v8.h" +#include +#include +#include +#include + +namespace node { +namespace crypto { + +void LogSecret( + SSL* ssl, + const char* name, + const unsigned char* secret, + size_t secretlen); + +void SetALPN(SSL* ssl, const std::string& alpn); + +std::string GetSSLOCSPResponse(SSL* ssl); + +bool SetTLSSession(SSL* ssl, const unsigned char* buf, size_t length); + +std::unordered_multimap +GetCertificateAltNames(X509* cert); + +std::string GetCertificateCN(X509* cert); + +int VerifyPeerCertificate(SSL* ssl, int def = X509_V_ERR_UNSPECIFIED); + +int UseSNIContext(SSL* ssl, SecureContext* context); + +const char* GetClientHelloALPN(SSL* ssl); + +const char* GetClientHelloServerName(SSL* ssl); + +const char* GetServerName(SSL* ssl); + +v8::Local GetClientHelloCiphers(Environment* env, SSL* ssl); + +bool SetGroups(SecureContext* sc, const char* groups); + +const char* X509ErrorCode(int err); + +v8::Local GetValidationErrorReason(Environment* env, int err); + +v8::Local GetValidationErrorCode(Environment* env, int err); + +v8::Local GetCert(Environment* env, SSL* ssl); + +v8::Local GetCipherName(Environment* env, SSL* ssl); + +v8::Local GetCipherVersion(Environment* env, SSL* ssl); + +v8::Local GetEphemeralKey(Environment* env, SSL* ssl); + +v8::Local GetPeerCert( + Environment* env, + SSL* ssl, + bool abbreviated = false, + bool is_server = false); + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_CRYPTO_COMMON_H_ diff --git a/src/node_quic.cc b/src/node_quic.cc index 20b36639fe..2237d7232b 100755 --- a/src/node_quic.cc +++ b/src/node_quic.cc @@ -3,6 +3,7 @@ #include "env-inl.h" #include "histogram-inl.h" #include "node_crypto.h" // SecureContext +#include "node_crypto_common.h" #include "node_process.h" #include "node_quic_crypto.h" #include "node_quic_session-inl.h" @@ -90,7 +91,7 @@ void QuicInitSecureContext(const FunctionCallbackInfo& args) { InitializeSecureContext(sc, side); // TODO(@jasnell): Throw a proper node.js error with code - if (!SetGroups(sc, *groups)) + if (!crypto::SetGroups(sc, *groups)) return env->ThrowError("Failed to set groups"); } } // namespace diff --git a/src/node_quic_crypto.cc b/src/node_quic_crypto.cc index 35c4208d77..c75cdc14fe 100644 --- a/src/node_quic_crypto.cc +++ b/src/node_quic_crypto.cc @@ -1,6 +1,7 @@ #include "node_quic_crypto.h" #include "env-inl.h" #include "node_crypto.h" +#include "node_crypto_common.h" #include "node_quic_session-inl.h" #include "node_quic_util.h" #include "node_url.h" @@ -123,102 +124,6 @@ bool GenerateRandData(uint8_t* buf, size_t len) { return true; } -Local GetClientHelloCiphers(QuicSession* session) { - const unsigned char* buf; - Environment* env = session->env(); - QuicCryptoContext* ctx = session->CryptoContext(); - size_t len = SSL_client_hello_get0_ciphers(**ctx, &buf); - std::vector> ciphers_array; - for (size_t n = 0; n < len; n += 2) { - const SSL_CIPHER* cipher = SSL_CIPHER_find(**ctx, buf); - buf += 2; - const char* cipher_name = SSL_CIPHER_get_name(cipher); - const char* cipher_version = SSL_CIPHER_get_version(cipher); - Local obj = Object::New(env->isolate()); - obj->Set( - env->context(), - env->name_string(), - OneByteString(env->isolate(), cipher_name)).FromJust(); - obj->Set( - env->context(), - env->version_string(), - OneByteString(env->isolate(), cipher_version)).FromJust(); - ciphers_array.push_back(obj); - } - return Array::New(env->isolate(), ciphers_array.data(), ciphers_array.size()); -} - -const char* GetClientHelloServerName(QuicSession* session) { - const unsigned char* buf; - size_t len; - size_t rem; - - QuicCryptoContext* ctx = session->CryptoContext(); - - if (!SSL_client_hello_get0_ext( - **ctx, - TLSEXT_TYPE_server_name, - &buf, - &rem) || rem <= 2) { - return nullptr; - } - - len = *(buf++) << 8; - len += *(buf++); - if (len + 2 != rem) - return nullptr; - rem = len; - - if (rem == 0 || *buf++ != TLSEXT_NAMETYPE_host_name) - return nullptr; - rem--; - if (rem <= 2) - return nullptr; - len = *(buf++) << 8; - len += *(buf++); - if (len + 2 > rem) - return nullptr; - rem = len; - return reinterpret_cast(buf); -} - -const char* GetClientHelloALPN(QuicSession* session) { - const unsigned char* buf; - size_t len; - size_t rem; - - QuicCryptoContext* ctx = session->CryptoContext(); - - if (!SSL_client_hello_get0_ext( - **ctx, - TLSEXT_TYPE_application_layer_protocol_negotiation, - &buf, &rem) || rem < 2) { - return nullptr; - } - - len = (buf[0] << 8) | buf[1]; - if (len + 2 != rem) - return nullptr; - buf += 3; - return reinterpret_cast(buf); -} - -int UseSNIContext(SSL* ssl, crypto::SecureContext* context) { - SSL_CTX* ctx = context->ctx_.get(); - X509* x509 = SSL_CTX_get0_certificate(ctx); - EVP_PKEY* pkey = SSL_CTX_get0_privatekey(ctx); - STACK_OF(X509)* chain; - - int err = SSL_CTX_get0_chain_certs(ctx, &chain); - if (err) - err = SSL_use_certificate(ssl, x509); - if (err) - err = SSL_use_PrivateKey(ssl, pkey); - if (err && chain != nullptr) - err = SSL_set1_chain(ssl, chain); - return err; -} - // The Retry Token is an encrypted token that is sent to the client // by the server as part of the path validation flow. The plaintext // format within the token is opaque and only meaningful the server. @@ -360,86 +265,7 @@ bool InvalidRetryToken( return false; } -int VerifyPeerCertificate(SSL* ssl) { - int err = X509_V_ERR_UNSPECIFIED; - if (X509* peer_cert = SSL_get_peer_certificate(ssl)) { - X509_free(peer_cert); - err = SSL_get_verify_result(ssl); - } - return err; -} - namespace { -std::string GetCertificateCN(X509* cert) { - X509_NAME* subject = X509_get_subject_name(cert); - if (subject != nullptr) { - int nid = OBJ_txt2nid("CN"); - int idx = X509_NAME_get_index_by_NID(subject, nid, -1); - if (idx != -1) { - X509_NAME_ENTRY* cn = X509_NAME_get_entry(subject, idx); - if (cn != nullptr) { - ASN1_STRING* cn_str = X509_NAME_ENTRY_get_data(cn); - if (cn_str != nullptr) { - return std::string(reinterpret_cast( - ASN1_STRING_get0_data(cn_str))); - } - } - } - } - return std::string(); -} - -std::unordered_multimap GetCertificateAltNames( - X509* cert) { - std::unordered_multimap map; - crypto::BIOPointer bio(BIO_new(BIO_s_mem())); - BUF_MEM* mem; - int idx = X509_get_ext_by_NID(cert, NID_subject_alt_name, -1); - if (idx < 0) // There is no subject alt name - return map; - - X509_EXTENSION* ext = X509_get_ext(cert, idx); - CHECK_NOT_NULL(ext); - const X509V3_EXT_METHOD* method = X509V3_EXT_get(ext); - CHECK_EQ(method, X509V3_EXT_get_nid(NID_subject_alt_name)); - - GENERAL_NAMES* names = static_cast(X509V3_EXT_d2i(ext)); - if (names == nullptr) // There are no names - return map; - - for (int i = 0; i < sk_GENERAL_NAME_num(names); i++) { - USE(BIO_reset(bio.get())); - GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i); - if (gen->type == GEN_DNS) { - ASN1_IA5STRING* name = gen->d.dNSName; - BIO_write(bio.get(), name->data, name->length); - BIO_get_mem_ptr(bio.get(), &mem); - map.emplace("dns", std::string(mem->data, mem->length)); - } else { - STACK_OF(CONF_VALUE)* nval = i2v_GENERAL_NAME( - const_cast(method), gen, nullptr); - if (nval == nullptr) - continue; - X509V3_EXT_val_prn(bio.get(), nval, 0, 0); - sk_CONF_VALUE_pop_free(nval, X509V3_conf_free); - BIO_get_mem_ptr(bio.get(), &mem); - std::string value(mem->data, mem->length); - if (value.compare(0, 11, "IP Address:") == 0) { - map.emplace("ip", value.substr(11)); - } else if (value.compare(0, 4, "URI:") == 0) { - url::URL url(value.substr(4)); - if (url.flags() & url::URL_FLAGS_CANNOT_BE_BASE || - url.flags() & url::URL_FLAGS_FAILED) { - continue; // Skip this one - } - map.emplace("uri", url.host()); - } - } - } - sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free); - bio.reset(); - return map; -} bool SplitHostname( const char* hostname, @@ -533,55 +359,8 @@ bool CheckCertNames( return true; } - -const char* X509ErrorCode(int err) { - const char* code = "UNSPECIFIED"; -#define CASE_X509_ERR(CODE) case X509_V_ERR_##CODE: code = #CODE; break; - switch (err) { - CASE_X509_ERR(UNABLE_TO_GET_ISSUER_CERT) - CASE_X509_ERR(UNABLE_TO_GET_CRL) - CASE_X509_ERR(UNABLE_TO_DECRYPT_CERT_SIGNATURE) - CASE_X509_ERR(UNABLE_TO_DECRYPT_CRL_SIGNATURE) - CASE_X509_ERR(UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY) - CASE_X509_ERR(CERT_SIGNATURE_FAILURE) - CASE_X509_ERR(CRL_SIGNATURE_FAILURE) - CASE_X509_ERR(CERT_NOT_YET_VALID) - CASE_X509_ERR(CERT_HAS_EXPIRED) - CASE_X509_ERR(CRL_NOT_YET_VALID) - CASE_X509_ERR(CRL_HAS_EXPIRED) - CASE_X509_ERR(ERROR_IN_CERT_NOT_BEFORE_FIELD) - CASE_X509_ERR(ERROR_IN_CERT_NOT_AFTER_FIELD) - CASE_X509_ERR(ERROR_IN_CRL_LAST_UPDATE_FIELD) - CASE_X509_ERR(ERROR_IN_CRL_NEXT_UPDATE_FIELD) - CASE_X509_ERR(OUT_OF_MEM) - CASE_X509_ERR(DEPTH_ZERO_SELF_SIGNED_CERT) - CASE_X509_ERR(SELF_SIGNED_CERT_IN_CHAIN) - CASE_X509_ERR(UNABLE_TO_GET_ISSUER_CERT_LOCALLY) - CASE_X509_ERR(UNABLE_TO_VERIFY_LEAF_SIGNATURE) - CASE_X509_ERR(CERT_CHAIN_TOO_LONG) - CASE_X509_ERR(CERT_REVOKED) - CASE_X509_ERR(INVALID_CA) - CASE_X509_ERR(PATH_LENGTH_EXCEEDED) - CASE_X509_ERR(INVALID_PURPOSE) - CASE_X509_ERR(CERT_UNTRUSTED) - CASE_X509_ERR(CERT_REJECTED) - CASE_X509_ERR(HOSTNAME_MISMATCH) - } -#undef CASE_X509_ERR - return code; -} - } // namespace -Local GetValidationErrorReason(Environment* env, int err) { - const char* reason = X509_verify_cert_error_string(err); - return OneByteString(env->isolate(), reason); -} - -Local GetValidationErrorCode(Environment* env, int err) { - return OneByteString(env->isolate(), X509ErrorCode(err)); -} - int VerifyHostnameIdentity(SSL* ssl, const char* hostname) { int err = X509_V_ERR_HOSTNAME_MISMATCH; crypto::X509Pointer cert(SSL_get_peer_certificate(ssl)); @@ -624,8 +403,8 @@ int VerifyHostnameIdentity(SSL* ssl, const char* hostname) { // If we've made it this far, then we have to perform a more check return VerifyHostnameIdentity( hostname, - GetCertificateCN(cert.get()), - GetCertificateAltNames(cert.get())); + crypto::GetCertificateCN(cert.get()), + crypto::GetCertificateAltNames(cert.get())); } int VerifyHostnameIdentity( @@ -707,26 +486,6 @@ int VerifyHostnameIdentity( return err; } -const char* GetServerName(QuicSession* session) { - QuicCryptoContext* ctx = session->CryptoContext(); - return SSL_get_servername(**ctx, TLSEXT_NAMETYPE_host_name); -} - -// Get the SNI hostname requested by the client for the session -Local GetServerName( - Environment* env, - SSL* ssl, - const char* host_name) { - Local servername; - if (host_name != nullptr) { - servername = String::NewFromUtf8( - env->isolate(), - host_name, - v8::NewStringType::kNormal).ToLocalChecked(); - } - return servername; -} - // Get the ALPN protocol identifier that was negotiated for the session Local GetALPNProtocol(QuicSession* session) { Local alpn; @@ -744,155 +503,6 @@ Local GetALPNProtocol(QuicSession* session) { return alpn; } -Local GetCertificate(QuicSession* session) { - crypto::ClearErrorOnReturn clear_error_on_return; - QuicCryptoContext* ctx = session->CryptoContext(); - Local value = v8::Undefined(session->env()->isolate()); - X509* cert = SSL_get_certificate(**ctx); - if (cert != nullptr) - value = crypto::X509ToObject(session->env(), cert); - return value; -} - -Local GetEphemeralKey(QuicSession* session) { - Environment* env = session->env(); - Local context = env->context(); - - Local info = Object::New(env->isolate()); - QuicCryptoContext* ctx = session->CryptoContext(); - - EVP_PKEY* raw_key; - if (SSL_get_server_tmp_key(**ctx, &raw_key)) { - crypto::EVPKeyPointer key(raw_key); - int kid = EVP_PKEY_id(key.get()); - switch (kid) { - case EVP_PKEY_DH: - info->Set(context, env->type_string(), - FIXED_ONE_BYTE_STRING(env->isolate(), "DH")).FromJust(); - info->Set(context, env->size_string(), - Integer::New(env->isolate(), EVP_PKEY_bits(key.get()))) - .FromJust(); - break; - case EVP_PKEY_EC: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: - { - const char* curve_name; - if (kid == EVP_PKEY_EC) { - EC_KEY* ec = EVP_PKEY_get1_EC_KEY(key.get()); - int nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); - curve_name = OBJ_nid2sn(nid); - EC_KEY_free(ec); - } else { - curve_name = OBJ_nid2sn(kid); - } - info->Set(context, env->type_string(), - FIXED_ONE_BYTE_STRING( - env->isolate(), - "ECDH")).FromJust(); - info->Set(context, env->name_string(), - OneByteString( - env->isolate(), - curve_name)).FromJust(); - info->Set(context, env->size_string(), - Integer::New( - env->isolate(), - EVP_PKEY_bits(key.get()))).FromJust(); - } - break; - default: - break; - } - } - return info; -} - -Local GetCipherName(QuicSession* session) { - Local cipher; - QuicCryptoContext* ctx = session->CryptoContext(); - const SSL_CIPHER* c = SSL_get_current_cipher(**ctx); - if (c != nullptr) { - const char* cipher_name = SSL_CIPHER_get_name(c); - cipher = OneByteString(session->env()->isolate(), cipher_name); - } - return cipher; -} - -Local GetCipherVersion(QuicSession* session) { - Local version; - QuicCryptoContext* ctx = session->CryptoContext(); - const SSL_CIPHER* c = SSL_get_current_cipher(**ctx); - if (c != nullptr) { - const char* cipher_version = SSL_CIPHER_get_version(c); - version = OneByteString(session->env()->isolate(), cipher_version); - } - return version; -} - -bool SetTLSSession(SSL* ssl, const unsigned char* buf, size_t length) { - crypto::SSLSessionPointer s(d2i_SSL_SESSION(nullptr, &buf, length)); - return s != nullptr && SSL_set_session(ssl, s.get()) == 1; -} - -std::string GetSSLOCSPResponse(SSL* ssl) { - const unsigned char* resp; - int len = SSL_get_tlsext_status_ocsp_resp(ssl, &resp); - if (len < 0) len = 0; - return std::string(reinterpret_cast(resp), len); -} - -Local GetPeerCertificate( - QuicSession* session, - bool abbreviated) { - crypto::ClearErrorOnReturn clear_error_on_return; - - QuicCryptoContext* ctx = session->CryptoContext(); - - Local result = v8::Undefined(session->env()->isolate()); - Local issuer_chain; - - // NOTE: This is because of the odd OpenSSL behavior. On client `cert_chain` - // contains the `peer_certificate`, but on server it doesn't. - crypto::X509Pointer cert( - session->IsServer() ? SSL_get_peer_certificate(**ctx) : nullptr); - STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(**ctx); - if (!cert && (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0)) - return result; - - // Short result requested. - if (abbreviated) { - return - crypto::X509ToObject( - session->env(), - cert ? cert.get() : sk_X509_value(ssl_certs, 0)); - } - - if (auto peer_certs = crypto::CloneSSLCerts(std::move(cert), ssl_certs)) { - // First and main certificate. - crypto::X509Pointer cert(sk_X509_value(peer_certs.get(), 0)); - CHECK(cert); - result = crypto::X509ToObject(session->env(), cert.release()); - - Local issuer_chain = - crypto::GetLastIssuedCert( - &cert, - **ctx, - crypto::AddIssuerChainToObject( - &cert, - result.As(), - std::move(peer_certs), - session->env()), - session->env()); - // Last certificate should be self-signed. - if (X509_check_issued(cert.get(), cert.get()) == X509_V_OK) - USE(issuer_chain->Set( - session->env()->context(), - session->env()->issuercert_string(), - issuer_chain)); - } - return result; -} - namespace { int CertCB(SSL* ssl, void* arg) { QuicSession* session = static_cast(arg); @@ -1004,24 +614,6 @@ int SendAlert( return 1; } -void SetALPN(SSL* ssl, const std::string& alpn) { - SSL_set_alpn_protos( - ssl, - reinterpret_cast(alpn.c_str()), - alpn.length()); -} - -void SetHostname(SSL* ssl, const std::string& hostname) { - // TODO(@jasnell): Need to determine if setting localhost - // here is the right thing to do. - if (hostname.length() == 0 || - SocketAddress::numeric_host(hostname.c_str())) { - SSL_set_tlsext_host_name(ssl, "localhost"); - } else { - SSL_set_tlsext_host_name(ssl, hostname.c_str()); - } -} - bool SetTransportParams(ngtcp2_conn* connection, SSL* ssl) { ngtcp2_transport_params params; ngtcp2_conn_get_local_transport_params(connection, ¶ms); @@ -1035,30 +627,24 @@ bool SetTransportParams(ngtcp2_conn* connection, SSL* ssl) { SSL_set_quic_transport_params(ssl, buf.data(), nwrite) == 1; } -void LogSecret( - SSL* ssl, - const char* name, - const unsigned char* secret, - size_t secretlen) { - if (auto keylog_cb = SSL_CTX_get_keylog_callback(SSL_get_SSL_CTX(ssl))) { - unsigned char crandom[32]; - if (SSL_get_client_random(ssl, crandom, 32) != 32) - return; - std::string line = name; - line += " " + StringBytes::hex_encode( - reinterpret_cast(crandom), 32); - line += " " + StringBytes::hex_encode( - reinterpret_cast(secret), secretlen); - keylog_cb(ssl, line.c_str()); - } -} - SSL_QUIC_METHOD quic_method = SSL_QUIC_METHOD{ SetEncryptionSecrets, AddHandshakeData, FlushFlight, SendAlert }; + +void SetHostname(SSL* ssl, const std::string& hostname) { + // TODO(@jasnell): Need to determine if setting localhost + // here is the right thing to do. + if (hostname.length() == 0 || + SocketAddress::numeric_host(hostname.c_str())) { + SSL_set_tlsext_host_name(ssl, "localhost"); + } else { + SSL_set_tlsext_host_name(ssl, hostname.c_str()); + } +} + } // namespace void InitializeTLS(QuicSession* session) { @@ -1077,7 +663,7 @@ void InitializeTLS(QuicSession* session) { switch (ctx->Side()) { case NGTCP2_CRYPTO_SIDE_CLIENT: { SSL_set_connect_state(**ctx); - SetALPN(**ctx, session->GetALPN()); + crypto::SetALPN(**ctx, session->GetALPN()); SetHostname(**ctx, session->GetHostname()); if (ctx->IsOptionSet(QUICCLIENTSESSION_OPTION_REQUEST_OCSP)) SSL_set_tlsext_status_type(**ctx, TLSEXT_STATUSTYPE_ocsp); @@ -1100,10 +686,6 @@ void InitializeTLS(QuicSession* session) { SetTransportParams(session->Connection(), **ctx); } -bool SetGroups(crypto::SecureContext* sc, const char* groups) { - return SSL_CTX_set1_groups_list(**sc, groups) == 1; -} - void InitializeSecureContext( crypto::SecureContext* sc, ngtcp2_crypto_side side) { @@ -1173,31 +755,31 @@ bool SetCryptoSecrets( switch (level) { case NGTCP2_CRYPTO_LEVEL_EARLY: - LogSecret( + crypto::LogSecret( **ctx, QUIC_CLIENT_EARLY_TRAFFIC_SECRET, rx_secret, secretlen); break; case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: - LogSecret( + crypto::LogSecret( **ctx, QUIC_CLIENT_HANDSHAKE_TRAFFIC_SECRET, rx_secret, secretlen); - LogSecret( + crypto::LogSecret( **ctx, QUIC_SERVER_HANDSHAKE_TRAFFIC_SECRET, tx_secret, secretlen); break; case NGTCP2_CRYPTO_LEVEL_APP: - LogSecret( + crypto::LogSecret( **ctx, QUIC_CLIENT_TRAFFIC_SECRET_0, rx_secret, secretlen); - LogSecret( + crypto::LogSecret( **ctx, QUIC_SERVER_TRAFFIC_SECRET_0, tx_secret, diff --git a/src/node_quic_crypto.h b/src/node_quic_crypto.h index a158b81653..c5424cbc81 100644 --- a/src/node_quic_crypto.h +++ b/src/node_quic_crypto.h @@ -33,10 +33,6 @@ bool SetCryptoSecrets( const uint8_t* tx_secret, size_t secretlen); -// Called by QuicInitSecureContext in node_quic.cc -// to set the TLS groups for the context. -bool SetGroups(crypto::SecureContext* sc, const char* groups); - // Called by QuicInitSecureContext to initialize the // given SecureContext with the defaults for the given // QUIC side (client or server). @@ -67,17 +63,6 @@ bool UpdateKey( std::vector* current_rx_secret, std::vector* current_tx_secret); -// Get the server name identified in the client hello -const char* GetClientHelloServerName(QuicSession* session); - -// Get the alpn protocol identified in the client hello -const char* GetClientHelloALPN(QuicSession* session); - -const char* GetServerName(QuicSession* session); - -// Replaces the SecureContext to be used in the handshake. -int UseSNIContext(SSL* ssl, crypto::SecureContext* context); - bool GenerateRetryToken( uint8_t* token, size_t* tokenlen, @@ -93,50 +78,15 @@ bool InvalidRetryToken( std::array* token_secret, uint64_t verification_expiration); -// Called by QuicSession::VerifyPeerIdentity to perform basic -// validation checks against the peer provided certificate. -int VerifyPeerCertificate(SSL* ssl); - int VerifyHostnameIdentity(SSL* ssl, const char* hostname); int VerifyHostnameIdentity( const char* hostname, const std::string& cert_cn, const std::unordered_multimap& altnames); -v8::Local GetValidationErrorReason(Environment* env, int err); -v8::Local GetValidationErrorCode(Environment* env, int err); - -v8::Local GetCertificate(QuicSession* session); -v8::Local GetEphemeralKey(QuicSession* session); - -bool SetTLSSession(SSL* ssl, const unsigned char* buf, size_t length); - -std::string GetSSLOCSPResponse(SSL* ssl); - -// Get the SNI hostname requested by the client for the session -v8::Local GetServerName( - Environment* env, - SSL* ssl, - const char* host_name); - -// Get the list of cipher algorithms advertised in the client hello -v8::Local GetClientHelloCiphers(QuicSession* Session); - // Get the ALPN protocol identifier that was negotiated for the session v8::Local GetALPNProtocol(QuicSession* session); -// Get the negotiated cipher name for the TLS session -v8::Local GetCipherName(QuicSession* session); - -// Get the negotiated cipher version for the TLS session -v8::Local GetCipherVersion(QuicSession* session); - -// Get a JavaScript rendering of the X509 certificate provided by the peer -// TODO(@jasnell): This currently only works for the Client side -v8::Local GetPeerCertificate( - QuicSession* session, - bool abbreviated); - } // namespace quic } // namespace node diff --git a/src/node_quic_http3_application.cc b/src/node_quic_http3_application.cc index 7167ad29ee..ee80487edb 100644 --- a/src/node_quic_http3_application.cc +++ b/src/node_quic_http3_application.cc @@ -26,6 +26,11 @@ bool IsZeroLengthHeader(nghttp3_rcbuf* name, nghttp3_rcbuf* value) { Http3RcBufferPointer::IsZeroLength(value); } +// nghttp3 uses a numeric identifier for a large number +// of known HTTP header names. These allow us to use +// static strings for those rather than allocating new +// strings all of the time. The list of strings supported +// is included in node_http_common.h const char* to_http_header_name(int32_t token) { switch (token) { default: @@ -72,6 +77,7 @@ MaybeLocal Http3Header::GetName(QuicApplication* app) const { return eternal.Get(env->isolate()); } + // This is exceedingly unlikely but we need to be prepared just in case. if (UNLIKELY(!name_)) return String::Empty(env->isolate()); @@ -104,7 +110,7 @@ Http3Application::Http3Application( QuicSession* session) : QuicApplication(session), alloc_info_(MakeAllocator()) { - // Collect Configuration Details + // Collect Configuration Details. An aliased buffer is used here. Environment* env = session->env(); SetConfig(env, IDX_HTTP3_QPACK_MAX_TABLE_CAPACITY, &qpack_max_table_capacity_); @@ -115,11 +121,18 @@ Http3Application::Http3Application( env->quic_state()->http3config_buffer[IDX_HTTP3_CONFIG_COUNT] = 0; // Reset } +// Submit informational headers (response headers that use a 1xx +// status code). bool Http3Application::SubmitInformation( int64_t stream_id, v8::Local headers) { + // If the QuicSession is not a server session, return false + // immediately. Informational headers cannot be sent by an + // HTTP/3 client + if (!Session()->IsServer()) + return false; + Http3Headers nva(Session()->env(), headers); - // TODO(@jasnell): Do we need more granularity on error conditions? Debug( Session(), "Submitting %d informational headers for stream %" PRId64, @@ -146,7 +159,6 @@ bool Http3Application::SubmitHeaders( if (!(flags & QUICSTREAM_HEADER_FLAGS_TERMINAL)) reader_ptr = &reader; - // TODO(@jasnell): Do we need more granularity on error conditions? switch (Session()->CryptoContext()->Side()) { case NGTCP2_CRYPTO_SIDE_CLIENT: Debug( @@ -183,7 +195,6 @@ bool Http3Application::SubmitTrailers( int64_t stream_id, v8::Local headers) { Http3Headers nva(Session()->env(), headers); - // TODO(@jasnell): Do we need more granularity on error conditions? Debug( Session(), "Submitting %d trailing headers for stream %" PRId64, @@ -235,6 +246,9 @@ nghttp3_conn* Http3Application::CreateConnection( return conn; } +// The HTTP/3 QUIC binding uses a single unidirectional control +// stream in each direction to exchange frames impacting the entire +// connection. bool Http3Application::CreateAndBindControlStream() { if (!Session()->OpenUnidirectionalStream(&control_stream_id_)) return false; @@ -247,6 +261,8 @@ bool Http3Application::CreateAndBindControlStream() { control_stream_id_) == 0; } +// The HTTP/3 QUIC binding creates two unidirectional streams in +// each direction to exchange header compression details. bool Http3Application::CreateAndBindQPackStreams() { if (!Session()->OpenUnidirectionalStream(&qpack_enc_stream_id_) || !Session()->OpenUnidirectionalStream(&qpack_dec_stream_id_)) { @@ -268,7 +284,8 @@ bool Http3Application::Initialize() { return false; // The QuicSession must allow for at least three local unidirectional streams. - // This number is fixed by the http3 specification. + // This number is fixed by the http3 specification and represent the + // control stream and two qpack management streams. if (Session()->GetMaxLocalStreamsUni() < 3) return false; @@ -308,6 +325,9 @@ bool Http3Application::Initialize() { return true; } +// All HTTP/3 control, header, and stream data arrives as QUIC stream data. +// Here we pass the received data off to nghttp3 for processing. This will +// trigger the invocation of the various nghttp3 callbacks. bool Http3Application::ReceiveStreamData( int64_t stream_id, int fin, @@ -334,7 +354,7 @@ void Http3Application::AcknowledgeStreamData( } void Http3Application::StreamOpen(int64_t stream_id) { - // FindOrCreateStream(stream_id); + Debug(Session(), "HTTP/3 Stream %" PRId64 " is open."); } void Http3Application::StreamClose( @@ -395,6 +415,7 @@ bool Http3Application::SendPendingData() { int fin = 0; ssize_t sveccnt = 0; + // First, grab the outgoing data from nghttp3 if (Connection() && Session()->GetMaxDataLeft()) { sveccnt = nghttp3_conn_writev_stream( @@ -414,6 +435,10 @@ bool Http3Application::SendPendingData() { nghttp3_vec* v = vec.data(); size_t vcnt = static_cast(sveccnt); + // Second, serialize as much of the outgoing data as possible to a + // QUIC packet for transmission. We'll keep iterating until there + // is no more data to transmit. + // TODO(@jasnell): Support the use of the NGTCP2_WRITE_STREAM_FLAG_MORE // flag to allow more efficient coallescing of packets. MallocedBuffer dest(Session()->GetMaxPacketLength()); @@ -480,6 +505,8 @@ bool Http3Application::SendStreamData(QuicStream* stream) { return SendPendingData(); } +// This is where nghttp3 pulls the data from the outgoing +// buffer to prepare it to be sent on the QUIC stream. ssize_t Http3Application::H3ReadData( int64_t stream_id, nghttp3_vec* vec, @@ -505,9 +532,14 @@ ssize_t Http3Application::H3ReadData( return count; } + // If count is zero here, it means that there is no data currently + // available to send but there might be later, so return WOULDBLOCK + // to tell nghttp3 to hold off attempting to serialize any more + // data for this stream until it is resumed. return count == 0 ? NGHTTP3_ERR_WOULDBLOCK : count; } +// Outgoing data is retained in memory until it is acknowledged. void Http3Application::H3AckedStreamData( int64_t stream_id, size_t datalen) { @@ -574,13 +606,18 @@ void Http3Application::H3BeginHeaders( stream->BeginHeaders(kind); } +// As each header name+value pair is received, it is stored internally +// by the QuicStream until stream->EndHeaders() is called, during which +// the collected headers are converted to an array and passed off to +// the javascript side. bool Http3Application::H3ReceiveHeader( int64_t stream_id, int32_t token, nghttp3_rcbuf* name, nghttp3_rcbuf* value, uint8_t flags) { - // Protect against zero-length headers + // Protect against zero-length headers (zero-length if either the + // name or value are zero-length). Such headers are simply ignored. if (!IsZeroLengthHeader(name, value)) { Debug(Session(), "Receiving header for stream %" PRId64, stream_id); QuicStream* stream = Session()->FindStream(stream_id); @@ -606,12 +643,14 @@ void Http3Application::H3EndHeaders(int64_t stream_id) { stream->EndHeaders(); } +// TODO(@jasnell): Implement Push Promise Support int Http3Application::H3BeginPushPromise( int64_t stream_id, int64_t push_id) { return 0; } +// TODO(@jasnell): Implement Push Promise Support bool Http3Application::H3ReceivePushPromise( int64_t stream_id, int64_t push_id, @@ -622,29 +661,32 @@ bool Http3Application::H3ReceivePushPromise( return true; } +// TODO(@jasnell): Implement Push Promise Support int Http3Application::H3EndPushPromise( int64_t stream_id, int64_t push_id) { return 0; } +// TODO(@jasnell): Implement Push Promise Support void Http3Application::H3CancelPush( int64_t push_id, int64_t stream_id) { } -void Http3Application::H3SendStopSending( - int64_t stream_id, - uint64_t app_error_code) { - Session()->ResetStream(stream_id, app_error_code); -} - +// TODO(@jasnell): Implement Push Promise Support int Http3Application::H3PushStream( int64_t push_id, int64_t stream_id) { return 0; } +void Http3Application::H3SendStopSending( + int64_t stream_id, + uint64_t app_error_code) { + Session()->ResetStream(stream_id, app_error_code); +} + int Http3Application::H3EndStream( int64_t stream_id) { QuicStream* stream = FindOrCreateStream(stream_id); diff --git a/src/node_quic_session.cc b/src/node_quic_session.cc index c98e611335..4f485f6ebf 100644 --- a/src/node_quic_session.cc +++ b/src/node_quic_session.cc @@ -1,6 +1,7 @@ #include "aliased_buffer.h" #include "debug_utils.h" #include "env-inl.h" +#include "node_crypto_common.h" #include "ngtcp2/ngtcp2.h" #include "ngtcp2/ngtcp2_crypto.h" #include "ngtcp2/ngtcp2_crypto_openssl.h" @@ -279,7 +280,7 @@ void QuicCryptoContext::EnableTrace() { } std::string QuicCryptoContext::GetOCSPResponse() { - return GetSSLOCSPResponse(ssl()); + return crypto::GetSSLOCSPResponse(ssl()); } ngtcp2_crypto_level QuicCryptoContext::GetReadCryptoLevel() { @@ -352,13 +353,19 @@ int QuicCryptoContext::OnClientHello() { HandleScope scope(env->isolate()); Context::Scope context_scope(env->context()); - const char* alpn = GetClientHelloALPN(session_); - const char* server_name = GetClientHelloServerName(session_); + const char* alpn = + crypto::GetClientHelloALPN( + session_->CryptoContext()->ssl()); + const char* server_name = + crypto::GetClientHelloServerName( + session_->CryptoContext()->ssl()); Local argv[] = { Undefined(env->isolate()), Undefined(env->isolate()), - GetClientHelloCiphers(session_) + crypto::GetClientHelloCiphers( + session_->env(), + session_->CryptoContext()->ssl()) }; if (alpn != nullptr) { @@ -421,7 +428,9 @@ int QuicCryptoContext::OnOCSP() { Context::Scope context_scope(env->context()); Local servername_str; - const char* servername = GetServerName(session_); + const char* servername = + crypto::GetServerName( + session_->CryptoContext()->ssl()); Local argv[] = { servername == nullptr ? @@ -460,7 +469,7 @@ void QuicCryptoContext::OnOCSPDone( session_->state_[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = 0; if (context != nullptr) { - int err = UseSNIContext(ssl(), context); + int err = crypto::UseSNIContext(ssl(), context); if (!err) { unsigned long err = ERR_get_error(); // NOLINT(runtime/int) if (!err) { @@ -598,7 +607,7 @@ void QuicCryptoContext::ResumeHandshake() { } bool QuicCryptoContext::SetSession(const unsigned char* data, size_t length) { - return SetTLSSession(ssl(), data, length); + return crypto::SetTLSSession(ssl(), data, length); } void QuicCryptoContext::SetTLSAlert(int err) { @@ -650,7 +659,7 @@ bool QuicCryptoContext::KeyUpdate( } int QuicCryptoContext::VerifyPeerIdentity(const char* hostname) { - int err = VerifyPeerCertificate(ssl()); + int err = crypto::VerifyPeerCertificate(ssl()); if (err) return err; @@ -963,8 +972,7 @@ void QuicSession::AddToSocket(QuicSocket* socket) { switch (crypto_context_->Side()) { case NGTCP2_CRYPTO_SIDE_SERVER: { - QuicCID rcid(rcid_); - socket->AssociateCID(rcid, scid); + socket->AssociateCID(QuicCID(rcid_), scid); if (pscid_.datalen) socket->AssociateCID(QuicCID(pscid_), scid); @@ -1238,7 +1246,9 @@ void QuicSession::HandshakeCompleted() { Context::Scope context_scope(env()->context()); Local servername = Undefined(env()->isolate()); - const char* hostname = GetServerName(this); + const char* hostname = + crypto::GetServerName( + this->CryptoContext()->ssl()); if (hostname != nullptr) { servername = String::NewFromUtf8( @@ -1255,14 +1265,14 @@ void QuicSession::HandshakeCompleted() { Local argv[] = { servername, GetALPNProtocol(this), - GetCipherName(this), - GetCipherVersion(this), + crypto::GetCipherName(this->env(), this->CryptoContext()->ssl()), + crypto::GetCipherVersion(this->env(), this->CryptoContext()->ssl()), Integer::New(env()->isolate(), max_pktlen_), err != 0 ? - GetValidationErrorReason(env(), err) : + crypto::GetValidationErrorReason(env(), err) : v8::Undefined(env()->isolate()).As(), err != 0 ? - GetValidationErrorCode(env(), err) : + crypto::GetValidationErrorCode(env(), err) : v8::Undefined(env()->isolate()).As() }; @@ -3042,21 +3052,24 @@ void QuicSessionDestroy(const FunctionCallbackInfo& args) { session->Destroy(); } -// TODO(@jasnell): Consolidate shared code with node_crypto void QuicSessionGetEphemeralKeyInfo(const FunctionCallbackInfo& args) { QuicSession* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - return args.GetReturnValue().Set(GetEphemeralKey(session)); + return args.GetReturnValue().Set( + crypto::GetEphemeralKey( + session->env(), + session->CryptoContext()->ssl())); } -// TODO(@jasnell): Consolidate with shared code in node_crypto void QuicSessionGetPeerCertificate(const FunctionCallbackInfo& args) { QuicSession* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); args.GetReturnValue().Set( - GetPeerCertificate( - session, - !args[0]->IsTrue())); + crypto::GetPeerCert( + session->env(), + session->CryptoContext()->ssl(), + !args[0]->IsTrue(), + session->IsServer())); } void QuicSessionGetRemoteAddress( @@ -3069,12 +3082,14 @@ void QuicSessionGetRemoteAddress( AddressToJS(env, **session->GetRemoteAddress(), args[0].As())); } -// TODO(@jasnell): Reconcile with shared code in node_crypto void QuicSessionGetCertificate( const FunctionCallbackInfo& args) { QuicSession* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - args.GetReturnValue().Set(GetCertificate(session)); + args.GetReturnValue().Set( + crypto::GetCert( + session->env(), + session->CryptoContext()->ssl())); } void QuicSessionPing(const FunctionCallbackInfo& args) { diff --git a/src/node_quic_session.h b/src/node_quic_session.h index 76587f3fa3..ea23918331 100644 --- a/src/node_quic_session.h +++ b/src/node_quic_session.h @@ -164,16 +164,23 @@ enum QuicSessionState : int { IDX_QUIC_SESSION_STATE_COUNT }; +// The QuicCryptoContext class encapsulates all of the crypto/TLS +// handshake details on behalf of a QuicSession. class QuicCryptoContext : public MemoryRetainer { public: SSL* operator*() { return ssl_.get(); } uint64_t Cancel(); + // Outgoing crypto data must be retained in memory until it is + // explicitly acknowledged. void AcknowledgeCryptoData(ngtcp2_crypto_level level, size_t datalen); + // Enables openssl's TLS tracing mechanism void EnableTrace(); + // Returns the server's prepared OCSP response for transmission. This + // is not used by client QuicSession instances. std::string GetOCSPResponse(); ngtcp2_crypto_level GetReadCryptoLevel(); @@ -184,6 +191,7 @@ class QuicCryptoContext : public MemoryRetainer { return options_ & option; } + // Emits a single keylog line to the JavaScript layer void Keylog(const char* line); int OnClientHello(); @@ -204,12 +212,15 @@ class QuicCryptoContext : public MemoryRetainer { int OnTLSStatus(); + // Receives and processes TLS handshake details int Receive( ngtcp2_crypto_level crypto_level, uint64_t offset, const uint8_t* data, size_t datalen); + // Resumes the TLS handshake following a client hello or + // OCSP callback void ResumeHandshake(); void SetOption(uint32_t option, bool on = true) { @@ -235,6 +246,7 @@ class QuicCryptoContext : public MemoryRetainer { size_t datalen); bool InitiateKeyUpdate(); + bool KeyUpdate( uint8_t* rx_key, uint8_t* rx_iv, diff --git a/src/node_quic_util.h b/src/node_quic_util.h index 58b9610601..2b61bd5091 100644 --- a/src/node_quic_util.h +++ b/src/node_quic_util.h @@ -326,29 +326,40 @@ struct QuicPathStorage { std::array remote_addrbuf; }; +// Simple wrapper for ngtcp2_cid that handles hex encoding +// and conversion to std::string automatically class QuicCID { public: - explicit QuicCID(ngtcp2_cid* cid) : cid_(*cid) {} - explicit QuicCID(const ngtcp2_cid* cid) : cid_(*cid) {} - explicit QuicCID(const ngtcp2_cid& cid) : cid_(cid) {} + explicit QuicCID(ngtcp2_cid* cid) : + cid_(*cid), + str_(cid->data, cid->data + cid->datalen) {} + explicit QuicCID(const ngtcp2_cid* cid) : + cid_(*cid), + str_(cid->data, cid->data + cid->datalen) {} + explicit QuicCID(const ngtcp2_cid& cid) : + cid_(cid), + str_(cid.data, cid.data + cid.datalen) {} QuicCID(const uint8_t* cid, size_t len) { ngtcp2_cid_init(&cid_, cid, len); + str_ = std::string(cid_.data, cid_.data + cid_.datalen); } - std::string ToStr() const { - return std::string(cid_.data, cid_.data + cid_.datalen); - } + std::string ToStr() const { return str_; } std::string ToHex() const { - MaybeStackBuffer dest; - dest.AllocateSufficientStorage(cid_.datalen * 2); - dest.SetLengthAndZeroTerminate(cid_.datalen * 2); - size_t written = StringBytes::hex_encode( - reinterpret_cast(cid_.data), - cid_.datalen, - *dest, - dest.length()); - return std::string(*dest, written); + if (hex_.empty() && cid_.datalen > 0) { + size_t len = cid_.datalen * 2; + MaybeStackBuffer dest; + dest.AllocateSufficientStorage(len); + dest.SetLengthAndZeroTerminate(len); + size_t written = StringBytes::hex_encode( + reinterpret_cast(cid_.data), + cid_.datalen, + *dest, + dest.length()); + hex_ = std::string(*dest, written); + } + return hex_; } const ngtcp2_cid* operator*() const { return &cid_; } @@ -358,6 +369,8 @@ class QuicCID { private: ngtcp2_cid cid_; + std::string str_; + mutable std::string hex_; }; // https://stackoverflow.com/questions/33701430/template-function-to-access-struct-members