Skip to content
Wireshark Wiki 中文翻译整理专题首页原始页面

如何导出 gRPC 的 TLS 主密钥

Golang

grpc-go 借助 golang 的 keyLogWriter 功能,支持导出 gRPC 的 TLS 主密钥。

以下 gRPC 客户端代码基于 https://github.com/grpc/grpc-go/blob/v1.34.x/examples/features/encryption/TLS/client/main.go 修改而来。

packagemainimport("context""crypto/tls""crypto/x509""flag""fmt""io/ioutil""log""os""time""google.golang.org/grpc""google.golang.org/grpc/credentials""google.golang.org/grpc/examples/data"ecpb"google.golang.org/grpc/examples/features/proto/echo")// Note that I changed the port from 50051 to 60051, because I decode the // traffic on 50051 as plain HTTP2, and decode traffic on 60051 as TLS.// So that you should not forget to change server port from 50051 to 60051 too.varaddr=flag.String("addr","localhost:60051","the address to connect to")funccallUnaryEcho(clientecpb.EchoClient,messagestring){ctx,cancel:=context.WithTimeout(context.Background(),10*time.Second)defercancel()resp,err:=client.UnaryEcho(ctx,&ecpb.EchoRequest{Message:message})iferr!=nil{log.Fatalf("client.UnaryEcho(_) = _, %v: ",err)}fmt.Println("UnaryEcho: ",resp.Message)}// The keyFile is the path of key log file.funcNewCredentials(certFile,serverNameOverride,keyFilestring)(credentials.TransportCredentials,error){b,err:=ioutil.ReadFile(certFile)iferr!=nil{returnnil,err}cp:=x509.NewCertPool()if!cp.AppendCertsFromPEM(b){returnnil,fmt.Errorf("credentials: failed to append certificates")}// w: = os.Stdout // for debugw,err:=os.OpenFile(keyFile,os.O_WRONLY|os.O_CREATE|os.O_TRUNC,0600)iferr!=nil{returnnil,err}returncredentials.NewTLS(&tls.Config{ServerName:serverNameOverride,RootCAs:cp,KeyLogWriter:w,}),nil}funcmain(){flag.Parse()// Create tls based credential.creds,err:=NewCredentials(data.Path("x509/ca_cert.pem"),"x.test.example.com","d:\\keyfile.txt")iferr!=nil{log.Fatalf("failed to load credentials: %v",err)}// Set up a connection to the server.conn,err:=grpc.Dial(*addr,grpc.WithTransportCredentials(creds),grpc.WithBlock())iferr!=nil{log.Fatalf("did not connect: %v",err)}deferconn.Close()// Make a echo client and send an RPC.rgc:=ecpb.NewEchoClient(conn)callUnaryEcho(rgc,"hello world")}

运行后(命令类似 "$> go run client/main.go"),导出的密钥材料会写入文件 "d:\keyfile.txt"。你可以将 Wireshark TLS 的 '(Pre)-Master-Secret log filename' (tls.keylog_file) 首选项设置为此文件,并将 60051 端口上的流量解码为 tls,以解析加密的 gRPC 消息。关于如何使用 key log file 解析 TLS,请参阅 Wireshark TLS wiki 页面。

> 请注意,golang 的 TLS 实现不依赖 openssl 或 boringssl。

Java

有两种方式可以为 grpc-java 抓包文件导出 TLS 的密钥材料。

方案 1. 使用 jSSLKeyLog

这是最简单的方式,尽管它不支持原生 boringssl。

主要步骤包括:

  • https://jsslkeylog.github.io/ 下载 jSSLKeyLog。
  • 确保你的客户端或服务器代码使用 JDK security provider(通过 GrpcSslContexts.configure(sslServerContextBuilder, Security.getProvider("SunJSSE")))。
  • 然后使用 -javaagent:path/to/jSSLKeyLog.jar=/path/to/your_logfile.log VM 参数运行客户端或服务器程序,例如:
java -javaagent:mylibs/jSSLKeyLog.jar=d:/keyfile.txt ... -jar client_or_server.jar ...

你将得到 tls keyfile 'd:/keyfile.txt'。

如果你在 grpc 客户端侧抓包,初始化代码可能类似于:

