跳转到: 导航, 搜索

MessageSecurity

消息安全

目前 OpenStack 中的消息安全尚未实现。最近有一些提案,旨在为 RPC 消息实现签名,并最终实现加密。

实现这些安全特性是一项微妙的任务,因为存在安全性和性能之间的通常权衡,以及 OpenStack 分布式环境的特殊问题。

公钥加密与共享密钥

当前提案中的一个假设是,将使用公钥加密来提供消息的完整性,但是简单的共享密钥加密模型也适用于消息队列。

提出公钥加密的一个原因是,人们认为公钥信任模型的开销较低。但是,让我们分析一下使用任何一种模型需要什么。

公钥基础设施

首先,需要发送消息的每个服务都需要拥有一个公钥/私钥对,这意味着需要某种形式的安全存储来存储私钥。

接下来,公钥也必须可用。有两种策略可以做到这一点:一种是 PKI 模型,其中公钥由中央机构签名;另一种是受信任的存储库,其中所有公钥都存入,并且通过交叉签名或可信方保证其有效性。这反过来要求客户端定期检查其对等方的密钥是否仍然有效且未被撤销。无论是使用 CRL 或 OCSP 响应器查询的 PKI 样式系统,还是检查公钥有效性的中央信任机构,都是如此。

还有一项非平凡的任务是确定密钥的生成位置,因为基于虚拟机的系统往往缺乏足够的熵,并且在安装时生成密钥对可能存在问题。之后,存在如何将公钥传达给 CA 进行签名或将其传达给受信任存储库进行存入的问题。

共享密钥基础设施

首先,需要发送消息的每个服务都需要拥有自己的密钥,这意味着需要某种形式的安全存储来存储该密钥。

在共享密钥模型中,理想情况下,每个参与者都应该与每个其他对等方拥有不同的共享密钥,但是随着对等方数量的增加,这很快就变得不可能实现,无论是在存储方面还是在所需的交换方面。密钥服务器方法是唯一合理的途径。

使用密钥服务器,每个服务只需要一个与密钥服务器共享的密钥。实际用于在任何两个对等方之间通信的密钥由密钥服务器提供,至少在它们可以相互发送消息之前,其中一个对等方需要联系密钥服务器。密钥服务器将提供票据,其中包含绑定到特定对等方对的签名和加密密钥 (SEK),并允许它们安全地通信。

一旦获得密钥,这两个对等方就从密钥服务器独立出来,可以在密钥无效之前发送任意数量的消息。在这样的系统中,服务和密钥服务器之间共享的密钥具有长期有效性,而签名和加密密钥可以具有相对较短的有效期限,以便对消息的暴力破解攻击不会导致访问任何长期密钥,并且价值有限。

可信服务器的安全考虑

无论哪种模型,中央服务器都必须存储一些密钥并保证其有效性。

在公钥模型中,中央服务器需要能够提供密钥有效性的证明,并标记被认为已泄露的密钥为已撤销。为此,需要由该服务器处理签名密钥,并使用签名通过 CRL 或 OCSP 标记公钥为有效或已撤销。对于短寿命公钥,需要提供身份验证系统,以便服务可以进行身份验证并存储其公钥。在两种情况下,整个系统都必须依赖于识别受信任机构的某个公钥,无论是代表 PKI 的公钥和证书,还是用于保护与受信任存储库的连接的公钥和 x509 证书。无论哪种情况,该密钥的泄露都会危及整个系统。

在共享密钥模型中,密钥服务器将持有用于加密其存储中的服务密钥的主密钥。服务和密钥服务器之间的身份验证基于一个可以轻松轮换的共享密钥,除非该密钥已被泄露。无需撤销,因为所有需要的操作就是从密钥服务器中删除已泄露的密钥。但是,生成新的共享密钥将需要重复整个注册过程,并且如果密钥在与其他服务器的会话中间泄露,则需要终止所有这些会话,因为无法确定哪些会话是真实的,哪些是虚假的。

无论哪种系统,无状态服务都需要联系受信任的机构,无论是 PKI、受信任的存储库还是密钥服务器,以便获取会话密钥或检查撤销状态。对于两种系统,可以缓存会话密钥或撤销检查直到密钥过期或下一个验证间隔过期,因此服务与中央系统之间的所需通信开销是相似的。

共享密钥和密钥服务器提案

与纯粹的基于公钥的系统相比,使用密钥服务器的一个优势是,密钥服务器可以规范加密和签名密钥交换,并可以应用访问控制,从而拒绝系统中任意对等方之间的通信。这使得执行集中式访问控制、防止未经授权的通信以及避免在接收端执行事后身份验证访问控制和策略查找变得更加容易。

鉴于否则,无论是基于公钥的系统还是基于共享密钥的系统,在可信服务器的安全性和通信要求方面,开销看起来相似,因此我们提出使用基于密钥服务器的共享密钥系统。

