From 06e9c0db0d8c9e3281aba910a5dc7cba21f62cdb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 00:45:04 +0000 Subject: [PATCH 1/2] test(x509): guard against multi-curve signature verification fallback Add a dedicated regression test that locks in the invariant that ECDSA certificate signatures are verified only against the curve declared in the issuer's SubjectPublicKeyInfo, never falling back to trying other curves. The weak multi-try verifier (verify_ecdsa_signature) was already removed in favour of verify_ecdsa_declared_curve; this test proves the try-all path is gone in both directions and rejects genuine wrong-curve keys. Closes #28 Co-authored-by: Ty Schenk --- keetanetwork-x509/tests/builders.rs | 80 +++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/keetanetwork-x509/tests/builders.rs b/keetanetwork-x509/tests/builders.rs index cc58be2..3136e9b 100644 --- a/keetanetwork-x509/tests/builders.rs +++ b/keetanetwork-x509/tests/builders.rs @@ -393,3 +393,83 @@ fn test_ecdsa_verification_requires_declared_curve() -> Result<(), Box. +#[test] +fn test_no_multi_curve_verification_fallback() -> Result<(), Box> { + // Self-signed secp256r1 certificate. + let r1_seed = generate_random_seed()?; + let r1_seed_bytes = (*r1_seed.expose_secret()).into_secret(); + let r1_private_key = Secp256r1Derivation::derive_from_seed(r1_seed_bytes)?; + let r1_account = Account::::from(r1_private_key); + let r1_public_key = r1_account.keypair.to_public_key(); + let r1_spki = SubjectPublicKeyInfo::from(r1_public_key.clone()); + let r1_dn = utils::create_dn(&[(oids::CN, "Fallback Test secp256r1")])?; + let r1_cert = keetanetwork_x509::builder::CertificateBuilder::new() + .with_subject_public_key(r1_spki.clone()) + .with_subject_dn(r1_dn.clone()) + .with_issuer_dn(r1_dn) + .with_serial_number(SerialNumber::from(1u64)) + .with_validity_days(365) + .build(&r1_account)?; + + // Self-signed secp256k1 certificate. + let k1_seed = generate_random_seed()?; + let k1_seed_bytes = (*k1_seed.expose_secret()).into_secret(); + let k1_private_key = Secp256k1Derivation::derive_from_seed(k1_seed_bytes)?; + let k1_account = Account::::from(k1_private_key); + let k1_public_key = k1_account.keypair.to_public_key(); + let k1_spki = SubjectPublicKeyInfo::from(k1_public_key.clone()); + let k1_dn = utils::create_dn(&[(oids::CN, "Fallback Test secp256k1")])?; + let k1_cert = keetanetwork_x509::builder::CertificateBuilder::new() + .with_subject_public_key(k1_spki.clone()) + .with_subject_dn(k1_dn.clone()) + .with_issuer_dn(k1_dn) + .with_serial_number(SerialNumber::from(1u64)) + .with_validity_days(365) + .build(&k1_account)?; + + // Each certificate verifies under its own correctly declared curve. + assert!(r1_cert.verify_signature(&r1_spki)?, "secp256r1 certificate should verify with its declared curve"); + assert!(k1_cert.verify_signature(&k1_spki)?, "secp256k1 certificate should verify with its declared curve"); + + // Reusing the real public key bytes but declaring the other curve must not + // verify. The removed multi-try helper would have accepted these because it + // also tried the certificate's actual curve. + let r1_key_bytes = Vec::::from(r1_public_key); + let r1_bytes_as_k1 = SubjectPublicKeyInfo::new(k1_spki.algorithm.clone(), &r1_key_bytes)?; + assert!( + !matches!(r1_cert.verify_signature(&r1_bytes_as_k1), Ok(true)), + "secp256r1 signature declared as secp256k1 must not verify" + ); + + let k1_key_bytes = Vec::::from(k1_public_key); + let k1_bytes_as_r1 = SubjectPublicKeyInfo::new(r1_spki.algorithm.clone(), &k1_key_bytes)?; + assert!( + !matches!(k1_cert.verify_signature(&k1_bytes_as_r1), Ok(true)), + "secp256k1 signature declared as secp256r1 must not verify" + ); + + // A genuine, valid key of the other curve must also be rejected: there is no + // fallback to the certificate's actual curve. + assert!( + !r1_cert.verify_signature(&k1_spki)?, + "secp256r1 certificate must not verify against an unrelated secp256k1 key" + ); + assert!( + !k1_cert.verify_signature(&r1_spki)?, + "secp256k1 certificate must not verify against an unrelated secp256r1 key" + ); + + Ok(()) +} From 1b6751be6d11bb1b480390152e99fc38efe67e60 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 00:55:44 +0000 Subject: [PATCH 2/2] chore(deps): bump rustls to 0.23.45 to fix RUSTSEC-2026-0285 Security Audit (cargo audit) failed on a newly published advisory, RUSTSEC-2026-0285: rustls 0.23.40 accepts TLS 1.3 handshake messages across encryption level boundaries (fixed in >=0.23.45). rustls is a transitive dependency (reqwest/hyper-rustls/tokio-rustls/ rustls-platform-verifier). Update the lockfile to rustls 0.23.45, which also bumps aws-lc-rs/aws-lc-sys and rustls-webpki. Verified the client crate still builds and 'cargo audit' now passes. Co-authored-by: Ty Schenk --- Cargo.lock | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 310db63..d65359f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,9 +110,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "zeroize", @@ -120,14 +120,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -1804,6 +1805,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "polyval" version = "0.6.2" @@ -2247,9 +2254,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "once_cell", @@ -2310,9 +2317,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring",