序言
安全需求
安全方案
敏感数据加密传输
认证
鉴权
数据完整性和一致性
证书的基本原理
单向证书
双向证书
gRPC安全机制
SSL/TLS认证
GoogleOAuth2.0
自定义安全认证策略
网络安全领域在攻和防对抗规模群体已经成熟,但是两端从业者对于安全原理掌握程度参差不齐,中间鸿沟般的差距构成了漏洞研究领域的主战场。笔者“三省吾身”,在工作中会犯错误把一些加密、认证、鉴权的概念和实现方案搞混,尤其是加解密涉及算法和公私钥机制的概念不深入细节。
最近的几个影响颇大的安全漏洞,Apache Shiro 权限绕过漏洞、CVE-2020-14882weblogic 绕过登录、微软ZeroLogon,这些漏洞原理的共同点都是和基本的安全算法、认证鉴权方案缺陷有关。也许未来的漏洞攻防将转移到安全基础领域的对抗,从业人员除了要求推进安全方案的必要性,涉及安全建设的可用性更为重要,所以特此开专栏系列,为大家普及一些安全基本功。
本文主要通过介绍gRPC的双向认证方案,理清证书领域的知识。
RPC是一种技术思想,实现有阿里的 Dubbo/SOFA、Google gRPC、Facebook 的 Thrift,实现时的远程通信规范和协议可以用RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。这种服务间通信机制为企业内部各系统、模块之间的微服务和接口之间互相调用,RPC实现需要考虑安全性,RPC 调用安全主要涉及如下三点:
个人 / 企业敏感数据加密:例如针对个人的账号、密码、手机号等敏感信息进行加密传输,打印接口日志时需要做数据模糊化处理等,不能明文打印;
对调用方的身份认证:调用来源是否合法,是否有访问某个资源的权限,防止越权访问;
数据防篡改和完整性:通过对请求参数、消息头和消息体做签名,防止请求消息在传输过程中被非法篡改。
常见的安全攻防重视rpc协议的反序列漏洞,但是如果业务方问道如果做以上的安全需求,SDL同学就傻眼了,正确的做法是区分加密传输、认证、鉴权、数据完整性和一致性四个方向:
当存在跨网络边界的 RPC 调用时,往往需要通过 TLS/SSL 对传输通道进行加密,以防止请求和响应消息中的敏感数据泄漏。跨网络边界调用场景主要有三种:
后端微服务直接开放给端侧,例如手机 App、TV、多屏等,没有统一的 API 网关/SLB 做安全接入和认证;
后端微服务直接开放给 DMZ 部署的管理或者运维类 Portal;
后端微服务直接开放给第三方合作伙伴 / 渠道。
除了跨网络之外,对于一些安全等级要求比较高的业务场景,即便是内网通信,只要跨主机 /VM/ 容器通信,都强制要求对传输通道进行加密。在该场景下,即便只存在内网各模块的 RPC 调用,仍然需要做 SSL/TLS。
使用 SSL/TLS 的典型场景如下所示:
通道加密的的实现技术难度稍大,对性能有损耗,定制化程度高,但是效果显著,建设收益明显
有些 RPC 调用并不涉及敏感数据的传输,或者敏感字段占比较低,为了最大程度的提升吞吐量,降低调用时延,通常会采用 HTTP/TCP + 敏感字段单独加密的方式,既保障了敏感信息的传输安全,同时也降低了采用 SSL/TLS 加密通道带来的性能损耗,对于 JDK 原生的 SSL 类库,这种性能提升尤其明显。
它的工作原理如下所示:
敏感数据加密
通常使用 Handler 拦截机制,对请求和响应消息进行统一拦截,根据注解或者加解密标识对敏感字段进行加解密,这样可以避免侵入业务。
采用该方案的缺点主要有两个:
对敏感信息的识别可能存在偏差,容易遗漏或者过度保护,需要解读数据和隐私保护方面的法律法规,而且不同国家对敏感数据的定义也不同,这会为识别带来很多困难;
接口升级时容易遗漏,例如开发新增字段,忘记识别是否为敏感数据。
内部 RPC 调用的身份认证场景,主要有如下两大类:
防止对方知道服务提供者的地址之后,绕过注册中心 / 服务路由策略直接访问 RPC 服务提供端;
RPC 服务只想供内部模块调用,不想开放给其它业务系统使用(双方网络是互通的)。
身份认证的方式较多,例如 HTTP Basic Authentication、OAuth2 等,比较简单使用的是令牌认证(Token)机制,它的工作原理如下所示:
工作原理如下:
RPC 客户端和服务端通过 HTTPS 与注册中心连接,做双向认证,以保证客户端和服务端与注册中心之间的安全;
服务端生成 Token 并注册到注册中心,由注册中心下发给订阅者。通过订阅 / 发布机制,向 RPC 客户端做 Token 授权;
服务端开启身份认证,对 RPC 调用进行 Token 校验,认证通过之后才允许调用后端服务接口。
身份认证可以防止非法调用,如果需要对调用方进行更细粒度的权限管控,则需要做对 RPC 调用做鉴权。例如管理员可以查看、修改和删除某个后台资源,而普通用户只能查看资源,不能对资源做管理操作。
在 RPC 调用领域比较流行的是基于 OAuth2.0 的权限认证机制,它的工作原理如下:
OAuth2.0 的认证流程如下:
客户端向资源拥有者申请授权(例如携带用户名 + 密码等证明身份信息的凭证);
资源拥有者对客户端身份进行校验,通过之后同意授权;
客户端使用步骤 2 的授权凭证,向认证服务器申请资源访问令牌(access token);
认证服务器对授权凭证进行合法性校验,通过之后,颁发 access token;
客户端携带 access token(通常在 HTTP Header 中)访问后端资源,例如发起 RPC 调用;
服务端对 access token 合法性进行校验(是否合法、是否过期等),同时对 token 进行解析,获取客户端的身份信息以及对应的资源访问权限列表,实现对资源访问权限的细粒度管控;
access token 校验通过,返回资源信息给客户端。
步骤 2 的用户授权,有四种方式:
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
需要指出的是,OAuth 2.0 是一个规范,不同厂商即便遵循该规范,实现也可能会存在细微的差异。大部分厂商在采用 OAuth 2.0 的基础之上,往往会衍生出自己特有的 OAuth 2.0 实现。
对于 access token,为了提升性能,RPC 服务端往往会缓存,不需要每次调用都与 AS 服务器做交互。同时,access token 是有过期时间的,根据业务的差异,过期时间也会不同。客户端在 token 过期之前,需要刷新 Token,或者申请一个新的 Token。
考虑到 access token 的安全,通常选择 SSL/TLS 加密传输,或者对 access token 单独做加密,防止 access token 泄漏。
关于oauth作为安全基本功系列今后还会有专栏。
RPC 调用,除了数据的机密性和有效性之外,还有数据的完整性和一致性需要保证,即如何保证接收方收到的数据与发送方发出的数据是完全相同的。
利用消息摘要可以保障数据的完整性和一致性,它的特点如下:
单向 Hash 算法,从明文到密文的不可逆过程,即只能加密而不能解密;
无论消息大小,经过消息摘要算法加密之后得到的密文长度都是固定的;
输入相同,则输出一定相同。
目前常用的消息摘要算法是 SHA-1、MD5 和 hmac,MD5 可产生一个 128 位的散列值。SHA-1 则是以 MD5 为原型设计的安全散列算法,可产生一个 160 位的散列值,安全性更高一些。hmac 除了能够保证消息的完整性,还能够保证来源的真实性。
由于 MD5 已被发现有许多漏洞,在实际应用中更多使用 SHA 和 hmac,而且往往会把数字签名和消息摘要混合起来使用。微信支付、阿里云调用是大家常用的签名机制,注意消息摘要不是加密,不是加密,不是加密。
目前使用最广的 SSL/TLS 工具 / 类库就是 OpenSSL,它是为网络通信提供安全及数据完整性的一种安全协议,囊括了主要的密码算法、常用的密钥和证书封装管理功能以及 SSL 协议。注意SSL和TLS有不同的历史和标准,HTTPS的意思是HTTP +SSL/ TLS,现在的安全方案一般是tls实现,SSL标准正被淘汰。只是因为沿袭历史称呼,所以经常混用两次名词, SSL被发现存在过 POODLE, DROWN协议算法本身的漏洞,注意区分大名鼎鼎的心脏滴血漏洞Heartbleed是OpenSSL的实现TLS和DTLS的心跳处理逻辑时有bug,而不是利用SSL/TLS协议本身的缺陷。
https是大家最熟悉的单项证书方案,由浏览器、ca中心、服务端三方实现。单向认证的过程,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。单向认证流程中,服务器端保存着公钥证书和私钥两个文件,整个握手过程如下:
单向认证流程
客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务器端;
服务器端将本机的公钥证书(server.crt)发送给客户端;
客户端读取公钥证书(server.crt),取出了服务端公钥;
客户端生成一个随机数(密钥R),用刚才得到的服务器公钥去加密这个随机数形成密文,发送给服务端;
服务端用自己的私钥(server.key)去解密这个密文,得到了密钥R
服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
双向通信流程,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。
双向认证流程
客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端;
服务器端将本机的公钥证书(server.crt)发送给客户端;
客户端读取公钥证书(server.crt),取出了服务端公钥;
客户端将客户端公钥证书(client.crt)发送给服务器端;
服务器端解密客户端公钥证书,拿到客户端公钥;
客户端发送自己支持的加密方案给服务器端;
服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后发送给客户端;
客户端使用自己的私钥解密加密方案,生成一个随机数R,使用服务器公钥加密后传给服务器端;
服务端用自己的私钥去解密这个密文,得到了密钥R
服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
整个双向认证的流程跑通,最终需要五个证书文件:
服务器端公钥证书:server.crt
服务器端私钥文件:server.key
客户端公钥证书:client.crt
客户端私钥文件:client.key
客户端集成证书(包括公钥和私钥,用于浏览器访问场景):client.p12
生成这一些列证书之前,我们需要先生成一个CA根证书,然后由这个CA根证书颁发服务器公钥证书和客户端公钥证书。
证书生成
证书实现的核心是加密,但是也可以被用来做认证,比如istio实现展示了如何用双向证书解决身份、通讯安全,:服务器身份(Server identities)被编码在证书里,但服务名称(service names)通过服务发现或 DNS 被检索。安全命名信息将服务器身份映射到服务名称。身份 A 到服务名称 B 的映射表示“授权 A 运行服务 B“。在双向 TLS 握手期间,客户端Envoy做了安全命名检查,以验证服务器证书中显示的服务帐户是否被授权运行目标服务。
谷歌提供了可扩展的安全认证机制,以满足不同业务场景需求,它提供的授权机制主要有四类:
通道凭证:默认提供了基于 HTTP/2 的 TLS,对客户端和服务端交换的所有数据进行加密传输;
调用凭证:被附加在每次 RPC 调用上,通过 Credentials 将认证信息附加到消息头中,由服务端做授权认证;
组合凭证:将一个频道凭证和一个调用凭证关联起来创建一个新的频道凭证,在这个频道上的每次调用会发送组合的调用凭证来作为授权数据,最典型的场景就是使用 HTTP S 来传输 Access Token;
Google 的 OAuth 2.0:gRPC 内置的谷歌的 OAuth 2.0 认证机制,通过 gRPC 访问 Google API 时,使用 Service Accounts 密钥作为凭证获取授权令牌。
用go语言显示下服务端和客户端的调用过程:
服务端使用了证书文件
func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } // create the TLS credentials from files creds, err := credentials.NewServerTLSFromFile("../cert/server.crt", "../cert/server.key") if err != nil { log.Fatalf("could not load TLS keys: %s", err) } // create a gRPC option array with the credentials opts := []grpc.ServerOption{grpc.Creds(creds)} // create a gRPC server object with server options(opts) s := grpc.NewServer(opts...) pb.RegisterSimpleMathServer(s, &rpcimpl.SimpleMathServer{}) reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
客户端使用
func GreatCommonDivisor(first, second string) { // create the client TLS credentials creds, err := credentials.NewClientTLSFromFile("../cert/server.crt", "") // initiate a connection with the server using creds conn, err := grpc.Dial(address, grpc.WithTransportCredentials(creds)) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewSimpleMathClient(conn) a, _ := strconv.ParseInt(first, 10, 32) b, _ := strconv.ParseInt(second, 10, 32) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.GreatCommonDivisor(ctx, &pb.GCDRequest{First: int32(a), Second: int32(b)}) if err != nil { log.Fatalf("cound not compute: %v", err) } log.Printf("The Greatest Common Divisor of %d and %d is %d", a, b, r.Result) }
gRPC 默认提供了多种 OAuth 2.0 认证机制,假如 gRPC 应用运行在 GCE 里,可以通过服务账号的密钥生成 Token 用于 RPC 调用的鉴权,密钥可以从环境变量 GOOGLE_APPLICATION_CREDENTIALS 对应的文件里加载。如果使用 GCE,可以在虚拟机设置的时候为其配置一个默认的服务账号,运行时可以与认证系统交互并为 Channel 生成 RPC 调用时的 access Token。
参考 Google 内置的 Credentials 实现类,实现自定义的 Credentials,可以扩展 gRPC 的鉴权策略。