tasign 0.2.0

TA ELF signing utilities with CMS/PKCS#7 support
//! SM2 / RSA / ECDSA 的 CMS 签名与验签回归:`cms-sign` 路径使用 mbedtls-smx;GmSSL 仅在脚本或
//! `cms-compat=gmssl` 演示中使用。

use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use std::path::Path;
use std::process::Command;
use tasign::{
    sign_gmssl_cms_attached_native, sign_sm2_cms, verify_elf_signature_from_parts, verify_sm2_cms,
    verify_sm2_cms_with_ca_pem, CmsSignAlgorithm, SignInputs,
};

static LEAF_PEM: &str = include_str!("fixtures/gmssl/leaf.crt");
static INT_PEM: &str = include_str!("fixtures/gmssl/intermediate.crt");
static CA_PEM: &str = include_str!("fixtures/gmssl/ca.crt");
const LEAF_KEY_PATH: &str = "tests/fixtures/gmssl/leaf.key";
static CA_LEAF_PEM: &str = include_str!("fixtures/gmssl/ca_leaf.crt");
const CA_LEAF_KEY_PATH: &str = "tests/fixtures/gmssl/ca_leaf.key";
const KEY_PASS: &str = "123456";

fn cert_der_from_pem(pem: &str) -> Vec<u8> {
    let b64 = pem
        .lines()
        .filter(|l| !l.starts_with("-----"))
        .collect::<String>();
    STANDARD.decode(b64).expect("pem b64")
}

fn der_to_pem_cms(der: &[u8]) -> String {
    let b64 = STANDARD.encode(der);
    let mut out = String::new();
    out.push_str("-----BEGIN CMS-----\n");
    for chunk in b64.as_bytes().chunks(64) {
        out.push_str(std::str::from_utf8(chunk).unwrap());
        out.push('\n');
    }
    out.push_str("-----END CMS-----\n");
    out
}

/// 仓库根目录:`tasign` 的上一级再上一级(`…/tasign-trae`)。
fn repo_root() -> std::path::PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
}

/// `GmSSL/build/bin/gmssl`(与 README / 脚本默认一致)。
fn gmssl_build_bin() -> std::path::PathBuf {
    repo_root().join("GmSSL/build/bin/gmssl")
}

#[test]
fn cms_sign_verify_roundtrip() {
    let plain = b"hello plain.bin payload for tasign";
    let leaf_der = cert_der_from_pem(LEAF_PEM);
    let int_der = cert_der_from_pem(INT_PEM);
    let ints: Vec<&[u8]> = vec![&int_der];
    let pkcs7 = sign_sm2_cms(SignInputs {
        plain,
        leaf_cert_der: Some(&leaf_der),
        intermediate_certs_der: &ints,
        cms_attached: false,
        cms_use_gmssl_oid: false,
        leaf_key_path: Some(Path::new(LEAF_KEY_PATH)),
        leaf_key_pass: Some(KEY_PASS),
        gmssl_path: None,
        algorithm: CmsSignAlgorithm::Sm2WithSm3,
        use_bjca: false,
        bjca_config_path: None,
    })
    .expect("sign");

    verify_elf_signature_from_parts(plain, &pkcs7, None).expect("verify");
}

#[test]
fn cms_sign_verify_roundtrip_with_ca_chain() {
    let plain = b"hello plain.bin payload for tasign chain verify";
    let leaf_der = cert_der_from_pem(LEAF_PEM);
    let int_der = cert_der_from_pem(INT_PEM);
    let ints: Vec<&[u8]> = vec![&int_der];
    let pkcs7 = sign_sm2_cms(SignInputs {
        plain,
        leaf_cert_der: Some(&leaf_der),
        intermediate_certs_der: &ints,
        cms_attached: false,
        cms_use_gmssl_oid: false,
        leaf_key_path: Some(Path::new(LEAF_KEY_PATH)),
        leaf_key_pass: Some(KEY_PASS),
        gmssl_path: None,
        algorithm: CmsSignAlgorithm::Sm2WithSm3,
        use_bjca: false,
        bjca_config_path: None,
    })
    .expect("sign");

    verify_elf_signature_from_parts(plain, &pkcs7, Some(CA_PEM.as_bytes()))
        .expect("verify with chain");
}

