wxpay-rs 2.0.1

WeChat Pay API v3 Rust SDK
Documentation
//! 签名器模块
//!
//! 提供请求签名功能,使用 SHA256-RSA 算法。

use async_trait::async_trait;
use base64::Engine;
use rsa::pkcs1::DecodeRsaPrivateKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
use sha2::{Digest, Sha256};

use crate::error::{WxPayError, WxPayResult};

/// 签名器 trait
///
/// 定义了生成请求签名的接口。
#[async_trait]
pub trait Signer: Send + Sync {
    /// 生成签名
    ///
    /// # 参数
    ///
    /// * `message` - 要签名的消息
    ///
    /// # 返回
    ///
    /// 返回 Base64 编码的签名字符串
    async fn sign(&self, message: &str) -> WxPayResult<String>;

    /// 获取商户号
    fn merchant_id(&self) -> &str;

    /// 获取证书序列号
    fn cert_serial_number(&self) -> &str;
}

/// SHA256-RSA 签名器
///
/// 使用 SHA256WithRSA 算法生成请求签名。
///
/// # 示例
///
/// ```rust,no_run
/// use wxpay_rs::auth::{Signer, Sha256RsaSigner};
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let private_key_pem = std::fs::read_to_string("path/to/private_key.pem")?;
/// let signer = Sha256RsaSigner::new(
///     "1900000109",
///     private_key_pem.as_bytes(),
///     "CERT123456",
/// )?;
///
/// let signature = signer.sign("test message").await?;
/// # Ok(())
/// # }
/// ```
pub struct Sha256RsaSigner {
    /// 商户号
    merchant_id: String,
    /// 商户私钥
    private_key: RsaPrivateKey,
    /// 证书序列号
    cert_serial_number: String,
}

impl Sha256RsaSigner {
    /// 创建新的 SHA256-RSA 签名器
    ///
    /// # 参数
    ///
    /// * `merchant_id` - 商户号
    /// * `private_key_pem` - 私钥(PEM 格式)
    /// * `cert_serial_number` - 证书序列号
    ///
    /// # 返回
    ///
    /// 返回签名器实例
    pub fn new(
        merchant_id: impl Into<String>,
        private_key_pem: &[u8],
        cert_serial_number: impl Into<String>,
    ) -> WxPayResult<Self> {
        let private_key = Self::parse_private_key(private_key_pem)?;
        Ok(Self {
            merchant_id: merchant_id.into(),
            private_key,
            cert_serial_number: cert_serial_number.into(),
        })
    }

    /// 解析私钥
    fn parse_private_key(pem: &[u8]) -> WxPayResult<RsaPrivateKey> {
        let pem_str = std::str::from_utf8(pem)
            .map_err(|e| WxPayError::InvalidPrivateKey(format!("无效的 UTF-8 编码:{}", e)))?;

        // 尝试 PKCS#8 格式
        if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(pem_str) {
            return Ok(key);
        }

        // 尝试 PKCS#1 格式
        if let Ok(key) = RsaPrivateKey::from_pkcs1_pem(pem_str) {
            return Ok(key);
        }

        Err(WxPayError::InvalidPrivateKey(
            "无法解析私钥,请确保是有效的 PKCS#8 或 PKCS#1 PEM 格式".to_string(),
        ))
    }

    /// 构建签名消息
    ///
    /// 微信支付 API v3 签名格式:
    /// HTTP_METHOD\nURL_PATH\nTIMESTAMP\nNONCE_STR\nBODY\n
    pub fn build_sign_message(
        method: &str,
        url: &str,
        timestamp: i64,
        nonce: &str,
        body: &str,
    ) -> String {
        // 性能优化:预分配容量并就地格式化时间戳,避免 `format!` 的临时 String 分配。
        use std::fmt::Write;
        let mut s = String::with_capacity(
            method.len() + url.len() + nonce.len() + body.len() + /*timestamp*/ 20 + /*换行*/ 5,
        );
        let _ = write!(
            s,
            "{}\n{}\n{}\n{}\n{}\n",
            method, url, timestamp, nonce, body
        );
        s
    }

