前段时间为了获取某通信采用 gRPC 的游戏的报文需要破除 gRPC 的证书验证机制,所以对 gRPC 源码的证书验证部分进行了详细的查证,在此记录下来。
本文将从 C# 和 C++(gRPC core)两个层级分析 gRPC 证书验证机制源码。
注:本文对 C# 层的 gRPC 源码分析基于 v1.46.x 分支,由于目前 gRPC 项目组已决定将 gRPC C# 部分迁移到 grpc-dotnet ,故此分支已处于 deprecated 状态,对未来的 gRPC C# 分析估计起不到太大作用。
C# 层 先下载 v1.46.x 分支的源码,用 VS 打开。
可以找到 C# 层与证书验证相关的最关键的类:Grpc.Core.SslCredentials
,继承于抽象类 Grpc.Core.ChannelCredentials
。
这个类的构造方法
1 2 3 4 5 6 7 8 9 public SslCredentials ( string ? rootCertificates, KeyCertificatePair? keyCertificatePair, VerifyPeerCallback? verifyPeerCallback ) { this .rootCertificates = rootCertificates; this .keyCertificatePair = keyCertificatePair; this .verifyPeerCallback = verifyPeerCallback; }
有 3 个参数。
rootCertificates
:所有可信的服务端根证书字符串,采用 PEM 编码格式。
keyCertificatePair
:服务端证书和私钥的 pair。通常客户端不可能包含私钥,所以这个值一般为 null,基本上不用管。
verifyPeerCallback
:留给用户的自定义的验证函数。此函数必须返回一个 bool 值,如果返回值为 true 则代表通过验证,反之。
除了这三个参数以外,这个类里还包含一个叫做 InternalPopulateConfiguration
的方法:
1 2 3 4 5 6 7 8 9 10 public override void InternalPopulateConfiguration ( ChannelCredentialsConfiguratorBase configurator, object state ) { configurator.SetSslCredentials( state, rootCertificates, keyCertificatePair, verifyPeerCallback); }
这个方法的作用是将储存在自己中的上述三个参数交给 ChannelCredentialsConfiguratorBase
,用以配置 ChannelCredentials。
ChannelCredentialsConfiguratorBase
是一个抽象类,它的一个实现是 DefaultChannelCredentialsConfigurator
,其中对应的方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public override void SetSslCredentials ( object state, string rootCertificates, KeyCertificatePair keyCertificatePair, VerifyPeerCallback verifyPeerCallback ) { GrpcPreconditions.CheckState(!configured); configured = true ; nativeCredentials = GetOrCreateNativeCredentials( (ChannelCredentials) state, () => CreateNativeSslCredentials( rootCertificates, keyCertificatePair, verifyPeerCallback)); }
可以发现,这里也仅仅起到了传递参数的作用,其实是调用了 CreateNativeSslCredentials
函数。继续往下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private ChannelCredentialsSafeHandle CreateNativeSslCredentials ( string rootCertificates, KeyCertificatePair keyCertificatePair, VerifyPeerCallback verifyPeerCallback ){ IntPtr verifyPeerCallbackTag = IntPtr.Zero; if (verifyPeerCallback != null ) { verifyPeerCallbackTag = new VerifyPeerCallbackRegistration( verifyPeerCallback).CallbackRegistration.Tag; } return ChannelCredentialsSafeHandle.CreateSslCredentials( rootCertificates, keyCertificatePair, verifyPeerCallbackTag); }
再继续跟踪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static ChannelCredentialsSafeHandle CreateSslCredentials ( string pemRootCerts, KeyCertificatePair keyCertPair, IntPtr verifyPeerCallbackTag ){ if (keyCertPair != null ) { return Native.grpcsharp_ssl_credentials_create( pemRootCerts, keyCertPair.CertificateChain, keyCertPair.PrivateKey, verifyPeerCallbackTag); } else { return Native.grpcsharp_ssl_credentials_create( pemRootCerts, null , null , verifyPeerCallbackTag); } }
NativeMethods
中可以找到委托:
1 public readonly Delegates.grpcsharp_ssl_credentials_create_delegate grpcsharp_ssl_credentials_create;
到这里 C# 的代码基本上就截止了,现在需要进一步跟踪到 Native 函数中去。
C# Native 层 从 DllImportsFromSharedLib
的 ImportName
中可以发现这些 Native 函数是从 grpc_csharp_ext.c
中导入的,在 src/csharp/ext
中可以找到该文件。
顺带一提,_ext
的后缀是 grpc 针对各语言的扩展模块,不属于核心模块。
以下是 grpc_csharp_ext.c
中的 grpcsharp_ssl_credentials_create
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 grpcsharp_ssl_credentials_create (const char * pem_root_certs, const char * key_cert_pair_cert_chain, const char * key_cert_pair_private_key, void * verify_peer_callback_tag) { grpc_ssl_pem_key_cert_pair key_cert_pair; verify_peer_options verify_options; grpc_ssl_pem_key_cert_pair* key_cert_pair_ptr = NULL ; verify_peer_options* verify_options_ptr = NULL ; if (key_cert_pair_cert_chain || key_cert_pair_private_key) { memset (&key_cert_pair, 0 , sizeof (key_cert_pair)); key_cert_pair.cert_chain = key_cert_pair_cert_chain; key_cert_pair.private_key = key_cert_pair_private_key; key_cert_pair_ptr = &key_cert_pair; } else { GPR_ASSERT (!key_cert_pair_cert_chain); GPR_ASSERT (!key_cert_pair_private_key); } if (verify_peer_callback_tag != NULL ) { memset (&verify_options, 0 , sizeof (verify_peer_options)); verify_options.verify_peer_callback_userdata = verify_peer_callback_tag; verify_options.verify_peer_destruct = grpcsharp_verify_peer_destroy_handler; verify_options.verify_peer_callback = grpcsharp_verify_peer_handler; verify_options_ptr = &verify_options; } return grpc_ssl_credentials_create (pem_root_certs, key_cert_pair_ptr, verify_options_ptr, NULL ); }
看上去好大一堆!第一反应是总算找到干了点实际事情的函数了。
然而实际上却发现这一大堆代码都是在进行参数检测而已,实际上只有最后一句最关键,而最后一句干的事情仍然是参数传递。
Core 模块 现在开始进入核心模块了。首先看 ssl_credentials.h
文件,这里面包含了证书类的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class grpc_ssl_credentials : public grpc_channel_credentials { public : grpc_ssl_credentials (const char * pem_root_certs, grpc_ssl_pem_key_cert_pair* pem_key_cert_pair, const grpc_ssl_verify_peer_options* verify_options); ~grpc_ssl_credentials () override ; grpc_core::RefCountedPtr<grpc_channel_security_connector> create_security_connector ( grpc_core::RefCountedPtr<grpc_call_credentials> call_creds, const char * target, const grpc_channel_args* args, grpc_channel_args** new_args) override ; void set_min_tls_version (grpc_tls_version min_tls_version) ; void set_max_tls_version (grpc_tls_version max_tls_version) ; private : void build_config (const char * pem_root_certs, grpc_ssl_pem_key_cert_pair* pem_key_cert_pair, const grpc_ssl_verify_peer_options* verify_options) ; grpc_ssl_config config_; };
再来看实现。在 src/core/lib/security/credentials/ssl/ssl_credentials.cc
中找到对应的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 grpc_channel_credentials* grpc_ssl_credentials_create ( const char * pem_root_certs, grpc_ssl_pem_key_cert_pair* pem_key_cert_pair, const verify_peer_options* verify_options, void * reserved) { GRPC_API_TRACE ( "grpc_ssl_credentials_create(pem_root_certs=%s, " "pem_key_cert_pair=%p, " "verify_options=%p, " "reserved=%p)" , 4 , (pem_root_certs, pem_key_cert_pair, verify_options, reserved)); GPR_ASSERT (reserved == nullptr ); return new grpc_ssl_credentials ( pem_root_certs, pem_key_cert_pair, reinterpret_cast <const grpc_ssl_verify_peer_options*>( verify_options)); }
嗯,还是参数传递,继续:
1 2 3 4 5 6 grpc_ssl_credentials::grpc_ssl_credentials ( const char * pem_root_certs, grpc_ssl_pem_key_cert_pair* pem_key_cert_pair, const grpc_ssl_verify_peer_options* verify_options) : grpc_channel_credentials (GRPC_CHANNEL_CREDENTIALS_TYPE_SSL) { build_config (pem_root_certs, pem_key_cert_pair, verify_options); }
这就是上面头文件所定义类的构造方法实现,继续:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void grpc_ssl_credentials::build_config ( const char * pem_root_certs, grpc_ssl_pem_key_cert_pair* pem_key_cert_pair, const grpc_ssl_verify_peer_options* verify_options ) { config_.pem_root_certs = gpr_strdup (pem_root_certs); if (pem_key_cert_pair != nullptr ) { GPR_ASSERT (pem_key_cert_pair->private_key != nullptr ); GPR_ASSERT (pem_key_cert_pair->cert_chain != nullptr ); config_.pem_key_cert_pair = static_cast <tsi_ssl_pem_key_cert_pair*>( gpr_zalloc (sizeof (tsi_ssl_pem_key_cert_pair))); config_.pem_key_cert_pair->cert_chain = gpr_strdup (pem_key_cert_pair->cert_chain); config_.pem_key_cert_pair->private_key = gpr_strdup (pem_key_cert_pair->private_key); } else { config_.pem_key_cert_pair = nullptr ; } if (verify_options != nullptr ) { memcpy (&config_.verify_options, verify_options, sizeof (verify_peer_options)); } else { memset (&config_.verify_options, 0 , sizeof (verify_peer_options)); } }
总算找到点有意思的代码了。分析一下。
1 config_.pem_root_certs = gpr_strdup (pem_root_certs);
gpr_strdup
是一个将 const char*
转为 char*
的无关紧要的函数,不需要理会。
这段代码干了一件事情,将 pem_root_certs
写入 config_.pem_root_certs
。
那么 config_
是什么? 在 ssl_security_connector.h
里找到结构体:
1 2 3 4 5 6 7 struct grpc_ssl_config { tsi_ssl_pem_key_cert_pair* pem_key_cert_pair; char * pem_root_certs; verify_peer_options verify_options; grpc_tls_version min_tls_version = grpc_tls_version::TLS1_2; grpc_tls_version max_tls_version = grpc_tls_version::TLS1_3; };
这个结构体保存了 3 个关键变量。
pem_key_cert_pair*
:服务端证书和私钥的 pair
pem_root_certs
:所有可信的根证书字符串
verify_options
:用户自定义验证函数
到这里就很明显了。最开始的 SslCredentials
映射到 Core 模块就相当于这个结构体。
现在需要找到使用到这个结构体的地方,肯定就是检测证书的地方。可以从同一文件的注释中找到线索:
1 2 3 4 - secure_peer_name is the secure peer name that should be checked in grpc_channel_security_connector_check_peer. This parameter may be NULL in which case the peer name will not be checked. Note that if this parameter is not NULL, then, pem_root_certs should not be NULL either.
现在来到 ssl_security_connector.cc
文件中,找到关键方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void check_peer (tsi_peer peer, grpc_endpoint* , grpc_core::RefCountedPtr<grpc_auth_context>* auth_context, grpc_closure* on_peer_checked) override {const char * target_name = overridden_target_name_.empty () ? target_name_.c_str () : overridden_target_name_.c_str (); grpc_error_handle error = ssl_check_peer ( target_name, &peer, auth_context); if (error == GRPC_ERROR_NONE && verify_options_->verify_peer_callback != nullptr ) { const tsi_peer_property* p = tsi_peer_get_property_by_name (&peer, TSI_X509_PEM_CERT_PROPERTY); if (p == nullptr ) { error = GRPC_ERROR_CREATE_FROM_STATIC_STRING ( "Cannot check peer: missing pem cert property." ); } else { char * peer_pem = static_cast <char *>(gpr_malloc (p->value.length + 1 )); memcpy (peer_pem, p->value.data, p->value.length); peer_pem[p->value.length] = '\0' ; int callback_status = verify_options_->verify_peer_callback ( target_name, peer_pem, verify_options_->verify_peer_callback_userdata); gpr_free (peer_pem); if (callback_status) { error = GRPC_ERROR_CREATE_FROM_CPP_STRING (absl::StrFormat ( "Verify peer callback returned a failure (%d)" , callback_status)); } } } grpc_core::ExecCtx::Run (DEBUG_LOCATION, on_peer_checked, error); tsi_peer_destruct (&peer);}
没错,这里就是进行证书验证的地方!
关键点我已经用注释标出来了,简单再解释一下。
首先处理 target_name
,如果 Channel 有设置 ssl_target_name_override
则首先对其进行覆盖。
然后是关键点,调用 ssl_check_peer
函数对 hostname 和证书进行检测。
如果上一步没问题,并且用户自定义检测函数 verify_peer_callback
不为 null,则继续进行自定义检测。
调用 verify_options_->verify_peer_callback
,进行用户定义的检测。
最后找到同一文件中的 ssl_check_peer
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 grpc_error_handle ssl_check_peer ( const char * peer_name, const tsi_peer* peer, grpc_core::RefCountedPtr<grpc_auth_context>* auth_context) { grpc_error_handle error = grpc_ssl_check_alpn (peer); if (error != GRPC_ERROR_NONE) { return error; } if (peer_name != nullptr && !grpc_ssl_host_matches_name (peer, peer_name)) { return GRPC_ERROR_CREATE_FROM_CPP_STRING ( absl::StrCat ("Peer name " , peer_name, " is not in peer certificate" )); } *auth_context = grpc_ssl_peer_to_auth_context (peer, GRPC_SSL_TRANSPORT_SECURITY_TYPE); return GRPC_ERROR_NONE; }
分析一下。
首先检测 ALPN(如果支持)。
如果 peer_name
不为 null,调用 grpc_ssl_host_matches_name
进行检测。
将 peer
写入 auth_context
。
在 lib/security/security_connector/ssl_utils.cc
中找到 grpc_ssl_host_matches_name
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int grpc_ssl_host_matches_name (const tsi_peer* peer, absl::string_view peer_name) { absl::string_view allocated_name; absl::string_view ignored_port; grpc_core::SplitHostPort (peer_name, &allocated_name, &ignored_port); if (allocated_name.empty ()) return 0 ; const size_t zone_id = allocated_name.find ('%' ); if (zone_id != absl::string_view::npos) { allocated_name.remove_suffix (allocated_name.size () - zone_id); } return tsi_ssl_peer_matches_name (peer, allocated_name); }
只看最后一句。继续到 tsi/ssl_transport_security.cc
中找到 tsi_ssl_peer_matches_name
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int tsi_ssl_peer_matches_name (const tsi_peer* peer, absl::string_view name) { size_t i = 0 ; size_t san_count = 0 ; const tsi_peer_property* cn_property = nullptr ; int like_ip = looks_like_ip_address (name); for (i = 0 ; i < peer->property_count; i++) { const tsi_peer_property* property = &peer->properties[i]; if (property->name == nullptr ) continue ; if (strcmp (property->name, TSI_X509_SUBJECT_ALTERNATIVE_NAME_PEER_PROPERTY) == 0 ) { san_count++; absl::string_view entry (property->value.data, property->value.length) ; if (!like_ip && does_entry_match_name (entry, name)) { return 1 ; } else if (like_ip && name == entry) { return 1 ; } } else if (strcmp (property->name, TSI_X509_SUBJECT_COMMON_NAME_PEER_PROPERTY) == 0 ) { cn_property = property; } } if (san_count == 0 && cn_property != nullptr && !like_ip) { if (does_entry_match_name (absl::string_view (cn_property->value.data, cn_property->value.length), name)) { return 1 ; } } return 0 ; }
注释中已经解释得很明确了。先检测证书中的 SAN 域,如果有匹配则立即返回成功;如果证书中没有 SAN 域,那么就对 CN 域进行检测。
最后调用同一文件中的 does_entry_match_name
函数检测,这个函数是真正的检测实现,里面包含了一大堆字符串处理和对比,内容很长且对分析没有太大意义所以不列出来了,只需要知道这个函数返回 1 则是成功,0 则是失败。
到此源码分析基本上就可以结束了。
总结 总结一下现在获得的信息。
ssl_check_peer
函数的位置是 lib/security/security_connector/ssl/ssl_security_connector.cc
ssl_check_peer
之后的一大堆函数都只进行了检测,没有做其他多余的操作
而 ssl_check_peer
里除了调用检测以外,还构造了一个 auth_context
ssl_check_peer
返回 GRPC_ERROR_NONE
则是成功
ssl_check_peer
的下一步是 grpc_ssl_host_matches_name
函数,它返回 1 则是成功
从上面的信息可以看出来,想要破除掉 gRPC 的证书检测机制,需要 hook ssl_check_peer
方法,使其返回 grpc_error_handle::GRPC_ERROR_NONE
,同时还需要修改 auth_context
,使其包含所有客户端信用的证书字符串。
前者很简单,直接设置返回值就可以了。而后者比较麻烦,需要自己构造 grpc_auth_context
。不过有一个比较简便的方法:与其自己从头开始构建 auth_context
,不如在 ssl_check_peer
的参数 tsi_peer*
中将我们需要的证书添加进去,这样甚至还不需要强行设置返回值为 GRPC_ERROR_NONE
,因为我们的假证书已经在客户端信用的 tsi_peer*
列表里了。
在 tsi/transport_security_interface.h
中能找到 tsi_peer
的结构体定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct tsi_peer_property { char * name; struct { char * data; size_t length; } value; } tsi_peer_property; struct tsi_peer { tsi_peer_property* properties; size_t property_count; };
非常简单明了的两个结构体。一个 tsi_peer_property
就是一个信赖的根证书,证书内容保存在 data 里。tsi_peer
中存有 tsi_peer_property
的指针,也就是所有可信的根证书,即 pem 文件中的证书。
也就是说,只需要构建一个 tsi_peer_property
,将其添加到 tsi_peer.properties
中,最后使 tsi_peer.property_count
+1,就搞定了。
References
gRPC 证书验证机制分析
2022/05/grpc-credentials/
Copyright of used screenshots are belong to their original publishers.