尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Apache Spark 前向安全认证协议 v2.0 深度解析:X25519 + HKDF + AES-GCM 的密钥协商与实现

Apache Spark 前向安全认证协议 v2.0 深度解析:X25519 + HKDF + AES-GCM 的密钥协商与实现 Apache Spark 前向安全认证协议 v2.0 深度解析X25519 HKDF AES-GCM 的密钥协商与实现【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/spark导读本文完整解析 Apache Spark 网络层使用的Forward Secure Auth Protocol v2.0前向安全认证协议该协议是 Spark 内置认证与加密体系spark.network.crypto.*的核心它基于X25519Curve25519 上的临时 Diffie-Hellman 密钥交换、HKDF密钥派生与AES-GCM认证加密在预共享密钥PSK的基础上完成双方身份认证、会话密钥协商与传输加密。读完本文你将掌握该协议的握手流程、消息格式、安全属性、v1.0/v2.0 兼容性差异以及其在 Spark 源码中的完整实现路径与对应配置项。协议设计文档位于 common/network-common/src/main/java/org/apache/spark/network/crypto/README.md所有实现均可在该目录下的 Java 源码与TransportConf配置解析中逐一验证。协议概述为什么需要前向安全的认证协议Spark 的节点间通信Driver-Executor、Shuffle Service、BlockManager 等在启用加密后需要在建立连接时完成两件事认证确认对方持有合法凭证与密钥协商生成双方一致的会话加密密钥。旧方案依赖 SASL而 v2.0 协议采用了业界成熟的临时密钥交换思路双方共享一个预共享密钥pre-shared secret它可能只是一个低熵low-entropy口令因此不能直接用作会话加密密钥协议用预共享密钥通过HKDF派生出一个密钥加密密钥key-encrypting keyKEK并用它来AES-GCM 加密一个 X25519 临时公钥加密公钥的目的不是为了保密公钥本身无需保密而是借助 AES-GCM 的认证标签与 AADAssociated Authenticated Data关联认证数据证明消息确实来自持有预共享密钥的一方双方随后用 X25519 计算 ECDHE 共享秘密再经 HKDF 派生出会话密钥与两条数据流的初始化向量IV。由于每次连接都会重新生成 X25519 临时密钥对即使预共享密钥在未来被泄露攻击者也无法还原过去会话的密钥——这正是前向安全forward secure的含义。协议设计上严格遵循Noise 协议框架中的NNpsk0模式即双方均无静态密钥、仅客户端单方携带预共享密钥前缀的模式曲线选择为 X25519。从源码结构看整个协议由 AuthEngine.java 单类承载其 Javadoc 明确写着This supports a forward-secure authentication protocol based on X25519 Diffie-Hellman Key Exchange, using a pre-shared key to derive an AES-GCM key encrypting key.第一步Client Challenge客户端质询协议由客户端发起。给定应用标识appId后客户端首先做三件事生成一个16 字节的随机 saltnonSecretSalt明文传输、无需保密以预共享密钥为输入密钥IKM、随机 salt 为盐salt、Concat(appId, nonSecretSalt)为上下文信息info通过HKDF派生 16 字节的密钥加密密钥生成 X25519 临时密钥对用上述 KEK 以AES-GCM加密临时公钥并将aadState作为 GCM 的 AAD 参与认证计算。协议原文中的伪代码preSharedKey lookupKey(appId) nonSecretSalt Random(16 bytes) aadState Concat(appId, nonSecretSalt) keyEncryptingKey HKDF(preSharedKey, nonSecretSalt, aadState) clientKeyPair X25519.generate() randomIV Random(16 bytes) ciphertext AES-GCM-Encrypt( key keyEncryptingKey, iv randomIV, plaintext clientKeyPair.publicKey(), aad aadState) clientChallenge (appId, nonSecretSalt, randomIV, ciphertext)注意appId与随机 salt双重绑定到密文既参与了 HKDF 派生又被纳入 AES-GCM 的 AAD。这样任何一方篡改appId或 salt解密方的 GCM 认证都会失败。文档特别指出协议并不依赖对客户端公钥的保密——完全可以用一个 MAC 替代 AES-GCM 加密加密只是顺带完成的认证手段。实现对应AuthEngine.challenge()在源码 AuthEngine.java 中客户端逻辑由challenge()方法实现AuthMessage challenge() throws GeneralSecurityException { setClientPrivateKey(X25519.generatePrivateKey()); return encryptEphemeralPublicKey( X25519.publicFromPrivate(clientPrivateKey), EMPTY_TRANSCRIPT); }其中encryptEphemeralPublicKey同文件 L92-L112真实完成了随机 salt → HKDF 派生 KEK →AesGcmJce来自 Google Tink加密公钥的完整流程加密时以Bytes.concat(appId, nonSecretSalt, transcript)作为 AAD。首次质询的 transcript 为空EMPTY_TRANSCRIPT即客户端是协议的第一条消息。第二步Server Response And Challenge服务端响应与质询服务端收到客户端质询后执行对称操作恢复客户端公钥assert(appId clientChallenge.appId) preSharedKey lookupKey(appId) aadState Concat(appId, clientChallenge.nonSecretSalt) keyEncryptingKey HKDF(preSharedKey, nonSecretSalt, aadState) clientPublicKey AES-GCM-Decrypt( key keyEncryptingKey, iv clientChallenge.randomIV, ciphertext clientChallenge.ciphertext, aad aadState)解密成功的唯一前提是服务端持有了与客户端一致的预共享密钥并且appId、salt、密文均未被篡改——这就完成了对客户端的认证。随后服务端做三件事校验appId一致生成自己的 X25519 临时密钥对把服务端公钥用以当前协议 transcriptConcat(appId, nonSecretSalt, clientChallenge)为 context 派生的新 KEK加密后回给客户端这里的 transcript 包含了刚刚收到的完整客户端质询消息用自己的临时私钥与客户端公钥计算 X25519 共享秘密。preSharedKey lookupKey(appId) nonSecretSalt Random(16 bytes) aadState Concat(appId, nonSecretSalt, clientChallenge) keyEncryptingKey HKDF(preSharedKey, nonSecretSalt, aadState) randomIV Random(16 bytes) serverKeyPair X25519.generate() ciphertext AES-GCM-Encrypt( key keyEncryptingKey, iv randomIV, plaintext serverKeyPair.publicKey(), aad aadState) serverResponse (appId, nonSecretSalt, randomIV, ciphertext)实现对应AuthEngine.response()服务端逻辑在 AuthEngine.response()先decryptEphemeralPublicKey(encryptedClientPublicKey, EMPTY_TRANSCRIPT)恢复客户端公钥再X25519.generatePrivateKey()生成服务端临时私钥encryptEphemeralPublicKey(..., getTranscript(encryptedClientPublicKey))把服务端公钥加密最后调用X25519.computeSharedSecret计算共享秘密。其中getTranscript(AuthMessage...)同文件 L246-L251将参与消息按序编码拼成字节数组作为 HKDF 的 transcript 输入——每一条新消息都会把之前所有消息纳入密钥派生从而把每一轮握手绑定到前面所有轮次。第三步会话密钥与双向 IV 的派生一旦共享秘密计算完成双方就需要用它派生出会话加密密钥和两条数据流的 IV。协议原文sharedSecret X25519.computeSharedSecret(clientPublicKey, serverKeyPair.privateKey()) derivedKey HKDF(sharedSecret, salttranscript, infoderiveKey) clientIv HKDF(sharedSecret, salttranscript, infoclientIv) serverIv HKDF(sharedSecret, salttranscript, infoserverIv)IV 本身不保密但为了双方确定性推导它们与协议 transcript 绑定。客户端收到服务端响应后解密出服务端临时公钥即可重构出完全相同的共享秘密与两条 IV。实现对应generateTransportCipher()与三条 info 常量源码 AuthEngine.generateTransportCipher() 是会话密钥派生的落脚点它与类顶部三条常量一一对应public static final byte[] DERIVED_KEY_INFO derivedKey.getBytes(UTF_8); public static final byte[] INPUT_IV_INFO inputIv.getBytes(UTF_8); public static final byte[] OUTPUT_IV_INFO outputIv.getBytes(UTF_8);derivedKey把 ECDHE 共享秘密经 HKDF 处理成 16 字节 AES 会话密钥AES_GCM_KEY_SIZE_BYTES 16inputIv/outputIv分别派生出两条流各自的 IV配合isClient标志区分客户端、服务端各自使用的 IV 顺序。随后代码根据配置选择会话加密算法AES/CTR/NoPadding默认值对应LEGACY_CIPHER_ALGORITHM→ 构造 CtrTransportCipher.javaAES/GCM/NoPadding对应CIPHER_ALGORITHM→ 构造 GcmTransportCipher.java后者使用 Tink 的AesGcmHkdfStreaming流式加解密并处理 TCP 分片、多消息合并等网络层细节其他值直接抛出IllegalArgumentException。线缆上的消息格式AuthMessage每一次质询/响应最终都是一个 AuthMessage.java 记录Java recordrecord AuthMessage(String appId, byte[] salt, byte[] ciphertext) implements Encodable其二进制编码为1 字节 TAG_BYTE(0xFB) 长度前缀的appId 长度前缀的salt 长度前缀的ciphertext见同文件 encodedLength()/encode()。解码时首先校验 TAG 字节若不符则抛出IllegalArgumentException(Expected ClientChallenge, received something else.)——这一机制用于防止把无关消息误当质询解析。注意协议原文中的randomIV在AuthMessage中并未单独编码——因为 GCM 的 nonce/IV 已由 Tink 的AesGcmJce随密文一并输出标准做法是将 IV 前置在密文之前AuthMessage只保留appId salt ciphertext三要素。安全属性分析前向安全与妥协后果协议文档的 Security Comments 一节给出了明确的安全定位协议归属本质上是 Noise 框架中的NNpsk0模式底层是 X25519 上的 ECDHE。前向保密如果预共享密钥被泄露攻击者无法恢复过去任何会话的密钥因为过去的会话使用了当时独立的临时密钥对与 PSK 泄露无关。妥协代价PSK 泄露后攻击者可以冒充未来会话因为他现在能构造出合法的质询/响应消息。被动监听安全即便 PSK 泄露被动窃听者看到的仍是密文只有主动发起欺骗性会话的攻击者才能拿到明文。v1.0 与 v2.0一次针对共享秘密的加固协议演进的关键差异文档Security Changes Compatibility一节如下v1.0旧版当前默认没有对sharedSecret做最后的 HKDF 派生直接使用 X25519 输出的编码后 X 坐标作为密钥材料。文档明确指出这是 atypical非典型做法标准实践应当先把共享坐标送入 HKDF。v2.0最新增加了derivedKey HKDF(sharedSecret, salttranscript, infoderiveKey)这一步。兼容性后果是决定性的使用 v1.0 与 v2.0 的两个 Spark 实例将协商出不同的会话密钥因此无法跨版本发送加密 RPC。为了保证向后兼容例如滚动升级期间新旧节点共存v1.0 仍被保留为默认协议版本。配置开关与源码证据AuthEngine构造函数AuthEngine.java#L65-L73读取版本号并设置回退标志// This is for backward compatibility with version 1.0 of this protocol, // which did not perform a final HKDF round. this.unsafeSkipFinalHkdf conf.authEngineVersion() UNSAFE_SKIP_HKDF_VERSION;随后在generateTransportCipher()中当unsafeSkipFinalHkdf为真时derivedKey直接取sharedSecret跳过 HKDF否则才做 HKDF。常量UNSAFE_SKIP_HKDF_VERSION 1的命名也直白地指出了 v1.0 的不安全性质。配置解析位于 TransportConf.java配置项默认值含义spark.network.crypto.enabledfalse是否启用强加密同时启用本认证协议用于协商密钥spark.network.crypto.authEngineVersion1协议版本合法值为 1 或 2默认 1 以保持向后兼容v2 具备更强的安全属性spark.network.crypto.cipherAES/CTR/NoPadding会话数据加密算法可选AES/CTR/NoPadding或AES/GCM/NoPaddingspark.network.crypto.saslFallbacktrue新认证协议失败时是否回退到 SASL默认开启兼容不支持新协议的外部 Shuffle Service协议在 Spark 网络栈中的落地Bootstrap 与 RPC Handler协议通过两组类接入 Spark 的 Netty 传输层客户端侧AuthClientBootstrap.java 实现TransportClientBootstrap。其doBootstrap()在连接建立后调用doSparkAuth()构造AuthEngine→ 发送challenge()→ 用client.sendRpcSync(challengeData.nioBuffer(), conf.authRTTimeoutMs())同步等待响应 →deriveSessionCipher(challenge, response)派生会话密钥 →engine.sessionCipher().addToChannel(channel)把加解密 Handler 注入 Netty Pipeline。若配置saslFallbacktrue且失败原因不是超时则回退到 SASL 认证doSaslAuth。服务端侧AuthServerBootstrap.java 实现TransportServerBootstrap客户端连入时若encryptionEnabled()为真则包装出 AuthRpcHandler.java。AuthRpcHandler.doAuthChallenge()负责解码AuthMessage→ 按appId从SecretKeyHolder取预共享密钥查不到即拒绝连接→engine.response(challenge)完成服务端响应与共享秘密计算 → 把sessionCipher().addToChannel(channel)注入通道 → 标记客户端 ID。任何一步失败都会立即channel.close()并回调失败。会话密码学原语X25519、HKDF、AES-GCM全部由Google Tink的subtle包提供com.google.crypto.tink.subtle.X25519、Hkdf、AesGcmJce见 AuthEngine.java。此外AuthRpcHandler保留了 SASL 回退路径当质询消息无法按新协议解析、且conf.saslFallback()为真时会动态创建SaslRpcHandler并转交从而兼容不支持新协议的老客户端 / 外部 Shuffle Service。总结与建议Forward Secure Auth Protocol v2.0 是 Spark 在共享低熵预共享密钥这一现实约束下将 X25519 ECDHE、HKDF 与 AES-GCM 组合成一个紧凑两轮握手的工程实践临时密钥保证前向安全AES-GCM 认证保证消息来源可信transcript 绑定保证每一步都与历史消息不可分割。其完整设计文档、可对照的AuthEngine实现、AuthMessage编解码、Bootstrap/RPC Handler 接入点以及TransportConf中的配置解析均可在此仓库的common/network-common/src/main/java/org/apache/spark/network/crypto/目录下直接研读。对于需要部署的生产集群建议在确认所有节点均能同步升级后将spark.network.crypto.authEngineVersion显式设为2以获得更强的密钥派生安全性升级过程中需特别注意 v1.0 与 v2.0 之间无法互相协商加密 RPC滚动升级应规划好版本过渡窗口。【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/spark创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表