注意
本文档适用于 Ceph 开发版本。
Cephx 认证协议的详细描述
Peter Reiher
本文档提供了关于 Cephx 授权协议的更详细信息,该协议的高级流程在 Yehuda(12/19/09)的备忘录中进行了描述。由于本备忘录讨论了调用例程的细节和使用的变量,因此它代表了一个快照。本文档创建之后,代码可能会发生变化,并且文档不太可能同步更新。幸运的是,代码注释将指示协议实现方式的主要变化。
简介
协议的基本思想基于 Kerberos。客户端希望从服务器获取某些内容。服务器只会向授权客户端提供请求的服务。而不是要求每个服务器处理身份验证和授权问题,系统使用了一个授权服务器。因此,客户端必须首先与授权服务器通信以进行身份验证并获得凭证,这些凭证将授予其访问所需服务的权限。
授权与身份验证不同。身份验证提供了一些证据,证明某一方是其声称的身份。授权提供了一些证据,证明特定一方被允许做某事。通常,安全的授权意味着安全的身份验证(因为没有身份验证,你可能会为冒名顶替者授权),但反过来则不一定成立。可以进行身份验证而不进行授权。本协议的目的是进行授权。
基本方法是在整个过程中使用对称加密。每个客户端 C 都有自己的密钥,只有它和授权服务器 A 知道。每个服务器 S 都有自己的密钥,只有它和授权服务器 A 知道。授权信息将通过票据传递,并使用提供服务的实体的密钥进行加密。将有一个 A 交给 C 的票据,允许 C 向 A 请求其他票据。这个票据将使用 A 的密钥加密,因为 A 是需要检查它的人。后来将会有 A 签发的票据,允许 C 与 S 通信以请求服务。这些票据将使用 S 的密钥加密,因为 S 需要检查它们。由于我们希望提供通信的安全性,因此也设置了会话密钥。目前,这些会话密钥仅用于本协议期间以及客户端 C 和服务器 S 之间的握手,当客户端提供其服务票据时。它们可以用于整个过程中的身份验证或保密,只需对系统进行一些更改。
如果要实现本协议所需的预期安全效果,则多个参与者需要相互证明某些内容。
1. 客户端 C 必须向身份验证器 A 证明它确实是 C。由于所有操作都是通过消息完成的,客户端还必须证明证明身份的消息是新鲜的,并且没有被攻击者重放。
2. 身份验证器 A 必须向客户端 C 证明它确实是身份验证器。同样,也需要证明没有发生重放。
3. A 和 C 必须安全地共享一个会话密钥,用于在他们之间分发后续的授权材料。同样,不允许重放,并且密钥必须仅 A 和 C 知道。
4. A 必须从 C 获取证据,允许 A 查找 C 对服务器 S 的授权操作。
5. C 必须从 A 获取一个票据,该票据将向 S 证明 C 可以执行其授权的操作。这个票据必须只能由 C 使用。
6. C 必须从 A 获取一个会话密钥,以保护 C 和 S 之间的通信。会话密钥必须是新鲜的,并且不能是重放的结果。
第一阶段:
客户端设置以知道它需要某些内容,使用名为need
的变量,该变量是AuthClientHandler
类的一部分,该类从CephxClientHandler
继承。在这种情况下,编码在need
变量中的CEPH_ENTITY_TYPE_AUTH
指示我们需要从头开始启动身份验证协议。由于我们始终与同一个授权服务器通信,如果我们之前已经通过了这个协议的步骤(并且生成的票据/会话尚未超时),我们可以跳过这个步骤,只需请求客户端票据。但它必须在最初完成,我们将假设我们处于那种状态。
第一阶段中 C 发送给 A 的消息是在CephxClientHandler::build_request()
中构建的(在auth/cephx/CephxClientHandler.cc
中)。此例程用于多个目的。在这种情况下,我们首先调用validate_tickets()
(来自位于CephXTicketManager::validate_tickets()
例程)。此代码遍历可能的票据列表,以确定我们需要的内容,并根据需要在auth/cephx/CephxProtocol.h
中的need
标志中设置值。然后我们调用ticket.get_handler()
。此例程(位于CephxProtocol.h
中)在票据映射中查找指定类型的票据(用于授权的票据),为其创建一个票据处理对象,并将处理程序放入映射中的正确位置。然后我们进入处理单个情况的专用代码。这里的案例是我们仍然需要向 A 进行身份验证(即if (need & CEPH_ENTITY_TYPE_AUTH)
分支)。
现在我们创建一个类型为CEPHX_GET_AUTH_SESSION_KEY
的消息。我们需要使用 C 的密钥对此消息进行身份验证,因此我们从本地密钥存储库中获取该密钥。我们创建一个随机挑战,其目的是防止重放。我们使用cephx_calc_client_server_challenge()
对该挑战进行加密。我们已经从 pre-cephx 阶段获取了一个服务器挑战(一个类似的随机字节集,但由服务器创建并发送给客户端)。我们将这两个挑战和我们的密钥结合起来,生成一个组合加密挑战值,该值放入req.key
.
如果我们有旧票据,我们将其存储在req.old_ticket
中。我们即将获取一个新的。
整个req
结构,包括旧票据和两个挑战的加密哈希,都被放入消息中。然后我们返回此函数,消息被发送。
现在我们切换到身份验证器侧,A。服务器接收发送的消息,类型为CEPH_GET_AUTH_SESSION_KEY
。消息在prep_auth()
中进行处理,mon/AuthMonitor.cc
中,它调用handle_request()
是CephxServiceHandler.cc
来执行大部分工作。此例程也处理多个情况。
控制流由与消息关联的request_type
。Cephadm 还支持使用cephx_header
决定。我们这里的案例是CEPH_GET_AUTH_SESSION_KEY
。我们需要 A 与 C 共享的密钥,因此我们调用从本地密钥存储库中的get_secret()
来获取它。(在代码中它被称为key_server
,但它不是一个单独的机器或处理实体。它更像是一个本地使用密钥的存储位置。)我们应该已经为此客户端设置了服务器挑战,因此我们确保我们确实有一个。 (此变量针对特定的CephxServiceHandler
,因此我们为每个此类结构创建一个不同的变量,推测每个 A 处理的客户端都有一个。)如果没有挑战,我们将需要重新开始,因为我们需要检查客户端的加密哈希,这取决于服务器挑战的一部分。
现在我们调用客户端用来计算哈希的相同例程,基于相同的值:客户端挑战(它在传入的消息中)、服务器挑战(我们保存的)和客户端的密钥(我们刚刚获得的)。我们检查客户端是否发送了我们预期的内容。如果是,我们知道我们正在与正确的客户端通信。我们知道会话是新鲜的,因为它使用我们发送给它的挑战来计算其加密哈希。因此,我们可以给它一个身份验证票据。
我们获取 C 的eauth
结构。它包含一个 ID、一个密钥和一组权限(capabilities)。
客户端在消息中发送了它的旧票据(如果它有的话)。如果是这样,我们将标志should_enc_ticket
设置为 true,并将全局 ID 设置为该旧票据中的全局 ID。如果对其旧票据的解码尝试失败(最可能的原因是它没有旧票据),should_enc_ticket
保持为 false。现在我们设置新票据,填写时间戳、C 的名称以及方法调用中提供的全局 ID(除非有旧票据)。我们需要一个新的会话密钥来帮助客户端安全地与我们通信,而不是使用其永久密钥。我们将服务 ID 设置为CEPH_ENTITY_TYPE_AUTH
,这将告诉客户端 C 如何处理我们发送给它的消息。cephx_build_service_ticket_reply()
.
cephx_build_service_ticket_reply()
is inauth/cephx/CephxProtocol.cc
。此例程将构建响应消息。其中大部分数据从其参数复制到消息结构中。部分信息(会话密钥和有效期)使用 C 的永久密钥进行加密。如果should_encrypt_ticket
标志被设置,则使用旧票据的密钥进行加密。否则,没有旧票据密钥,因此新票据没有被加密。(当然,它已经使用 A 的永久密钥加密了。)推测第二次加密的目的是减少使用永久密钥加密的内容。
然后我们调用密钥服务器的get_service_caps()
例程,使用实体名称、一个标志CEPH_ENTITY_TYPE_MON
和权限,此例程将填充权限变量。使用该常量标志意味着我们将获取客户端的权限,而不是其他数据服务器。这里的票据是访问授权器 A,而不是服务 S。此调用的结果是将权限变量(例程我们所在的参数)填充为将允许 C 访问 A 授权服务的监控器权限。密钥服务器
handle_request()
itself does not send the response message. It builds up the
result_bl
,基本上包含该消息的内容和权限结构,但它不发送消息。我们回到prep_auth()
,在mon/AuthMonitor.cc
,以发送该消息。此例程对刚刚填充的权限结构进行一些小操作。经过这个小操作后产生的全局 ID 被放入响应消息中。响应消息在这里构建(主要来自response_bl
缓冲区)并发送。
这完成了协议的第一阶段。此时,C 已经向 A 进行了身份验证,A 已经生成了一个新的会话密钥和票据,允许 C 从 A 获取服务器票据。
第二阶段
当 C 接收到来自 A 的包含新票据和会话密钥的消息时,此阶段开始。此阶段的目标是为 C 提供一个会话密钥和票据,允许它与其他 S 通信。
A 发送给 C 的消息被分配到build_request()
in CephxClientHandler.cc
,该例程与第一阶段早期构建协议第一条消息时使用的例程相同。这次调用validate_tickets()
时,need
变量将不包含CEPH_ENTITY_TYPE_AUTH
,因此将使用例程的大部分不同分支。这是由if (need)
指示的分支。我们有一个用于授权器的票据,但我们仍然需要服务票据。
我们必须向 A 发送另一个消息以获取服务器 S 的票据(和会话密钥)。我们将消息的request_type
设置为CEPHX_GET_PRINCIPAL_SESSION_KEY
并调用ticket_handler.build_authorizer()
来获取授权器。此例程位于CephxProtocol.cc
。我们将此授权器的密钥设置为我们从 A 获取的会话密钥,并创建一个新的 nonce。我们将全局 ID、服务 ID 和票据放入授权器的一部分消息缓冲区中。然后我们创建一个新的CephXAuthorize
结构。我们刚刚创建的 nonce 放在那里。我们使用当前的会话密钥对此结构进行加密,并将其放入授权器的缓冲区中。然后我们返回授权器。CephXAuthorize
structure with the current session key and stuff it into the authorizer’s buffer. We
return the authorizer.
回到build_request()
,我们取刚刚构建的授权器的一部分(其缓冲区,而不是会话密钥或任何其他内容),并将其放入我们正在创建的将发送给 A 的消息缓冲区中。然后我们删除授权器。我们将我们想要的内容的要求放入req.keys
,并将req
放入缓冲区。然后我们返回,消息被发送。
授权器 A 接收此消息,其类型为CEPHX_GET_PRINCIPAL_SESSION_KEY
。消息在prep_auth()
,在mon/AuthMonitor.cc
中,它再次调用handle_request()
in CephxServiceHandler.cc
来执行大部分工作。
在这种情况下,handle_request()
将采取CEPHX_GET_PRINCIPAL_SESSION_KEY
案例。它将调用cephx_verify_authorizer()
in CephxProtocol.cc
。在这里,我们将从输入缓冲区中获取一些数据,包括全局 ID 和服务 ID 以及 A 的票据。票据包含一个secret_id
,指示使用哪个密钥对其进行加密。从票据中提取的密钥如果是 -1,则票据未指定 A 应该使用的密钥。在这种情况下,A 应该使用 C 想要联系的特定实体的密钥,而不是所有相同类型的服务器实体共享的旋转密钥。要获取该密钥,A 必须咨询密钥存储库以找到正确的密钥。否则,已经从密钥存储库中获取了一个结构来保存必要的密钥。服务器密钥基于时间过期进行旋转(密钥旋转不在本文档中涵盖),因此遍历该结构以找到其当前密钥。无论如何,A 现在知道用于创建此票据的密钥。现在使用此密钥解密票据的加密部分。它应该是一个 A 的票据。
票据还包含 C 应该使用来加密此消息其他部分的会话密钥。使用该会话密钥解密消息的其余部分。
创建一个CephXAuthorizeReply
to hold our reply. Extract the nonce (which was in the stuffcephx_verify_authorizer()
and returnhandle_request()
. 这将用于向 C 证明 A(而不是攻击者)创建了此响应。
验证消息有效且来自 C 后,现在我们需要为其构建一个 S 票据。我们需要知道它想要与其他 S 通信,以及它想要哪些服务。从其消息中提取描述这些内容的票据请求。现在遍历票据请求以查看它想要什么。(它可能正在同一请求中请求多个不同的服务,但在此讨论中我们将假设它只请求一个。)一旦我们知道它想要哪个服务 ID,就调用build_session_auth_info()
.
build_session_auth_info()
is inCephxKeyServer.cc
。它检查 S 的service_ID
的密钥是否可用,并将其放入参数的一个子字段中,并调用位于同一文件中的同样命名的_build_session_auth_info()
。此例程用新的auth_info
结构填充 ID、票据和一些时间戳。它生成一个新的会话密钥并将其放入结构中。然后它调用get_caps()
来填充info.ticket
caps 字段。get_caps()
也在CephxKeyServer.cc
中。它用 S 允许 C 的权限填充它提供的caps_info
结构。
一旦build_session_auth_info()
返回,A 有一个列表,列出了 C 允许 S 的权限。我们将基于此上下文的当前 TTL 设置有效期,并将其放入我们正在准备以响应消息的info_vec
结构中。
现在调用build_cephx_response_header()
,也位于CephxServiceHandler.cc
。填充request_type
,它是CEPHX_GET_PRINCIPAL_SESSION_KEY
,状态为 0,以及结果缓冲区。
现在调用cephx_build_service_ticket_reply()
时检查 nonce,该例程位于CephxProtocol.cc
。与 A 在第一阶段处理其响应的末尾使用的相同例程一样。在这里,会话密钥(现在是与 S 而不是 A 通信的会话密钥)和该密钥的有效期将被使用现有的 C 和 A 之间共享的会话密钥进行加密。参数should_encrypt_ticket
在这里为 false,并且没有提供用于该加密的密钥。相关的票据,一旦 C 将其发送到那里,就已经使用 S 的密钥加密了。因此,本质上,此例程将 ID 信息、加密的会话密钥和允许 C 与 S 通信的票据放入要发送到 C 的缓冲区中。
此例程返回后,我们从handle_request()
退出,回到prep_auth()
并最终回到底层的消息发送代码。
客户端接收此消息。在消息通过Pipe::connect()
时检查 nonce,该例程位于msg/SimpleMessager.cc
。在while(1)
中此例程中间的一个长循环中,它获取一个授权器。如果获取成功,最终它将调用verify_reply()
,该例程检查 nonce。connect()
never explicitly
消息最终会通过到handle_response()
,在CephxClientHandler.cc
。在此例程中,我们调用get_handler()
来获取一个票据处理程序来持有我们刚刚收到的票据。此例程嵌入在CephXTicketManager
结构的定义中。它接受一个类型(CEPH_ENTITY_TYPE_AUTH
,在这种情况下)并遍历tickets_map
以查找该类型。应该有一个,并且它应该在其条目中包含 C 和 A 之间会话的会话密钥。此密钥将用于解密 A 提供的信息,特别是允许 C 与 S 通信的新会话密钥。
然后我们调用verify_service_ticket_reply()
,在CephxProtocol.cc
。此例程需要确定票据是否有效,并获取与此票据关联的会话密钥。它使用与 A 共享的会话密钥解密消息缓冲区的加密部分。此票据没有被加密(嗯,没有两次 - 票据总是被加密,但有时双重加密,这个不是)。因此,它可以直接存储在服务票据缓冲区中。我们现在从该缓冲区中获取票据。
我们使用与 C 和 A 共享的会话密钥解密的内容包括新的会话密钥。那是我们当前此票据的会话密钥,因此设置它。检查有效性和设置过期时间。现在如果我们到达这里,返回 true。
回到handle_response()
,我们现在调用validate_tickets()
来调整我们认为需要的内容,因为我们现在有一个我们没有的票据。如果我们已经处理了所有需要的内容,我们将返回 0。
这结束了协议的第二阶段。我们现在已经成功为客户端 C 设置了一个与服务器 S 通信的票据和会话密钥。S 将知道 C 是其声称的身份,因为 A 将对其进行验证。C 将知道它正在与 S 通信,同样是因为 A 对其进行了验证。C 和 S 通信的会话密钥的副本仅分别在 C 和 S 的永久密钥下加密发送,因此没有其他方(除了所有方都信任的 A)知道该会话密钥。票据将安全地指示 S C 允许执行的操作,并由 A 作证。在 A 和 C 之间来回传递的 nonce 确保它们没有受到重放攻击。C 尚未实际与 S 通信,但它已经准备好。如果永久密钥之一被泄露,这里的大部分安全性都会失效。C 的密钥泄露意味着攻击者可以冒充 C 并获取 C 的所有权限,并且可以窃听 C 的合法对话。他也可以冒充 A,但仅在 C 的对话中。由于它(根据假设)没有任何服务的密钥,因此它无法为服务生成任何新的票据,但它可以重放旧的票据和会话密钥,直到 S 的永久密钥更改或旧票据超时。
如果永久密钥之一被泄露,这里的大部分安全性都会失效。S 的密钥泄露意味着攻击者可以冒充 S 与任何人通信,并且可以窃听任何用户与 S 的对话。除非某些客户端的密钥也被泄露,否则攻击者无法为 S 生成新的假客户端票据,因为这样做需要它使用它不知道的客户端密钥将自己身份验证为 A。
Compromise of S’s key means that the attacker can pose as S to anyone, and can eavesdrop on any user’s conversation with S. Unless some client’s key is also compromised, the attacker cannot generate new fake client tickets for S, since doing so requires it to authenticate himself as A, using the client key it doesn’t know.
由 Ceph 基金会带给您
Ceph 文档是一个社区资源,由非盈利的 Ceph 基金会资助和托管Ceph Foundation. 如果您想支持这一点和我们的其他工作,请考虑加入现在加入.