gRPC 证书验证机制分析

前段时间为了获取某通信采用 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
/// <summary>
/// Populates channel credentials configurator with this instance's configuration.
/// End users never need to invoke this method as it is part of internal implementation.
/// </summary>
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 层

DllImportsFromSharedLibImportName 中可以发现这些 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;

// TODO(mattstev): Plumb to wrapped languages. Until then, setting the TLS
// version should be done for testing purposes only.
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
/* Deprecated in favor of grpc_ssl_credentials_create_ex. Will be removed
* once all of its call sites are migrated to grpc_ssl_credentials_create_ex. */
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)); // 记录log
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)); // verify_options = null
}

嗯,还是参数传递,继续:

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 // = null
) {
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 {
// Otherwise set all options to default values
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* /*ep*/,
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::GRPC_ERROR_NONE
grpc_error_handle error = ssl_check_peer(
target_name, &peer,
auth_context);
// 如果默认验证成功,并且还要自定义验证时才调用
// 说明自定义验证只是起到额外验证作用,不会覆盖掉gRPC内置的验证
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';
// 调用自定义验证函数,返回不为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);
}

没错,这里就是进行证书验证的地方!

关键点我已经用注释标出来了,简单再解释一下。

  1. 首先处理 target_name,如果 Channel 有设置 ssl_target_name_override 则首先对其进行覆盖。
  2. 然后是关键点,调用 ssl_check_peer 函数对 hostname 和证书进行检测。
  3. 如果上一步没问题,并且用户自定义检测函数 verify_peer_callback 不为 null,则继续进行自定义检测。
  4. 调用 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;
}
/* Check the peer name if specified. */
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;
}

分析一下。

  1. 首先检测 ALPN(如果支持)。
  2. 如果 peer_name 不为 null,调用 grpc_ssl_host_matches_name 进行检测。
  3. 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;

// IPv6 zone-id should not be included in comparisons.
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);

/* Check the SAN first. */
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++;
// entry 是从 pem 文件里取出的一个证书
absl::string_view entry(property->value.data, property->value.length);
if (!like_ip && does_entry_match_name(entry, name)) {
return 1; // 一旦检测到 SAN 中有符合的域名,立即返回成功
} else if (like_ip && name == entry) {
/* IP Addresses are exact matches only. */
return 1;
}
} else if (strcmp(property->name,
TSI_X509_SUBJECT_COMMON_NAME_PEER_PROPERTY) == 0) {
cn_property = property;
}
}

/* If there's no SAN, try the CN, but only if its not like an IP Address */
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; /* Not found. */
}

注释中已经解释得很明确了。先检测证书中的 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
/* Property values may contain NULL characters just like C++ strings.
The length field gives the length of the string. */
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

 Comments
Comment plugin failed to load
Loading comment plugin