Files
nodeagent/crates/nodeagent-client/src/crypto.rs
T
jkooe 8ab8f8ae80 feat(crates): 入库 Rust 控制端客户端 nodeagent-client
复刻 @nodeagent/client 的协议行为,供桌面控制台(Tauri 壳)经 Cargo path 依赖:
- 模块:error / types / ulid / crypto / tls / discovery / client
- 握手:hello → challenge → auth → auth_ok;psk(HMAC-SHA256) 与 ed25519 两种模式
- 证书指纹 sha256(DER) 钉住(E_CERT_MISMATCH);ULID(Crockford Base32)
- 断线指数退避重连 1s→30s(±15% 抖动)

Rust async Send 三层陷阱(本轮主要成本,解法已写入 client.rs 注释):
1. futures_util::lock::Mutex 无手写 Sync 实现 → 含它的类型整体 !Sync
2. tokio::sync::MutexGuard 非 Send
3. establish → spawn(read_loop) → on_disconnected → establish 构成**类型级递归**,
   编译器无法证明 Send。最终采用**监督者模式**:单个 supervise 任务串行
   「建立+握手 → 等读循环 Notify 断开 → 退避重连」,读循环只 notify 不回调
   establish,从而打破递归;发送半用专用写任务 + mpsc 解耦(同步 send,不跨 await 持锁)。

测试:9 单测(crypto/ulid/Send+Sync 断言)+ 2 E2E(psk 握手→invoke 全链路、连不上端口
返回 NODE_OFFLINE),`cargo clippy --all-targets -- -D warnings` 零告警。
另附 examples/connect.rs(连真实被控端)与 examples/probe.rs(字段契约探针,
输出顶层键+数组首元素键,并按 system.info.os 区分「平台不支持」与「真失败」)。

.gitignore:crates/ 转正后必须显式忽略 /crates/target/ —— 此前是靠整个 /crates/
被忽略才没让 4GB 级构建产物进库,取消那条保护后会裸奔。
2026-10-07 10:13:41 +08:00

132 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 密码学工具:HMAC / ed25519 签名 / 证书指纹 / nonce。
//! 逐一对齐 `packages/protocol/src/{hmac,ed25519}.ts` 与 `net.ts`。
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use hmac::{Hmac, KeyInit, Mac};
use sha2::{Digest, Sha256};
type HmacSha256 = Hmac<Sha256>;
/// 计算 HMAC-SHA256,返回 Base64(对齐 hmac.ts 的 `computeHmac`)。
/// 密钥为预共享密钥字符串,nonce 以 UTF-8 编码。
pub fn compute_hmac(key: &str, nonce: &str) -> String {
// HMAC 接受任意长度 key,此处不可能失败,unwrap 仅为类型收敛。
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC 接受任意长度 key");
mac.update(nonce.as_bytes());
STANDARD.encode(mac.finalize().into_bytes())
}
/// 生成随机 nonce(32 字节,Base64),对齐 `generateNonce`。
pub fn generate_nonce() -> String {
let mut bytes = [0u8; 32];
for b in bytes.iter_mut() {
*b = rand::random::<u8>();
}
STANDARD.encode(bytes)
}
/// 证书指纹:sha256(DER) 的十六进制小写 64 位(对齐 net.ts 的 `certFingerprint`)。
pub fn cert_fingerprint(der: &[u8]) -> String {
let digest = Sha256::digest(der);
digest.iter().map(|b| format!("{b:02x}")).collect()
}
/// 对 nonce 原文做 ed25519 签名,返回 Base64(对齐 ed25519.ts 的 `signNonce`)。
/// 私钥为 Base64(PKCS8 DER)。
pub fn sign_nonce(private_key_b64: &str, nonce: &str) -> Result<String, String> {
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::Signer;
let der = STANDARD
.decode(private_key_b64)
.map_err(|e| format!("私钥 Base64 解码失败: {e}"))?;
let signing_key = ed25519_dalek::SigningKey::from_pkcs8_der(&der)
.map_err(|e| format!("PKCS8 私钥解析失败: {e}"))?;
let sig = signing_key.sign(nonce.as_bytes());
Ok(STANDARD.encode(sig.to_bytes()))
}
/// 验签(对齐 `verifyNonce`);任何异常均视为失败(不抛出)。
pub fn verify_nonce(public_key_b64: &str, nonce: &str, signature_b64: &str) -> bool {
use ed25519_dalek::pkcs8::DecodePublicKey;
use ed25519_dalek::Verifier;
let Ok(pk_der) = STANDARD.decode(public_key_b64) else {
return false;
};
let Ok(vk) = ed25519_dalek::VerifyingKey::from_public_key_der(&pk_der) else {
return false;
};
let Ok(sig_bytes) = STANDARD.decode(signature_b64) else {
return false;
};
let Ok(sig) = ed25519_dalek::Signature::from_slice(&sig_bytes) else {
return false;
};
vk.verify(nonce.as_bytes(), &sig).is_ok()
}
/// 公钥指纹(SHA-256 前 16 位 hex),对齐 `keyId`。
pub fn key_id(public_key_b64: &str) -> String {
let der = STANDARD.decode(public_key_b64).unwrap_or_default();
let digest = Sha256::digest(&der);
// 前 16 个 hex 字符 = 前 8 字节。
digest.iter().take(8).map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use base64::engine::general_purpose::STANDARD;
use ed25519_dalek::pkcs8::{EncodePrivateKey, EncodePublicKey};
fn gen_keypair() -> (String, String) {
let sk = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let pk = sk.verifying_key();
(
STANDARD.encode(pk.to_public_key_der().expect("编码公钥").as_bytes()),
STANDARD.encode(sk.to_pkcs8_der().expect("编码私钥").as_bytes()),
)
}
#[test]
fn hmac_matches_known_vector() {
// 与 Node 版对拍的固定向量:HMAC-SHA256("key", "nonce")。
let out = compute_hmac("secret", "hello");
// 用独立的 hmac 重算并比对,避免自证。
let mut mac = HmacSha256::new_from_slice(b"secret").unwrap();
mac.update(b"hello");
let expect = STANDARD.encode(mac.finalize().into_bytes());
assert_eq!(out, expect);
// Base64(HMAC-SHA256("secret","hello")) 的已知值(由 RFC 测试向量推得,供跨端核对)。
assert_eq!(out, "iKqz7ejTrflNJquQ07r9SiCDBww7zOnAFO4EpEOEfAs=");
}
#[test]
fn sign_and_verify_roundtrip() {
let (pk, sk) = gen_keypair();
let nonce = generate_nonce();
let sig = sign_nonce(&sk, &nonce).unwrap();
assert!(verify_nonce(&pk, &nonce, &sig));
assert!(!verify_nonce(&pk, "tampered", &sig));
}
#[test]
fn key_id_is_16_hex() {
let (pk, _sk) = gen_keypair();
let id = key_id(&pk);
assert_eq!(id.len(), 16);
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn cert_fingerprint_sha256_der() {
// 空 DER 的 sha256 是已知常量。
assert_eq!(
cert_fingerprint(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
}