#[test]
fn cms_verify_fails_with_wrong_intermediate() {
    let plain = b"hello plain.bin payload for tasign wrong intermediate";
    let leaf_der = cert_der_from_pem(LEAF_PEM);
    // 反例:故意把 leaf 证书当作 intermediate 塞入 CMS,链路应无法构建到 Root CA。
    let wrong_int_der = leaf_der.clone();
    let ints: Vec<&[u8]> = vec![&wrong_int_der];
    let pkcs7 = sign_sm2_cms(SignInputs {
        plain,
        leaf_cert_der: Some(&leaf_der),
        intermediate_certs_der: &ints,
        cms_attached: false,
        cms_use_gmssl_oid: false,
        leaf_key_path: Some(Path::new(LEAF_KEY_PATH)),
        leaf_key_pass: Some(KEY_PASS),
        gmssl_path: None,
        algorithm: CmsSignAlgorithm::Sm2WithSm3,
        use_bjca: false,
        bjca_config_path: None,
    })
    .expect("sign");

    let err = verify_sm2_cms_with_ca_pem(&pkcs7, plain, CA_PEM.as_bytes()).unwrap_err();
    let s = err.to_string();
    assert!(
        s.contains("chain verify failed") || s.contains("certificate") || s.contains("verify"),
        "{s}"
    );
}

#[test]
fn cms_rejects_tampered_plain() {
    let plain = b"original";
    let leaf_der = cert_der_from_pem(LEAF_PEM);
    let int_der = cert_der_from_pem(INT_PEM);
    let ints: Vec<&[u8]> = vec![&int_der];
    let pkcs7 = sign_sm2_cms(SignInputs {
        plain,
        leaf_cert_der: Some(&leaf_der),
        intermediate_certs_der: &ints,
        cms_attached: false,
        cms_use_gmssl_oid: false,
        leaf_key_path: Some(Path::new(LEAF_KEY_PATH)),
        leaf_key_pass: Some(KEY_PASS),
        gmssl_path: None,
        algorithm: CmsSignAlgorithm::Sm2WithSm3,
        use_bjca: false,
        bjca_config_path: None,
    })
    .expect("sign");

    let err = verify_sm2_cms(&pkcs7, b"tampered").unwrap_err();
    let s = err.to_string();
    assert!(
        s.contains("messageDigest") || s.contains("mismatch") || s.contains("SM2"),
        "{s}"
    );
}

#[test]
fn cms_sign_verify_without_intermediate() {
    let plain = b"plain no intermediate";
    let leaf_der = cert_der_from_pem(LEAF_PEM);
    let ints: Vec<&[u8]> = vec![];
    let pkcs7 = sign_sm2_cms(SignInputs {
        plain,
        leaf_cert_der: Some(&leaf_der),
        intermediate_certs_der: &ints,
        cms_attached: false,
        cms_use_gmssl_oid: false,
        leaf_key_path: Some(Path::new(LEAF_KEY_PATH)),
        leaf_key_pass: Some(KEY_PASS),
        gmssl_path: None,
        algorithm: CmsSignAlgorithm::Sm2WithSm3,
        use_bjca: false,
        bjca_config_path: None,
    })
    .expect("sign");
    verify_sm2_cms(&pkcs7, plain).expect("verify");
}