    /// 构建 Authorization Header
    ///
    /// 格式:WECHATPAY2-SHA256-RSA2048 mchid="...",nonce_str="...",timestamp="...",serial_no="...",signature="..."
    pub fn build_authorization_header(
        &self,
        nonce: &str,
        timestamp: i64,
        signature: &str,
    ) -> String {
        // 性能优化:预分配容量并就地格式化时间戳,避免 `format!` 的临时 String 分配。
        use std::fmt::Write;
        const PREFIX: &str = r#"WECHATPAY2-SHA256-RSA2048 mchid=""#;
        let mut s = String::with_capacity(
            PREFIX.len()
                + self.merchant_id.len()
                + nonce.len()
                + self.cert_serial_number.len()
                + signature.len()
                + /*固定分隔与引号*/ 64
                + /*timestamp*/ 20,
        );
        let _ = write!(
            s,
            r#"WECHATPAY2-SHA256-RSA2048 mchid="{}",nonce_str="{}",timestamp="{}",serial_no="{}",signature="{}""#,
            self.merchant_id, nonce, timestamp, self.cert_serial_number, signature
        );
        s
    }
}

#[async_trait]
impl Signer for Sha256RsaSigner {
    async fn sign(&self, message: &str) -> WxPayResult<String> {
        // 计算 SHA256 哈希
        let mut hasher = Sha256::new();
        hasher.update(message.as_bytes());
        let hash = hasher.finalize();

        // 使用 RSA PKCS1v15 签名
        let signature = self
            .private_key
            .sign(Pkcs1v15Sign::new::<Sha256>(), &hash)
            .map_err(|e| WxPayError::SignError(format!("RSA 签名失败: {}", e)))?;

        // Base64 编码
        Ok(base64::engine::general_purpose::STANDARD.encode(&signature))
    }

    fn merchant_id(&self) -> &str {
        &self.merchant_id
    }

    fn cert_serial_number(&self) -> &str {
        &self.cert_serial_number
    }
}

impl std::fmt::Debug for Sha256RsaSigner {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Sha256RsaSigner")
            .field("merchant_id", &self.merchant_id)
            .field("cert_serial_number", &self.cert_serial_number)
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use base64::Engine;

    /// 测试用 PKCS#8 私钥(PEM),2048-bit,与 auth/verifier 测试用证书配套。
    const TEST_PRIVATE_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDQFwtb0xnMYumg\neu5lhc+Fv/XfU2hJcPnWtjhm3MVBhEM73dmsZ0yrvOxZtJhs4dfKs8BlWKvDInnz\n05+2lrDdAkNNvt0XE/0B55n2Hbk4yZIx6zOfsJlrcEoLMTfE8YNhmGeRmE+L3OJ2\nL9IAeMZW5If3T20E65+8BohE8nwLYXndXDTMZD1MAHj3fygCn2TZHKqLUf9lzYoe\naK5Wc9A8kmO6dMcefXkskvJKJZ+S/G0f+1aFcN8MaI7GFgUkdszgnElZKWxfiv/r\nXQt2T88ZcK0Apsypl5fludW9IzKjpTrJtGx8R4tVfZ0veQz3xTU7joRU7mUjByhf\nSes6QE3tAgMBAAECgf8ZVV+Mo6arELULVJaxcBj+WjW/epK3s4lhxSLDYx1LXKQo\nJa+FIw5dL3hBc5BwW7kUdHh33ikLGKdq3S4UjJlQ+XWNgYRpIDCCitpeRurF1G8i\npKp5m9u8Y29K7YhcnF/iVyuaDhuhFhh79avGDZjCpg/ni+6PKssc7llTYNy5MGya\nBNkxzXX2Oo5WI1IBOptOEUb6iWYz5FoAf91Ai0K8mFuB5tPCv67DqB2Rq4c6LMoX\nVzwzMZ64GhzYC6vyjltzMjtYTIDvheOZsOUgJe1pAaChwiGRDpmuf8/oybSQFFsy\n1PYF+TddnNk0NOQCPI0qXLHE2OXtdDAigPiA5v8CgYEA6/BnV4O/ZS34WvaGucPx\nQp9s59FolMyWtwELLxOZaO1LPAa9pdNC1+IfUl6zpeRu2z1kNG9f2TbgtTVrF7Lu\n5XvuhJ2OqnL8GgGYpS0vj2Sx5XRO8/pgxiAnpRy7Mkp1jA4+ZTpNQH3FoA6LZZfM\n1v/ijOH9NeHUWEw64OE/OoMCgYEA4ch19Yp73ijLvEUyAkqYrvPOkm7G02mlRD4T\nTUe2tGe8HUbOZGi5CphvItto9mssPDDsEVLilkrPDKlg3899L+ZLE8vHzw6QVoaK\n8LDQaapWbW3LazwLAna4kpNDd06h+Rx7j/n1lha6Vj/2dbEQhAAllos92B7SCNf8\nYIiXqs8CgYACC3tZztKB1fwpDantQj19DlSrTa1SXNORkni+V7Ukq6nTQ1uxbDtQ\nE62h0SBNd8VeMRIFQlHaWBdqeqQK+IoJgyF2FMd/wq9cqlbgV5vp6j2Ad5mXk7vy\n+6RcUfttXCfYpubziaXRwUVNNdMPdllYI6+a+Ppw1Rw6B68a89jQcQKBgFaW+JY4\njBTBdJE5wFocnb3LBxgln98IjzdCz0g+DpXVitF3jEP53a1wlH67wt9ubsKOyJpE\nPV4CRrHGa76p5oruOTDYYELKhRSJ+NMiHGvJxeelyfPQTTCes16TV7Zz066j+8dV\nx5fOE5xsX2r3gyv8mm3H7OnruAVoQAQNno0FAoGBAOvD07di46NEaY7OTGzt4JwE\nWa/0KzWvrQ6SCaHUnZ1yIqL6jEV7RCxKGr206cW9nlG2+n2QqAC8dinDrdLspLZG\noEqm/DoCUaghQOGnh7teguj3eqS+MHU5T/ugSJdJoMNtpQ/BlSnqkWLPoh+yrvh5\nmVKYyABhNkZONhC533bA\n-----END PRIVATE KEY-----\n";

