ferritls-rustls 0.9.0

rustls CryptoProvider adapter backed by ferritls-core (pure Rust)
Documentation
//! 适配层 API 形状测试(M6/M7)。
//!
//! 清单断言防止套件/组清单与文档漂移;provider smoke 覆盖两个出厂
//! 配置的构成与 `fips()` 语义(认证前恒 false)。

#[test]
fn suite_inventory_matches_docs() {
    assert_eq!(ferritls_rustls::cipher::TLS13_SUITE_NAMES.len(), 5);
    assert!(ferritls_rustls::cipher::TLS13_SUITE_NAMES.contains(&"TLS_AES_128_GCM_SHA256"));
    assert!(ferritls_rustls::cipher::TLS13_SUITE_NAMES.contains(&"TLS_AES_256_GCM_SHA384"));
    assert!(ferritls_rustls::cipher::TLS13_SUITE_NAMES.contains(&"TLS_CHACHA20_POLY1305_SHA256"));
    assert!(ferritls_rustls::cipher::TLS13_SUITE_NAMES.contains(&"TLS_AES_128_CCM_SHA256"));
    assert!(ferritls_rustls::cipher::TLS13_SUITE_NAMES.contains(&"TLS_AES_128_CCM_8_SHA256"));
}

/// QUIC 接线防漂移(M8.5):GCM/ChaCha 三套件暴露 `quic::Algorithm`
/// 且密钥长度正确,CCM 不参与 QUIC。
#[test]
fn quic_algorithms_wired_for_three_suites() {
    let cases = [
        (ferritls_rustls::cipher::tls13_aes_128_gcm_sha256(), 16),
        (ferritls_rustls::cipher::tls13_aes_256_gcm_sha384(), 32),
        (
            ferritls_rustls::cipher::tls13_chacha20_poly1305_sha256(),
            32,
        ),
    ];
    for (suite, key_len) in cases {
        let quic_alg = suite
            .tls13()
            .expect("tls13")
            .quic
            .expect("GCM/ChaCha 套件必须接线 QUIC");
        assert_eq!(quic_alg.aead_key_len(), key_len);
        // 规则 3:认证前 fips() 恒 false
        assert!(!quic_alg.fips());
    }
    assert!(
        ferritls_rustls::cipher::tls13_aes_128_ccm_sha256()
            .tls13()
            .expect("tls13")
            .quic
            .is_none()
            && ferritls_rustls::cipher::tls13_aes_128_ccm_8_sha256()
                .tls13()
                .expect("tls13")
                .quic
                .is_none(),
        "CCM(含 CCM_8)不参与 QUIC(RFC 9001 §5.1 以 AES-GCM 为强制基准)"
    );
}

/// 套件批准面防漂移:CCM_8 仅默认模式(8 字节标签不在 SP 800-52r2
/// 批准套件面),批准模式清单不含 ChaCha/CCM_8。
#[test]
fn ccm8_suite_excluded_from_fips_inventory() {
    let fips: Vec<_> = ferritls_rustls::cipher::fips_tls13_suites();
    assert_eq!(fips.len(), 3, "批准模式 = GCM×2 + CCM");
    assert!(
        fips.iter()
            .all(|s| s.suite() != rustls::CipherSuite::TLS13_AES_128_CCM_8_SHA256)
    );

    let all: Vec<_> = ferritls_rustls::cipher::all_tls13_suites();
    assert_eq!(all.len(), 5);
    assert!(
        all.iter()
            .any(|s| s.suite() == rustls::CipherSuite::TLS13_AES_128_CCM_8_SHA256)
    );
}

#[test]
fn kx_group_names_are_stable() {
    use rustls::NamedGroup;
    assert_eq!(ferritls_rustls::kx::X25519_GROUP.name(), NamedGroup::X25519);
    assert_eq!(
        ferritls_rustls::kx::SECP256R1_GROUP.name(),
        NamedGroup::secp256r1
    );
    assert_eq!(
        ferritls_rustls::kx::SECP384R1_GROUP.name(),
        NamedGroup::secp384r1
    );
    assert_eq!(
        ferritls_rustls::kx::X25519MLKEM768_GROUP.name(),
        NamedGroup::X25519MLKEM768
    );
    assert_eq!(
        ferritls_rustls::kx::MLKEM512_GROUP.name(),
        NamedGroup::MLKEM512
    );
    assert_eq!(
        ferritls_rustls::kx::MLKEM768_GROUP.name(),
        NamedGroup::MLKEM768
    );
    assert_eq!(
        ferritls_rustls::kx::MLKEM1024_GROUP.name(),
        NamedGroup::MLKEM1024
    );
    assert_eq!(ferritls_rustls::kx::ALL_KX_GROUPS.len(), 7);
    assert_eq!(ferritls_rustls::kx::FIPS_KX_GROUPS.len(), 6);
    // 混合组在两个清单中均为首项(PQ 优先)
    assert_eq!(
        ferritls_rustls::kx::ALL_KX_GROUPS[0].name(),
        NamedGroup::X25519MLKEM768
    );
    assert_eq!(
        ferritls_rustls::kx::FIPS_KX_GROUPS[0].name(),
        NamedGroup::X25519MLKEM768
    );
}