#[test]
fn cms_sign_verify_without_intermediate_ca_issued_leaf_with_chain() {
    let plain = b"plain no intermediate with direct ca-issued leaf";
    let leaf_der = cert_der_from_pem(CA_LEAF_PEM);
    let ints: Vec<&[u8]> = vec![];
    let pkcs7 = sign_sm2_cms(SignInputs {
        plain,
        leaf_cert_der: Some(&leaf_der),
        intermediate_certs_der: &ints,
        cms_attached: false,
        cms_use_gmssl_oid: false,
        leaf_key_path: Some(Path::new(CA_LEAF_KEY_PATH)),
        leaf_key_pass: Some(KEY_PASS),
        gmssl_path: None,
        algorithm: CmsSignAlgorithm::Sm2WithSm3,
        use_bjca: false,
        bjca_config_path: None,
    })
    .expect("sign");
    verify_sm2_cms_with_ca_pem(&pkcs7, plain, CA_PEM.as_bytes()).expect("verify with root CA");
}

/// 闭环:`tasign` 生成的 **GmSSL 兼容 Attached CMS**(`sign_gmssl_cms_attached_native`,certificates 仅 leaf)
/// 须能被仓库内构建的 `GmSSL/build/bin/gmssl cmsverify` 验证通过。
#[test]
fn gmssl_cmsverify_leaf_only_native_signature() {
    let plain = b"tasign: cmsverify closed-loop, leaf-only, no intermediate";
    let leaf_der = cert_der_from_pem(LEAF_PEM);
    let leaf_key_pem = include_str!("fixtures/gmssl/leaf.key");

    let pkcs7_der = match sign_gmssl_cms_attached_native(&leaf_der, leaf_key_pem, KEY_PASS, plain) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("skip gmssl_cmsverify_leaf_only_native_signature: sign failed: {e}");
            return;
        }
    };

    let gmssl = gmssl_build_bin();
    if !gmssl.exists() {
        eprintln!(
            "skip gmssl_cmsverify_leaf_only_native_signature: not found {}",
            gmssl.display()
        );
        return;
    }

    let tmp = tempfile::tempdir().expect("tempdir");
    let cms_pem = tmp.path().join("signed.cms.pem");
    let out_plain = tmp.path().join("verified_content.bin");
    std::fs::write(&cms_pem, der_to_pem_cms(&pkcs7_der)).expect("write cms pem");

    let mut cmd = Command::new(&gmssl);
    cmd.args([
        "cmsverify",
        "-in",
        cms_pem.to_str().expect("utf8 path"),
        "-out",
        out_plain.to_str().expect("utf8 path"),
    ]);
    #[cfg(unix)]
    {
        let lib = repo_root().join("GmSSL/build/lib");
        if lib.is_dir() {
            let new_ld = match std::env::var("LD_LIBRARY_PATH") {
                Ok(old) if !old.is_empty() => format!("{}:{}", lib.display(), old),
                _ => lib.display().to_string(),
            };
            cmd.env("LD_LIBRARY_PATH", new_ld);
        }
    }

    let out = match cmd.output() {
        Ok(o) => o,
        Err(e) => {
            eprintln!("skip gmssl_cmsverify_leaf_only_native_signature: {e}");
            return;
        }
    };

    if !out.status.success() {
        let stderr = String::from_utf8_lossy(&out.stderr);
        if stderr.contains("libgmssl") || stderr.contains("cannot open shared object") {
            eprintln!("skip gmssl_cmsverify_leaf_only_native_signature: {stderr}");
            return;
        }
        panic!(
            "gmssl cmsverify failed (status {:?}):\nstdout={}\nstderr={}",
            out.status,
            String::from_utf8_lossy(&out.stdout),
            stderr
        );
    }

    let recovered = std::fs::read(&out_plain).expect("read verified content");
    assert_eq!(
        recovered.as_slice(),
        plain.as_slice(),
        "cmsverify output must match signed plaintext"
    );

    // 同一份数据再走 tasign 代码路径(mbedtls-smx)验签,应与 gmssl 命令一致通过。
    verify_sm2_cms(&pkcs7_der, plain).expect("tasign verify gmssl-compatible cms");
}