use hmac::{Hmac, KeyInit, Mac}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use tonic::Status; use crate::error::InternalError; use crate::nonce::NonceStore; pub const AUDIENCE: &str = "event"; const CLAIMS_VERSION: u8 = 1; type HmacSha256 = Hmac; /// 信封内层明文:用户标识 + 权威复核需要的最小字段。 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Claims { pub v: u8, pub aud: String, pub uid: i64, #[serde(default)] pub sv: i64, #[serde(default)] pub sid: String, pub iat: i64, pub exp: i64, pub nonce: String, pub rid: String, } /// 解密 + 验签 + 去重之后的身份;后续权限校验只使用这里的 user_id/security_version。 #[derive(Debug, Clone)] pub struct VerifiedIdentity { pub user_id: i64, pub security_version: i64, pub session_id: String, pub request_id: String, pub key_id: String, } impl VerifiedIdentity { pub fn user_id(&self) -> i64 { self.user_id } } fn now_ms() -> i64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis() as i64) .unwrap_or_default() } /// 规范化签名输入:长度前缀避免字段拼接歧义。 fn mac_input( key_id: &str, ciphertext: &str, mac_key_id: &str, issued_at: i64, expires_at: i64, nonce: &str, audience: &str, ) -> Vec { let mut input = Vec::new(); for part in [ key_id.as_bytes(), ciphertext.as_bytes(), mac_key_id.as_bytes(), &issued_at.to_be_bytes(), &expires_at.to_be_bytes(), nonce.as_bytes(), audience.as_bytes(), ] { input.extend_from_slice(&(part.len() as u32).to_be_bytes()); input.extend_from_slice(part); } input } fn compute_mac(key: &[u8], input: &[u8]) -> Result, InternalError> { let mut mac = HmacSha256::new_from_slice(key) .map_err(|error| InternalError::Config(format!("invalid mac key: {error}")))?; mac.update(input); Ok(mac.finalize().into_bytes().to_vec()) } fn verify_mac(key: &[u8], input: &[u8], presented: &[u8]) -> Result<(), InternalError> { let expected = compute_mac(key, input)?; if crypt::constant_time_eq(&expected, presented) { Ok(()) } else { Err(InternalError::Envelope("mac mismatch".to_string())) } } /// 平台侧:用 event 公钥封装用户标识,并用共享密钥做完整性签名。 pub struct EnvelopeSealer { encryptor: crypt::RsaEncryptor, key_id: String, mac_key_id: String, mac_key: Vec, ttl_secs: u64, clock_skew_secs: i64, audience: String, } impl EnvelopeSealer { pub fn new( key_id: &str, public_pem: &str, mac_key_id: &str, mac_key: &[u8], ttl_secs: u64, clock_skew_secs: u64, ) -> Result { let encryptor = crypt::RsaEncryptor::from_public_pem(key_id, public_pem) .map_err(|error| InternalError::Config(format!("invalid event public key: {error}")))?; Ok(Self { encryptor, key_id: key_id.to_owned(), mac_key_id: mac_key_id.to_owned(), mac_key: mac_key.to_vec(), ttl_secs: ttl_secs.max(1), clock_skew_secs: clock_skew_secs as i64, audience: AUDIENCE.to_string(), }) } pub fn seal( &self, user_id: i64, security_version: i64, session_id: &str, request_id: &str, ) -> Result { let issued_at = now_ms(); let expires_at = issued_at + (self.ttl_secs as i64) * 1000; let claims = Claims { v: CLAIMS_VERSION, aud: self.audience.clone(), uid: user_id, sv: security_version, sid: session_id.to_owned(), iat: issued_at, exp: expires_at, nonce: ::envelope::new_request_id(), rid: request_id.to_owned(), }; let plaintext = serde_json::to_vec(&claims) .map_err(|error| InternalError::Envelope(error.to_string()))?; let ciphertext = crypt::encrypt(&self.encryptor, &plaintext) .map_err(|error| InternalError::Crypt(error.to_string()))?; let mac = compute_mac( &self.mac_key, &mac_input( &self.key_id, &ciphertext, &self.mac_key_id, issued_at, expires_at, &claims.nonce, &self.audience, ), )?; Ok(proto::internal::v1::IdentityEnvelope { key_id: self.key_id.clone(), ciphertext, mac, mac_key_id: self.mac_key_id.clone(), issued_at_unix_ms: issued_at, expires_at_unix_ms: expires_at, nonce: claims.nonce, audience: self.audience.clone(), request_id: request_id.to_owned(), }) } pub fn clock_skew_secs(&self) -> i64 { self.clock_skew_secs } } /// event 侧:MAC 校验 -> 时间窗 -> nonce 去重 -> 私钥解密 -> claims 校验。 pub struct EnvelopeVerifier { decryptor: crypt::RsaDecryptor, mac_key_id: String, mac_key: Vec, clock_skew_secs: i64, nonce_ttl_secs: u64, store: NonceStore, } impl EnvelopeVerifier { pub fn new( key_id: &str, private_pem: &str, mac_key_id: &str, mac_key: &[u8], clock_skew_secs: u64, nonce_ttl_secs: u64, store: NonceStore, ) -> Result { let pair = crypt::RsaKeyPair::from_private_pem(key_id, private_pem).map_err(|error| { InternalError::Config(format!("invalid identity private key: {error}")) })?; let decryptor = crypt::RsaDecryptor::new(vec![pair]) .map_err(|error| InternalError::Config(format!("invalid identity key set: {error}")))?; Ok(Self { decryptor, mac_key_id: mac_key_id.to_owned(), mac_key: mac_key.to_vec(), clock_skew_secs: clock_skew_secs as i64, nonce_ttl_secs, store, }) } pub async fn verify( &self, envelope: &proto::internal::v1::IdentityEnvelope, ) -> Result { verified_identity_impl(self, envelope) .await .map_err(InternalError::into_status) } } async fn verified_identity_impl( verifier: &EnvelopeVerifier, envelope: &proto::internal::v1::IdentityEnvelope, ) -> Result { if envelope.audience != AUDIENCE { return Err(InternalError::Envelope("audience mismatch".to_string())); } if envelope.mac_key_id != verifier.mac_key_id { return Err(InternalError::Envelope("unknown mac key".to_string())); } let now = now_ms(); if envelope.issued_at_unix_ms > now + verifier.clock_skew_secs * 1000 { return Err(InternalError::Envelope( "envelope issued in the future".to_string(), )); } if envelope.expires_at_unix_ms + verifier.clock_skew_secs * 1000 < now { return Err(InternalError::Envelope("envelope expired".to_string())); } verify_mac( &verifier.mac_key, &mac_input( &envelope.key_id, &envelope.ciphertext, &envelope.mac_key_id, envelope.issued_at_unix_ms, envelope.expires_at_unix_ms, &envelope.nonce, &envelope.audience, ), &envelope.mac, )?; if envelope.nonce.trim().is_empty() || envelope.nonce.len() > 128 { return Err(InternalError::Envelope("invalid nonce".to_string())); } if !verifier .store .accept( &crate::nonce::key(&envelope.audience, &envelope.nonce), verifier.nonce_ttl_secs, ) .await? { return Err(InternalError::Replay); } let claims_json = crypt::decrypt_string(&verifier.decryptor, &envelope.ciphertext) .map_err(|_| InternalError::Envelope("decryption failed".to_string()))?; let claims: Claims = serde_json::from_str(&claims_json) .map_err(|_| InternalError::Envelope("claims are malformed".to_string()))?; if claims.v != CLAIMS_VERSION { return Err(InternalError::Envelope( "unsupported claims version".to_string(), )); } if claims.aud != AUDIENCE || claims.uid <= 0 { return Err(InternalError::Envelope( "claims audience or user is invalid".to_string(), )); } if claims.exp + verifier.clock_skew_secs * 1000 < now { return Err(InternalError::Envelope("claims expired".to_string())); } Ok(VerifiedIdentity { user_id: claims.uid, security_version: claims.sv, session_id: claims.sid, request_id: if claims.rid.is_empty() { envelope.request_id.clone() } else { claims.rid }, key_id: envelope.key_id.clone(), }) }