请注意,存储在密钥服务器中的服务长期密钥可用于派生,也可用于身份验证,但是密钥服务器的身份验证可以推迟到现有组件,例如通过 HTTPS 连接的基于密码的身份验证就足以对服务进行身份验证到密钥服务器。其他可行的方法包括 Kerberos keytabs 和 KDC 进行身份验证、x509 用户证书等。基本上,密钥服务器部分的身份验证可以根据需要抽象出来。

也就是说,我们还将描述一种基于共享密钥的身份验证方法,该方法适用于通过纯 HTTP(未加密)传输与密钥服务器的通信。

消息完整性和机密性

保护消息队列需要两个不同的组件

  • 完整性或消息签名和身份验证
  • 机密性或消息加密

为了减少一些身份验证和加密密钥的密码分析机会,我们将安全地提议使用单独的密钥进行加密和身份验证,即使我们不会使用容易受到已知攻击的机制。出于同样的原因,为了减少重放攻击,我们将提议一种使用不同密钥取决于通信方向的方案。例如,Svc.A -> Svc.B 的 SEK 对将与 Svc.B -> Svc.A 的 SEK 对不同。

标准

提供消息完整性的标准是HMAC。对于加密,目前最受尊敬的算法是AES,一种具有固定 128 位块大小的块密码。

由于目前的感觉是加密可能不是必需的,我们将将其视为可选的。为了避免更改消息格式,这意味着更方便的做法是使用“先加密,后身份验证”的方法,即身份验证步骤不因是否执行加密而异,而是被身份验证的消息可以是纯文本或加密的。

下一步是概述如何将加密和身份验证应用于消息,同时牢记霍顿原则

消息格式

该项目中使用的数据交换格式是JSON,因此我们将创建一个基于 JSON 语法的消息格式。我们想要确保的第一件事是身份验证涵盖所有消息以及与消息相关的元数据,这对于避免替换攻击非常重要,其中元数据可能会被交换并替换而不会影响签名。这意味着消息和元数据将是包含在一个更简单的容器中的序列化对象。

伪 JSON 符号

MetaData = jsonutils.dumps({
    'source': <sender>,
    'destination': <receiver>,
    'timestamp': <time.time()>, # 1/100th second resolution from UTC
    'nonce': <64bit unsigned number>, # must not repeat until the timestamp changes
    'esek': <encrypted SEK pair for the receiver (base64 encoded)>,
    'encryption': <true | false>
})
Message = jsonutils.dumps(raw_msg)

_METADATA_KEY = 'oslo.secure.metadata'
_SIGNATURE_KEY = 'oslo.secure.hmac'

RPC_Message = {
    _VERSION_KEY: _RPC_ENVELOPE_VERSION,
    _METADATA_KEY: MetaData,
    _MESSAGE_KEY: Message,
    _SIGNATURE_KEY: Signature
}

消息签名

签名是根据版本字符串和缓冲区的串联计算的。

Version = null terminated string containing the version number
MetaData = serialized JSON Metadata
Message = serialized JSON Message

Signature = HMAC(SignKey, (Version || MetaData || Message))

我们建议默认使用 HMAC-SHA-256 作为身份验证函数,如RFC 6234 中所述。

注意:需要特别注意确保输入的 RPC_Message 不能被滥用,并且管道的其余部分将只使用经过身份验证的内容。为此,验证函数应该输出一个单独的结构,该结构提供了解序列化的元数据和消息,并且进一步的组件不应访问原始 RPC_Message。如果需要维护相同的格式,将提供一个包含版本和序列化消息的新 RPC_Message 作为输出,该消息是从经过验证的值重建的。

Hashlib 具有实现此目的所需的所有代码。

消息加密

可选地,消息可以被加密,在这种情况下,元数据字段“encryption”将被设置为 True。

由于使用 nonce 尤其难以正确处理,并且使用消息队列可能涉及多个使用相同密钥的参与方,并且由于希望尽可能允许无状态服务,因此我们建议默认使用 AES-128-CBC 与随机 IV,以便加密内容。这需要发送端具有伪随机生成器,我们预计在典型的 OpenStack 部署中这不会成为问题。

加密

Plain-Text = P1 || P2 || P3 || ...
C0 = Random IV (128bit)
for i in range(1, N):
   Ci = ENC(EncKey, Pi^Ci-1)
Encrypted-Message = C0 || C1 || C2 || C3 || ...

解密

IV = C0
Cipher-Text = C1 || C2 || C3 || ...
for i in range (1, N):
    Pi = DEC(EncKey, Ci)^Ci-1
Plain-Text = P1 || P2 || P3 || ...

各种 python 加密模块具有实现此目的所需的所有代码。

票据