...NettyChannelBuilderchannelBuilder=NettyChannelBuilder.forAddress(host,port);if(secure){SslContextBuildersslClientContextBuilder=GrpcSslContexts.forClient().trustManager(newFile("cert/server.crt"));sslClientContextBuilder.protocols("TLSv1.2");// Note that you can not use // GrpcSslContexts.configure(sslClientContextBuilder, SslProvider.OPENSSL).//// If you does not include boringssl dependency in your pom.xml (or class path),// you can also use GrpcSslContexts.configure(sslClientContextBuilder) instead.//// Security.getProvider("SunJSSE") also can be replaced// by Security.getProviders()[0] if boringssl library is not in your class path.SslContextclientSslCtx=GrpcSslContexts.configure(sslClientContextBuilder,Security.getProvider("SunJSSE")).build();channelBuilder.sslContext(clientSslCtx).negotiationType(NegotiationType.TLS);}else{channelBuilder.negotiationType(NegotiationType.PLAINTEXT);}this.channel=channelBuilder.build();blockingStub=newPersonSearchBlockingStub(channel,useJson);...

如果你在服务器侧抓包,服务器启动代码可能需要改为:

...NettyServerBuilderserverBuilder=NettyServerBuilder.forPort(port).addService(useJson?newPersonSearchJsonImpl():newPersonSearchImpl());if(secure){SslContextBuildersslServerContextBuilder=GrpcSslContexts.forServer(newFile(certChainFilePath),newFile(privateKeyFilePath));//Note that you can use GrpcSslContexts.configure(sslServerContextBuilder, SslProvider.OPENSSL);SslContextBuilderserverSslCtxBuilder=GrpcSslContexts.configure(sslServerContextBuilder,Security.getProvider("SunJSSE"));SslContextserverSslCtx=serverSslCtxBuilder.build();serverBuilder.sslContext(serverSslCtx);}server=serverBuilder.build().start();...

测试环境:

  • grpc-java 版本:1.36.0-SNAPSHOT
  • protobuf 和 protoc 版本:3.12.0
  • openJDK:
  • openjdk-8u252-b09:支持 TLS1.2,但不支持 TLS1.3
  • openjdk-11.0.13_8:同时支持 TLS1.2 和 TLS1.3

方案 2. 重写 ProtocolNegotiators 类

这只是一个技巧,不推荐使用。你可以基于所使用 grpc-java 库版本的源代码重写 java 类 'io.grpc.netty.ProtocolNegotiators',并将其放入 classpath。关键修改是在 ClientTlsHandler 类的 handlerAdded0 方法或 ServerTlsHandler 类的 handlerAdded 方法中添加以下代码:

ctx.pipeline().addBefore(ctx.name(),null,SslMasterKeyHandler.newWireSharkSslMasterKeyHandler());

例如,如果将其放在 ClientTlsHandler 类中,用于在 grpc 客户端侧导出 master key:

@OverrideprotectedvoidhandlerAdded0(ChannelHandlerContextctx){SSLEnginesslEngine=sslContext.newEngine(ctx.alloc(),host,port);SSLParameterssslParams=sslEngine.getSSLParameters();sslParams.setEndpointIdentificationAlgorithm("HTTPS");sslEngine.setSSLParameters(sslParams);ctx.pipeline().addBefore(ctx.name(),/* name= */null,this.executor!=null?newSslHandler(sslEngine,false,this.executor):newSslHandler(sslEngine,false));// add following code for exporting master for wiresharkif(Boolean.getBoolean(SslMasterKeyHandler.SYSTEM_PROP_KEY)){ctx.pipeline().addBefore(ctx.name(),null,SslMasterKeyHandler.newWireSharkSslMasterKeyHandler());}}

这会将 master keys 写入名为 "io.netty.wireshark" 的日志。你可以配置 log4j.xml,将 master keys 导出到 key log file:

<appendername="key-file"class="org.apache.log4j.RollingFileAppender"><paramname="file"value="d:/keyfile.txt"/><layoutclass="org.apache.log4j.PatternLayout"><paramname="ConversionPattern"value="%m%n"/></layout></appender><categoryname="io.netty.wireshark"><priorityvalue="DEBUG"/><appender-refref="key-file"/></category>

通过设置系统属性启用此功能:

 -Dio.netty.ssl.masterKeyHandler=true

System.setProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY,"true");

这会使 master keys 被写入文件 `d:\keyfile.txt'。

这只是一个变通方法,因为类 'io.grpc.netty.ProtocolNegotiators' 在每个版本中可能不同。未来可能会提供基于 netty 的 SslMasterKeyHandler 功能的正式解决方案。

C++

"TLS Session Keys export for GRPC C++" PR(pull request)已合并(https://github.com/grpc/grpc/pull/26812),并且自 grpc v1.45.0 起以 EXPERIMENTAL API 形式支持该功能。

我们可以通过以下 EXPERIMENTAL API(位于 include/grpc/grpc_security.h)配置 key log file:

/** * EXPERIMENTAL API - Subject to change. * Configures a grpc_tls_credentials_options object with tls session key * logging capability. TLS channels using these credentials have tls session * key logging enabled. * - options is the grpc_tls_credentials_options object * - path is a string pointing to the location where TLS session keys would be * stored. */GRPCAPIvoidgrpc_tls_credentials_options_set_tls_session_key_log_file_path(grpc_tls_credentials_options*options,constchar*path);

或者使用 TlsCredentialsOptions 对象的 C++ 方法:

voidTlsCredentialsOptions::set_tls_session_key_log_file_path(conststd::string&tls_session_key_log_file_path)

> set_tls_session_key_log_file_path 方法依赖 openssl 或 boringssl 的 SSL_CTX_set_keylog_callback(自 openssl 1.1.1 起)或 SSL_get_client_random/SSL_get_server_random/SSL_SESSION_get_master_key 方法。

> 此函数仅为 EXPERIMENTAL,是因为 TlsCredentialsOptions 目前也是 EXPERIMENTAL。

C#/Python/...

目前,这些语言的 GRPC 实现不支持此功能。不过,由于其中一些实现都基于 GRPC C++,它们有可能在不久的将来支持此功能。

你也可以尝试 Wireshark tls wiki 页面中描述的方法。

> 如果有新的进展,此 wiki 页面将会更新。

示例抓包

  • grpc_person_search_protobuf_and_json_tls.pcapng -- Person search gRPC 示例抓包(60051 用于 protobuf payload,60052 用于 json)。
  • grpc_person_search_protobuf_and_json_tls.keylog.txt -- grpc_person_search_protobuf_and_json_tls.pcapng 的 Key log file。

相关 Wireshark Wiki 页面

网络分析技术档案