#[test]
fn default_provider_smoke() {
    let p = ferritls_rustls::default_provider();
    assert!(!p.fips(), "认证前 fips() 必须为 false");
    assert_eq!(p.cipher_suites.len(), 5);
    assert_eq!(p.kx_groups.len(), 7);
}

#[test]
fn fips_mode_provider_smoke() {
    let p = ferritls_rustls::fips_mode_provider();
    // 批准模式也必须为 false:批准模式 ≠ 已认证。
    assert!(!p.fips());
    assert_eq!(p.cipher_suites.len(), 3, "批准模式无 ChaCha20-Poly1305");
    assert_eq!(
        p.kx_groups.len(),
        6,
        "批准模式:X25519MLKEM768 混合 + 纯 ML-KEM 三参数集 + P-256/384,无独立 X25519"
    );
}

/// 验证算法表形状防漂移:默认表 9 项(含 Ed25519),批准表 8 项且是
/// 默认表的子集(同一组静态 verifier);scheme → 算法映射每组恰一
/// 算法、算法都来自本表 `all` 清单(TLS 1.3 只取首项,单元素切片是
/// 该语义的最保守形态);scheme 差集恰为 ED25519。
#[test]
fn verification_tables_shape_and_fips_subset() {
    use rustls::SignatureScheme;
    use std::ptr;

    let def = ferritls_rustls::verify::SUPPORTED_ALGORITHMS;
    let fips = ferritls_rustls::verify::FIPS_SUPPORTED_ALGORITHMS;

    assert_eq!(def.all.len(), 9, "默认表:ECDSA×2 + Ed25519 + RSA×6");
    assert_eq!(fips.all.len(), 8, "批准表无 Ed25519");
    for a in fips.all {
        assert!(
            def.all.iter().any(|b| ptr::eq(*a, *b)),
            "批准表出现默认表之外的 verifier"
        );
    }

    for (mapping, all, what) in [
        (def.mapping, def.all, "默认表"),
        (fips.mapping, fips.all, "批准表"),
    ] {
        let mut schemes: Vec<u16> = mapping.iter().map(|(s, _)| u16::from(*s)).collect();
        schemes.sort_unstable();
        schemes.dedup();
        assert_eq!(schemes.len(), mapping.len(), "{what} scheme 键不得重复");
        for (scheme, algs) in mapping {
            assert_eq!(
                algs.len(),
                1,
                "{what} {scheme:?} 映射必须单算法(TLS 1.3 首项语义的最保守形态)"
            );
            assert!(
                all.iter().any(|b| ptr::eq(algs[0], *b)),
                "{what} {scheme:?} 映射的 verifier 不在本表 all 中"
            );
        }
    }

    let def_schemes: Vec<u16> = def.mapping.iter().map(|(s, _)| u16::from(*s)).collect();
    let fips_schemes: Vec<u16> = fips.mapping.iter().map(|(s, _)| u16::from(*s)).collect();
    let diff: Vec<u16> = def_schemes
        .iter()
        .copied()
        .filter(|s| !fips_schemes.contains(s))
        .collect();
    assert_eq!(
        diff.as_slice(),
        &[u16::from(SignatureScheme::ED25519)],
        "批准表与默认表的 scheme 差集恰为 Ed25519"
    );
}

/// FIPS 表消费防漂移:两个 provider 的验证算法表必须指针一致地指向
/// 对应静态表,防止装配时误接(批准模式误接默认表会把 Ed25519 放进
/// 批准面,反之会让默认模式丢算法)。
#[test]
fn providers_consume_their_verification_tables() {
    use std::ptr;

    let def = ferritls_rustls::default_provider();
    let fips = ferritls_rustls::fips_mode_provider();

    assert!(ptr::eq(
        def.signature_verification_algorithms.all,
        ferritls_rustls::verify::SUPPORTED_ALGORITHMS.all
    ));
    assert!(ptr::eq(
        def.signature_verification_algorithms.mapping,
        ferritls_rustls::verify::SUPPORTED_ALGORITHMS.mapping
    ));
    assert!(ptr::eq(
        fips.signature_verification_algorithms.all,
        ferritls_rustls::verify::FIPS_SUPPORTED_ALGORITHMS.all
    ));
    assert!(ptr::eq(
        fips.signature_verification_algorithms.mapping,
        ferritls_rustls::verify::FIPS_SUPPORTED_ALGORITHMS.mapping
    ));
}