为了获得发送消息所需的签名和加密密钥,客户端需要从密钥分发服务器请求包含这些密钥的票据。获取票据需要客户端向 KDS 进行身份验证。

客户端身份验证和密钥派生

我们提出了一种请求和传输票据的身份验证和密钥检索方案。

身份验证方案

使用一个简单的身份验证方案来请求票据。请求不需要加密,因为发送的任何数据都不是敏感的,并且所有数据都可以从将要执行的活动中推断出来。最后,其中一些数据需要以明文形式存在,以便识别请求的服务并查找用于验证请求的正确密钥。

我们希望减少消耗密钥服务器资源的能力,因此我们将嵌入一个对限制任何给定消息的有效期限有用的时间戳。

除了时间戳之外,请求还需要包含三个名称。

  • 发出请求的服务名称,将用于查找共享密钥并验证请求。
  • 目标服务的名称。

接收到请求后,第一步是使用共享密钥对请求进行身份验证。验证请求后,必须检查时间戳的有效性。

伪 JSON 符号

MetaData = jsonutils.dumps({
    'requestor': <requestor>,
    'target': <target>,
    'timestamp': <time.time()>, # 1/100th second resolution from UTC
    'nonce': <64bit unsigned number>, # must not repeat until the timestamp changes
})

KeyEx_Request = {
    'metadata': MetaData, # base64 encoded
    'signature': Signature = HMAC(Key, MetaData) # base64 encoded
}

注意:正如消息签名一样,我们在这里也使用时间戳和 nonce,重放攻击对密钥服务器来说不是问题,但从一开始过滤时间戳/nonce 可以节省密钥服务器的资源。因此,检查重放攻击是可选的,但值得欢迎。

注意:如果使用外部身份验证,签名将被省略。

密钥派生

为了避免对密钥进行简单的攻击,并且为了能够快速使密钥过期,使用了密钥派生方案来生成 SEK 对。

在所有情况下,无论使用共享密钥进行客户端身份验证还是通过外部方式(例如通过 HTTPS 上的 x509 证书)进行身份验证,密钥服务器都将维护(或在需要时动态创建)一个长期服务密钥(在我们的身份验证方案中也是共享密钥),该密钥用于代表服务器执行密钥派生。这些密钥以加密形式存储,并使用密钥服务器主密钥进行加密。

密钥派生使用标准基于哈希的密钥派生函数 (HKDF) 进行,如RFC 5869 中所述。

提取函数可以与密钥服务器一起使用,并使用每次全新生成的随机盐和与请求者共享的密钥。或者,可以生成一个随机密钥并跳过提取函数。这取决于实现,并且不影响协议结果。

扩展函数接收输入参数以根据涉及的发送者/接收者/时间戳三元组生成不同的密钥,从而将会话密钥绑定到该三元组。

密钥派生输入

Time.T = The time in the request
TTL = Time To Leave, validity in seconds from Time.T
Svc.A = the sender service name
Svc.B = the receiver service name
Key.A = the sender long term key
Rnd.Salt = a random salt used for the extract function
Rnd.Key = the Key used as input for the expand function
Ls = Length of Signing Key (128bits)
Le = Lenght of Encryption Key (128bits)

提取函数(可选)

Rnd.Key = HKDF-Extract(Rnd.Salt, Key.A)

扩展函数

SEK = HKDF-Expand(Rnd.Key, Svc.A+','+Svc.B+','+Time.T, Ls+Le)

扩展函数输出的是一个长度为 256 位的字节数组 (Ls+Le),前一半将用作签名密钥,后一半用作加密密钥。

密钥交换

密钥派生步骤中获得的密钥需要发送回请求者。

此外,为了避免发送者和接收者都向密钥服务器查询,在正常情况下,我们发送使用接收者密钥加密的扩展函数随机密钥。

KeyData = jsonutils.dumps({
    'key': Rnd.Key, (base64 encoded to avoid json mangling)
    'timestamp': Time.T,
    'ttl': TTL
})

Esek = ENC(Key.B, KeyData)

源和目标不包含在内,因为它们已经随每条消息由发送者发送。 通过不在 Esek 中包含它们,我们强制接收者隐式检查它们是否有效,并避免接收者忘记检查消息元数据中的目标与加密 Esek 中的目标是否匹配的风险。

如果通信是通过经过验证的 HTTPS 等安全传输进行的,那么可以直接以明文形式返回 Ticket,但是,如果身份验证方案是通过 HTTP 等明文协议使用的,则需要使用加密来保护密钥。 为了避免混淆和可能的错误,我们采取保守的方法并始终以加密形式返回 Ticket。 回复还必须经过身份验证,以避免替换攻击。

我们将重用类似于之前描述的用于保护双方之间交换的消息的加密和身份验证方案。

回复格式

伪 JSON 符号

MetaData = jsonutils.dumps({
    'source': <sender>,
    'destination': <receiver>,
    'expiration': <calculated as timestamp sent in the request + TTL>
})