    fn test_signer() -> Sha256RsaSigner {
        Sha256RsaSigner::new("1900000109", TEST_PRIVATE_KEY_PEM.as_bytes(), "CERT123456")
            .expect("测试签名器应创建成功")
    }

    #[test]
    fn test_build_sign_message() {
        let message = Sha256RsaSigner::build_sign_message(
            "POST",
            "/v3/pay/transactions/jsapi",
            1609459200,
            "test_nonce",
            r#"{"app_id":"wx88888888"}"#,
        );

        assert!(message.starts_with("POST\n"));
        assert!(message.contains("/v3/pay/transactions/jsapi"));
        assert!(message.contains("1609459200"));
        assert!(message.contains("test_nonce"));
        assert!(message.ends_with("\n"));

        // 性能优化回归:与 format! 产物逐字节等价。
        assert_eq!(
            message,
            format!(
                "POST\n/v3/pay/transactions/jsapi\n1609459200\ntest_nonce\n{{\"app_id\":\"wx88888888\"}}\n"
            )
        );
    }

    #[test]
    fn test_build_authorization_header() {
        // 用真实签名器实例构建,验证格式正确性(商户号、序列号、时间戳、nonce、签名均嵌入)。
        let signer = test_signer();
        let header = signer.build_authorization_header("nonce_abc", 1700000000, "sig_xyz");

        assert!(header.starts_with("WECHATPAY2-SHA256-RSA2048 "));
        assert!(header.contains("mchid=\"1900000109\""));
        assert!(header.contains("nonce_str=\"nonce_abc\""));
        assert!(header.contains("timestamp=\"1700000000\""));
        assert!(header.contains("serial_no=\"CERT123456\""));
        assert!(header.contains("signature=\"sig_xyz\""));
    }

    #[tokio::test]
    async fn test_sign_is_deterministic_and_well_formed() {
        // PKCS1v15 + SHA256 是确定性签名:同一消息两次签名应完全一致。
        let signer = test_signer();
        let message = r#"{"app_id":"wx88888888","mchid":"1900000109"}"#;

        let sig_a = signer.sign(message).await.unwrap();
        let sig_b = signer.sign(message).await.unwrap();
        assert_eq!(sig_a, sig_b, "PKCS1v15 签名应为确定性的");

        // 2048-bit RSA 签名 = 256 字节 -> base64 长度 344(含可能的填充)。
        let bytes = base64::engine::general_purpose::STANDARD
            .decode(&sig_a)
            .expect("签名应为合法 base64");
        assert_eq!(bytes.len(), 256, "2048 位密钥签名应为 256 字节");

        // 不同消息应产生不同签名。
        let sig_other = signer.sign("different message").await.unwrap();
        assert_ne!(sig_a, sig_other);
    }

    #[tokio::test]
    async fn test_signer_accessors() {
        let signer = test_signer();
        assert_eq!(signer.merchant_id(), "1900000109");
        assert_eq!(signer.cert_serial_number(), "CERT123456");
    }

    #[test]
    fn test_new_rejects_invalid_private_key() {
        let result = Sha256RsaSigner::new("mch", b"not a valid pem", "serial");
        assert!(matches!(result, Err(WxPayError::InvalidPrivateKey(_))));
    }

    #[test]
    fn test_new_rejects_non_utf8_key() {
        // 非 UTF-8 字节应被拒绝,而非 panic。
        let result = Sha256RsaSigner::new("mch", &[0xff, 0xfe, 0xfd], "serial");
        assert!(matches!(result, Err(WxPayError::InvalidPrivateKey(_))));
    }
}