/// X25519MLKEM768 端到端形状与共享秘密一致性(M8.3):
/// 客户端 share 1216 B、服务端 share 1120 B、ss 64 B,两侧一致;
/// 篡改 share 必须被拒绝(ek 模校验 / 密文长度)。
#[test]
fn x25519_mlkem768_roundtrip_and_rejects() {
    use rustls::crypto::CompletedKeyExchange;
    let group = ferritls_rustls::kx::X25519MLKEM768_GROUP;

    // 正常往返(complete 消费 Box,负例先行)
    let client = group.start().expect("client start");
    assert_eq!(client.pub_key().len(), 1184 + 32, "客户端 share 形状");
    let server: CompletedKeyExchange = group
        .start_and_complete(client.pub_key())
        .expect("server start_and_complete");
    assert_eq!(server.pub_key.len(), 1088 + 32, "服务端 share 形状");

    // 负例 1:客户端 share 长度错误 → 服务端拒绝
    let mut bad_share = client.pub_key().to_vec();
    bad_share.pop();
    assert!(group.start_and_complete(&bad_share).is_err());

    // 负例 2:服务端 share 长度错误 → 客户端 complete 拒绝
    let client2 = group.start().expect("client start 2");
    let server2 = group
        .start_and_complete(client2.pub_key())
        .expect("server 2");
    let mut short = server2.pub_key.clone();
    short.pop();
    assert!(client2.complete(&short).is_err());

    // 正向 complete(消费 client)
    let ss_client = client.complete(&server.pub_key).expect("client complete");
    assert_eq!(ss_client.secret_bytes().len(), 64, "ss = ML-KEM ‖ X25519");
    assert_eq!(
        ss_client.secret_bytes(),
        server.secret.secret_bytes(),
        "两侧共享秘密一致"
    );
}

/// 纯 ML-KEM 组(0x0200–0x0202)端到端形状与共享秘密一致性(M8.4):
/// 客户端 share = ek、服务端 share = ct、ss 32 B,三参数集逐一验证;
/// ek/ct 长度不符必须被拒绝(§7.2 封装密钥检查 / 密文长度检查)。
#[test]
fn pure_mlkem_groups_roundtrip_and_rejects() {
    use rustls::crypto::CompletedKeyExchange;

    let cases = [
        (
            ferritls_rustls::kx::MLKEM512_GROUP,
            800, // ek
            768, // ct
        ),
        (ferritls_rustls::kx::MLKEM768_GROUP, 1184, 1088),
        (ferritls_rustls::kx::MLKEM1024_GROUP, 1568, 1568),
    ];
    for (group, ek_bytes, ct_bytes) in cases {
        let client = group.start().expect("client start");
        assert_eq!(client.pub_key().len(), ek_bytes, "客户端 share = ek");

        // 负例 1:客户端 share 长度错误 → 服务端拒绝
        let mut bad_share = client.pub_key().to_vec();
        bad_share.pop();
        assert!(group.start_and_complete(&bad_share).is_err());

        // 负例 2:内容非法(全 0xFF 首系数 ≥ q)→ §7.2 模校验拒绝
        let garbage = vec![0xffu8; ek_bytes];
        assert!(
            group.start_and_complete(&garbage).is_err(),
            "ek 模校验必须拒绝 0xFF 填充"
        );

        // 正常往返
        let server: CompletedKeyExchange = group
            .start_and_complete(client.pub_key())
            .expect("server start_and_complete");
        assert_eq!(server.pub_key.len(), ct_bytes, "服务端 share = ct");
        assert_eq!(server.secret.secret_bytes().len(), 32, "ss = 32 B");

        // 负例 3:服务端 share 长度错误 → 客户端 complete 拒绝
        let client2 = group.start().expect("client start 2");
        let server2 = group
            .start_and_complete(client2.pub_key())
            .expect("server 2");
        let mut short = server2.pub_key.clone();
        short.pop();
        assert!(client2.complete(&short).is_err());

        // 正向 complete(消费 client)
        let ss_client = client.complete(&server.pub_key).expect("client complete");
        assert_eq!(
            ss_client.secret_bytes(),
            server.secret.secret_bytes(),
            "两侧共享秘密一致"
        );
    }
}