Optionally encrypted buffer containing the Encryption and Signature pair as returned by the HKDF.
Ticket = jsonutils.dumps({
    'skey': <Signing Key from SEK>,
    'ekey': <Encryption Key from SEK>,
    'esek': Esek
})

KeyEx_Reply = {
    'metadata': MetaData, # base64 encoded
    'ticket': Ticket, or ENC(Key.A, Ticket) # base64 encoded
    'signature': Signature # base64 encoded
}

签名是对所有数据计算的

MetaData = serialized JSON Metadata
Ticket = serialized JSON Metadata, encrypted
Signature = HMAC(Key.A, (MetaData || Ticket))

我们再次建议使用 HMAC-SHA-256 作为默认身份验证函数,如 RFC 6234 中所述。

我们将重用与使用 AES-128-CBC 和随机 IV 进行消息加密相同的确切方案

RESTful API

将提供一个自定义的 RESTful API 来访问密钥服务器,将使用 GET 调用来获取 Ticket。

请求

POST /kds/ticket/{Signature}

{
    "request": {
        "metadata": MetaData,
        "signature": Signature
    }
}

回复

200 OK

{
    "reply": {
        "metadata": MetaData,
        "ticket": Ticket,
        "signature": Signature
    }
}

错误代码

  • 200 OK - 此状态码响应于成功的 GET 操作
  • 401 Unauthorized - 当未执行身份验证或身份验证失败时,将返回此状态码。
  • 403 Forbidden - 当 requester 字段与 senderreceiver 字段不匹配时,将返回此状态码。
  • 500 Internal Server Error - 当服务器实现中发生意外错误时,将返回此状态码。
  • 501 Not Implemented - 当实现无法满足请求因为它无法实现指定的整个 API 时,将返回此状态码。
  • 503 Service Unavailable - 当服务器无法与后端服务(数据库、memcache 等)通信时,将返回此状态码。

操作注意事项

密钥服务器查找

通常,发送者只需要对每对对等体进行一次查找,以便能够向接收者发送签名和/或加密的消息。 在消息中返回的到期时间之前,无需对同一接收者发送消息进行其他查找。 但是,当使用组名作为目标时,接收者可能需要执行查找。

组目标

当目标是服务组时,组中的所有接收者都需要能够查找组密钥,以便能够验证和解密消息。 为了避免长期共享组密钥及其管理,组密钥的生命周期很短,并且需要由组的成员按需检索。 通过使用短生命周期密钥,我们可以避免撤销问题。 因为任何组密钥都会在短时间内失效。 禁用受损的组成员足以剥夺其访问的最后一个已发布密钥到期后的任何有效组密钥。

组密钥查找

TODO

扇出消息

使用对称密钥对扇出消息进行签名是存在问题的,但是到目前为止只有 3 种情况使用扇出

  • nova network,但我被告知这种情况将会消失
  • nova compute 到所有调度器,请参见上面的组密钥
  • nova scheduler 到所有 compute,这存在问题,但是消息是一个广播请求,而不是命令,因此我们可以简单地不签名在这种情况下

如果不支持签名,我们可能需要使用一次性密钥方案,但是对密钥服务器的查找会非常多(每个 compute 节点一个)。

Keystone 中的密钥分发服务器

为什么是 Keystone ?

有效地将密钥分配给服务和处理服务组意味着为这些服务分配身份。 Keystone 是 Openstack 中的身份提供程序/网关,因此将密钥分发服务器嵌入 Keystone 似乎是自然的方法。 此特定实现使用 Ticket 来允许服务之间的安全 RPC 通信,这与 Keystone 用于基于 HTTP 的通信的令牌相似。

Keystone 中的新 KDS 服务

密钥分发服务器应在 keystone 的 /kds 下提供。 当前在 API 段落 中定义的唯一 GET 操作可以在 /kds/ticket 处访问

服务器意味着将密钥存储在数据库中,每个目标名称(以 topic.hostname 的形式),可逆加密,并使用在 keystone 启动时从配置文件选项中获取的 master key 保存在文件中。

它还依赖于作为 SecureMessage 工作的一部分构建的 oslo-incubator cryptoutils 库。

实现

实现将分阶段进行,并涉及多个组件

  • oslo-incubator 库
  • nova 和其他服务(开始执行签名)
  • keystone 作为密钥分发服务器

阶段 1

添加基本的加密函数和新的消息信封构建函数。 默认情况下使用新的信封更改代码,签名是可选的

阶段 2

添加支持获取密钥,但如果查找失败则回退到未签名

阶段 3

添加带有仅主机密钥的基本密钥服务器

阶段 4

添加对共享每服务类型密钥的支持。 添加访问控制检查以限制对这些密钥的访问。

阶段 5

根据需要默认打开签名