From 0f6ec738c2e9a787234335a972e2fca6efe92522 Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Thu, 2 Jul 2026 01:08:29 -0700 Subject: [PATCH 1/4] fix(x509): wrong hash algorithm used --- keetanetwork-bindings/src/x509.rs | 1 + .../src/algorithms/secp256k1.rs | 20 ++--- .../src/algorithms/secp256r1.rs | 12 +-- .../src/operations/signature.rs | 16 ++-- keetanetwork-crypto/src/test_utils.rs | 18 ----- keetanetwork-x509/src/builder.rs | 41 ++++++++--- keetanetwork-x509/src/certificates.rs | 7 +- keetanetwork-x509/src/error.rs | 5 ++ keetanetwork-x509/src/utils.rs | 54 +++++++++----- keetanetwork-x509/tests/builders.rs | 73 ++++++++++++++++++- 10 files changed, 163 insertions(+), 84 deletions(-) diff --git a/keetanetwork-bindings/src/x509.rs b/keetanetwork-bindings/src/x509.rs index 35a408a..2212db7 100644 --- a/keetanetwork-bindings/src/x509.rs +++ b/keetanetwork-bindings/src/x509.rs @@ -36,6 +36,7 @@ impl From for CodedError { CertificateError::InvalidExtension { .. } => "INVALID_EXTENSION", CertificateError::ChainValidationFailed { .. } => "CHAIN_VALIDATION_FAILED", CertificateError::UnsupportedVersion { .. } => "UNSUPPORTED_VERSION", + CertificateError::UnsupportedSignatureHash { .. } => "UNSUPPORTED_SIGNATURE_HASH", CertificateError::CertificateSignatureVerificationFailed => "SIGNATURE_VERIFICATION_FAILED", CertificateError::CertificateDuplicateIncluded => "CERTIFICATE_DUPLICATE", CertificateError::CertificateOrphanFound => "CERTIFICATE_ORPHAN", diff --git a/keetanetwork-crypto/src/algorithms/secp256k1.rs b/keetanetwork-crypto/src/algorithms/secp256k1.rs index 36a6b22..622d08d 100644 --- a/keetanetwork-crypto/src/algorithms/secp256k1.rs +++ b/keetanetwork-crypto/src/algorithms/secp256k1.rs @@ -39,7 +39,7 @@ use crate::operations::encryption::{AsymmetricEncryption, KeyExchange, KeyGenera use crate::utils::generate_random_seed; #[cfg(feature = "signature")] -use crate::hash::{hash_default, HashAlgorithm}; +use crate::hash::hash_default; #[cfg(feature = "signature")] use crate::operations::signature::{ CryptoSigner, CryptoSignerWithOptions, CryptoVerifier, CryptoVerifierWithOptions, SigningOptions, @@ -232,11 +232,8 @@ impl CryptoSignerWithOptions for Secp256k1PrivateKey { if message.len() != 32 { return Err(::signature::Error::new()); } + signing_key.sign_prehash(message) - } else if options.for_cert { - // For certificate signing, use SHA2-256 - let data = HashAlgorithm::Sha2_256.hash(message); - signing_key.sign_prehash(&data) } else { // For regular signing, use the default hash algorithm let data = hash_default(message).to_vec(); @@ -347,18 +344,11 @@ impl CryptoVerifierWithOptions for Secp256k1PublicKey { if message.len() != 32 { return Err(::signature::Error::new()); } + verifying_key.verify_prehash(message, signature) } else { - // For non-raw verification, hash the message first - let data = if options.for_cert { - // For certificates, use SHA2-256 - HashAlgorithm::Sha2_256.hash(message) - } else { - // For regular verification, use default hash - hash_default(message).to_vec() - }; - - // Use prehash verification since we've already computed the hash + // For regular verification, use the default hash algorithm + let data = hash_default(message).to_vec(); verifying_key.verify_prehash(&data, signature) } } diff --git a/keetanetwork-crypto/src/algorithms/secp256r1.rs b/keetanetwork-crypto/src/algorithms/secp256r1.rs index 9e08e9f..6052de2 100644 --- a/keetanetwork-crypto/src/algorithms/secp256r1.rs +++ b/keetanetwork-crypto/src/algorithms/secp256r1.rs @@ -32,7 +32,7 @@ use aead::KeyInit; use p256::ecdh::diffie_hellman; #[cfg(feature = "signature")] -use crate::hash::{hash_default, HashAlgorithm}; +use crate::hash::hash_default; #[cfg(feature = "signature")] use crate::operations::signature::{ CryptoSigner, CryptoSignerWithOptions, CryptoVerifier, CryptoVerifierWithOptions, SigningOptions, @@ -233,11 +233,8 @@ impl CryptoSignerWithOptions for Secp256r1PrivateKey { if message.len() != 32 { return Err(::signature::Error::new()); } + signing_key.sign_prehash(message) - } else if options.for_cert { - // For certificate signing, use SHA2-256 - let data = HashAlgorithm::Sha2_256.hash(message); - signing_key.sign_prehash(&data) } else { // For regular signing, use the default hash algorithm (SHA3-256) let data = hash_default(message).to_vec(); @@ -347,11 +344,8 @@ impl CryptoVerifierWithOptions for Secp256r1PublicKey { if message.len() != 32 { return Err(::signature::Error::new()); } + verifying_key.verify_prehash(message, signature) - } else if options.for_cert { - // For certificate verification, use SHA2-256 - let data = HashAlgorithm::Sha2_256.hash(message); - verifying_key.verify_prehash(&data, signature) } else { // For regular verification, use the default hash algorithm let data = hash_default(message).to_vec(); diff --git a/keetanetwork-crypto/src/operations/signature.rs b/keetanetwork-crypto/src/operations/signature.rs index f88a0cc..9f792aa 100644 --- a/keetanetwork-crypto/src/operations/signature.rs +++ b/keetanetwork-crypto/src/operations/signature.rs @@ -50,27 +50,21 @@ where /// Signing and verification options for cryptographic operations. /// /// Default options are: -/// - raw: false (will pre-hash the message) -/// - for_cert: false (use SEC format, not DER) +/// - raw: false (will pre-hash the message with the network default hash) +/// +/// Callers that need a different hash pre-hash the message themselves +/// and use [`SigningOptions::raw`]. #[derive(Debug, Copy, Clone, Default)] pub struct SigningOptions { /// If true, use the raw message without hashing /// If false, pre-hash the message before signing/verification pub raw: bool, - - /// For certificate processing - pub for_cert: bool, } impl SigningOptions { /// Create options for raw message processing (no pre-hashing) pub fn raw() -> Self { - Self { raw: true, for_cert: false } - } - - /// Create options for certificate processing - pub fn for_cert() -> Self { - Self { raw: false, for_cert: true } + Self { raw: true } } } diff --git a/keetanetwork-crypto/src/test_utils.rs b/keetanetwork-crypto/src/test_utils.rs index 91381d5..59fcbf9 100644 --- a/keetanetwork-crypto/src/test_utils.rs +++ b/keetanetwork-crypto/src/test_utils.rs @@ -330,21 +330,9 @@ macro_rules! test_signatures { let different_hash = [0x42u8; 32]; // Different from hash_default(message) let signature_raw = private_key.sign_with_options(different_hash, raw_options)?; - // Test with cert options (pre-hash, but for_cert flag set) - let cert_options = crate::operations::signature::SigningOptions::for_cert(); - let signature_cert = private_key.sign_with_options(message, cert_options)?; - // Signatures should be different when using different message processing assert_ne!(signature_default.to_bytes(), signature_raw.to_bytes()); - // For Ed25519, default and cert should be the same since they both pre-hash - // For ECDSA curves, they should be different due to different hash algorithms - if stringify!($seed_suffix) == "\"ed25519\"" { - assert_eq!(signature_default.to_bytes(), signature_cert.to_bytes()); - } else { - assert_ne!(signature_default.to_bytes(), signature_cert.to_bytes()); - } - // Verify that the regular signing (which signs raw message) differs from options-based signing (which pre-hashes) // For Ed25519: try_sign signs raw message, sign_with_options(default) signs hash of message // For ECDSA: try_sign pre-hashes using one method, sign_with_options(default) might use different hash @@ -375,12 +363,6 @@ macro_rules! test_signatures { .verify_with_options(pre_computed_hash, &signature_raw, raw_options) .is_ok()); - let cert_options = crate::operations::signature::SigningOptions::for_cert(); - let signature_cert = private_key.sign_with_options(message, cert_options)?; - assert!(public_key - .verify_with_options(message, &signature_cert, cert_options) - .is_ok()); - // Test verification failure with mismatched options assert!(public_key .verify_with_options(pre_computed_hash, &signature_raw, default_options) diff --git a/keetanetwork-x509/src/builder.rs b/keetanetwork-x509/src/builder.rs index 76b6cc2..fcc771c 100644 --- a/keetanetwork-x509/src/builder.rs +++ b/keetanetwork-x509/src/builder.rs @@ -81,7 +81,9 @@ use der::Encode; #[allow(unused_imports)] use keetanetwork_asn1::BitStringExt; use keetanetwork_asn1::SubjectPublicKeyInfo; -use keetanetwork_crypto::prelude::{Algorithm, CryptoSignerWithOptions, SignatureEncoding, SigningOptions}; +use keetanetwork_crypto::prelude::{ + Algorithm, CryptoSignerWithOptions, HashAlgorithm, SignatureEncoding, SigningOptions, +}; use x509_cert::name::DistinguishedName; use x509_cert::serial_number::SerialNumber; use x509_cert::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; @@ -1412,6 +1414,7 @@ pub struct CertificateBuilder { pub is_ca: Option, pub include_common_exts: bool, pub extensions: Vec, + pub signature_hash: HashAlgorithm, } impl Default for CertificateBuilder { @@ -1426,6 +1429,7 @@ impl Default for CertificateBuilder { is_ca: None, include_common_exts: true, extensions: Vec::new(), + signature_hash: HashAlgorithm::Sha3_256, } } } @@ -1436,6 +1440,18 @@ impl CertificateBuilder { Self::default() } + /// Override the hash algorithm used for ECDSA certificate signatures. + /// + /// The default is SHA3-256, which stamps the `ecdsa-with-SHA3-256` + /// signature algorithm OID. SHA2-256 stamps `ecdsa-with-SHA256`. + /// Other algorithms are rejected at build time. + /// + /// Ignored for Ed25519 signers, which sign the raw TBS bytes. + pub fn with_signature_hash(mut self, hash: HashAlgorithm) -> Self { + self.signature_hash = hash; + self + } + /// Set the subject's public key information. /// /// This is the public key that will be certified by this certificate. @@ -2275,7 +2291,8 @@ impl CertificateBuilder { /// /// The method automatically selects the appropriate signature algorithm: /// - **Ed25519**: Pure Ed25519 signatures (no hashing) - /// - **ECDSA (secp256k1/secp256r1)**: ECDSA with SHA-256 + /// - **ECDSA (secp256k1/secp256r1)**: ECDSA with SHA3-256 by default; + /// override via [`CertificateBuilder::with_signature_hash`] /// /// # Returns /// @@ -2345,8 +2362,13 @@ impl CertificateBuilder { let algorithm = signer.to_algorithm(); let signature_algorithm_oid = match algorithm { Algorithm::Ed25519 => oids::ED25519, - Algorithm::Secp256k1 => oids::ECDSA_WITH_SHA256, - Algorithm::Secp256r1 => oids::ECDSA_WITH_SHA256, + Algorithm::Secp256k1 | Algorithm::Secp256r1 => match self.signature_hash { + HashAlgorithm::Sha3_256 => oids::ECDSA_WITH_SHA3_256, + HashAlgorithm::Sha2_256 => oids::ECDSA_WITH_SHA256, + HashAlgorithm::Sha2_512 => { + return Err(CertificateError::UnsupportedSignatureHash { hash: self.signature_hash }) + } + }, }; // Build the TBS certificate with the correct signature algorithm @@ -2358,15 +2380,16 @@ impl CertificateBuilder { // Serialize the TBS certificate for signing let tbs_der = Vec::::try_from(&tbs_certificate)?; - // Determine signing options based on algorithm - let signing_options = match algorithm { - Algorithm::Ed25519 => SigningOptions::raw(), - Algorithm::Secp256k1 | Algorithm::Secp256r1 => SigningOptions::for_cert(), + // Ed25519 signs the raw TBS bytes; ECDSA signs a pre-hash of the TBS + // using the hash declared by the signature algorithm OID + let to_sign = match algorithm { + Algorithm::Ed25519 => tbs_der, + Algorithm::Secp256k1 | Algorithm::Secp256r1 => self.signature_hash.hash(&tbs_der), }; // Sign the TBS certificate let signature = signer - .sign_with_options(&tbs_der, signing_options) + .sign_with_options(&to_sign, SigningOptions::raw()) .map_err(|_| CertificateError::CertificateSignatureVerificationFailed)?; // Convert signature to bytes and encode properly for X.509 diff --git a/keetanetwork-x509/src/certificates.rs b/keetanetwork-x509/src/certificates.rs index 6cf5657..2f9d51a 100644 --- a/keetanetwork-x509/src/certificates.rs +++ b/keetanetwork-x509/src/certificates.rs @@ -1203,18 +1203,19 @@ impl Certificate { // Get public key bytes directly from the subject public key info let public_key_bytes = issuer_public_key.subject_public_key.raw_bytes(); - // Dispatch to appropriate verification function based on signature algorithm + // Dispatch to appropriate verification function based on signature + // algorithm; ECDSA pre-hashes the TBS with the OID-declared hash match cert_sig_oid.to_string().as_str() { oids::ED25519 => utils::verify_ed25519_signature(public_key_bytes, signature_bytes, &tbs_der), oids::ECDSA_WITH_SHA3_256 => { // For ECDSA, try both curves since the verification function handles curve detection - utils::verify_ecdsa_signature(public_key_bytes, signature_bytes, &tbs_der) + utils::verify_ecdsa_signature(public_key_bytes, signature_bytes, &tbs_der, HashAlgorithm::Sha3_256) } oids::ECDSA_WITH_SHA256 => { // For ECDSA, try both curves since the verification function handles curve detection - utils::verify_ecdsa_signature(public_key_bytes, signature_bytes, &tbs_der) + utils::verify_ecdsa_signature(public_key_bytes, signature_bytes, &tbs_der, HashAlgorithm::Sha2_256) } oids::SHA256_WITH_RSA => Err(CertificateError::InvalidCertificate), diff --git a/keetanetwork-x509/src/error.rs b/keetanetwork-x509/src/error.rs index c9247ee..d681913 100644 --- a/keetanetwork-x509/src/error.rs +++ b/keetanetwork-x509/src/error.rs @@ -1,6 +1,7 @@ use alloc::format; use alloc::string::String; +use keetanetwork_crypto::hash::HashAlgorithm; use keetanetwork_utils::{impl_error_from_with_fields, impl_variant_error_from}; use snafu::Snafu; @@ -35,6 +36,9 @@ pub enum CertificateError { /// Unsupported certificate version #[snafu(display("Unsupported certificate version: {version}"))] UnsupportedVersion { version: u32 }, + /// Unsupported hash algorithm for certificate signatures + #[snafu(display("Unsupported signature hash algorithm: {}", hash.name()))] + UnsupportedSignatureHash { hash: HashAlgorithm }, /// Certificate signature verification failed #[snafu(display("Certificate signature verification failed"))] CertificateSignatureVerificationFailed, @@ -96,6 +100,7 @@ mod tests { CertificateError::InvalidExtension { oid: "test".to_string() }, CertificateError::ChainValidationFailed { reason: "test".to_string() }, CertificateError::UnsupportedVersion { version: 1 }, + CertificateError::UnsupportedSignatureHash { hash: HashAlgorithm::Sha2_512 }, CertificateError::CertificateSignatureVerificationFailed, CertificateError::CertificateDuplicateIncluded, CertificateError::CertificateOrphanFound, diff --git a/keetanetwork-x509/src/utils.rs b/keetanetwork-x509/src/utils.rs index 73a93d8..8d96379 100644 --- a/keetanetwork-x509/src/utils.rs +++ b/keetanetwork-x509/src/utils.rs @@ -484,7 +484,8 @@ pub fn raw_to_der_signature(signature_bytes: &[u8]) -> Result, Certifica /// * `public_key` - The public key for verification /// * `signature_bytes` - Signature bytes (DER or raw format) /// * `tbs_der` - To-be-signed certificate data -/// * `hash_algorithm` - Hash algorithm for fallback verification +/// * `hash` - Hash algorithm declared by the certificate's signature +/// algorithm OID, used to pre-hash the TBS data /// * `sig_from_bytes` - Function to create signature from raw bytes /// /// # Returns @@ -496,6 +497,7 @@ fn try_verify_ecdsa_generic( public_key: K, signature_bytes: impl AsRef<[u8]>, tbs_der: impl AsRef<[u8]>, + hash: HashAlgorithm, sig_from_bytes: F, ) -> Result where @@ -503,17 +505,19 @@ where F: Fn(&[u8; 64]) -> Result, { let signature_bytes = signature_bytes.as_ref(); - let tbs_der = tbs_der.as_ref(); + + // Pre-hash the TBS data with the OID-declared hash, then verify the + // digest directly (raw mode) + let tbs_digest = hash.hash(tbs_der.as_ref()); + let options = SigningOptions::raw(); // Try DER-encoded signature first if signature_bytes.len() >= 2 && signature_bytes[0] == 0x30 { if let Ok(sig_array) = der_to_raw_signature(signature_bytes) { let signature = sig_from_bytes(&sig_array)?; - // For X.509 certificates, use for_cert option which matches the signing process - let options = SigningOptions::for_cert(); if public_key - .verify_with_options(tbs_der, &signature, options) + .verify_with_options(&tbs_digest, &signature, options) .is_ok() { return Ok(true); @@ -525,10 +529,8 @@ where let sig_array: [u8; 64] = signature_bytes.try_into().or_invalid_certificate()?; let signature = sig_from_bytes(&sig_array)?; - // For raw signatures, also use for_cert option to match signing - let options = SigningOptions::for_cert(); if public_key - .verify_with_options(tbs_der, &signature, options) + .verify_with_options(&tbs_digest, &signature, options) .is_ok() { return Ok(true); @@ -550,6 +552,8 @@ where /// * `public_key_bytes` - Raw bytes of the Secp256r1 public key /// * `signature_bytes` - Signature bytes (DER or raw format) /// * `tbs_der` - To-be-signed certificate data in DER format +/// * `hash` - Hash algorithm declared by the certificate's signature +/// algorithm OID /// /// # Returns /// @@ -560,6 +564,7 @@ where /// # Example /// /// ```rust,no_run +/// use keetanetwork_crypto::prelude::HashAlgorithm; /// use keetanetwork_x509::utils::try_verify_ecdsa_secp256r1; /// /// let public_key_bytes = &[/* 65 bytes of uncompressed public key */]; @@ -569,17 +574,19 @@ where /// let result = try_verify_ecdsa_secp256r1( /// public_key_bytes, /// signature_bytes, -/// tbs_data +/// tbs_data, +/// HashAlgorithm::Sha3_256 /// ); /// ``` pub fn try_verify_ecdsa_secp256r1( public_key_bytes: impl AsRef<[u8]>, signature_bytes: impl AsRef<[u8]>, tbs_der: impl AsRef<[u8]>, + hash: HashAlgorithm, ) -> Result { let public_key = Secp256r1PublicKey::try_from(public_key_bytes.as_ref()).or_invalid_certificate()?; - try_verify_ecdsa_generic(public_key, signature_bytes, tbs_der, |sig_array| { + try_verify_ecdsa_generic(public_key, signature_bytes, tbs_der, hash, |sig_array| { Secp256r1Signature::from_bytes((sig_array).into()).or_invalid_certificate() }) } @@ -596,6 +603,8 @@ pub fn try_verify_ecdsa_secp256r1( /// * `public_key_bytes` - Raw bytes of the Secp256k1 public key /// * `signature_bytes` - Signature bytes (DER or raw format) /// * `tbs_der` - To-be-signed certificate data in DER format +/// * `hash` - Hash algorithm declared by the certificate's signature +/// algorithm OID /// /// # Returns /// @@ -606,6 +615,7 @@ pub fn try_verify_ecdsa_secp256r1( /// # Example /// /// ```rust,no_run +/// use keetanetwork_crypto::prelude::HashAlgorithm; /// use keetanetwork_x509::utils::try_verify_ecdsa_secp256k1; /// /// let public_key_bytes = &[/* 65 bytes of uncompressed public key */]; @@ -616,16 +626,18 @@ pub fn try_verify_ecdsa_secp256r1( /// public_key_bytes, /// signature_bytes, /// tbs_data, +/// HashAlgorithm::Sha3_256, /// ); /// ``` pub fn try_verify_ecdsa_secp256k1( public_key_bytes: impl AsRef<[u8]>, signature_bytes: impl AsRef<[u8]>, tbs_der: impl AsRef<[u8]>, + hash: HashAlgorithm, ) -> Result { let public_key = Secp256k1PublicKey::try_from(public_key_bytes.as_ref()).or_invalid_certificate()?; - try_verify_ecdsa_generic(public_key, signature_bytes, tbs_der, |sig_array| { + try_verify_ecdsa_generic(public_key, signature_bytes, tbs_der, hash, |sig_array| { Secp256k1Signature::from_bytes((sig_array).into()).or_invalid_certificate() }) } @@ -699,6 +711,8 @@ pub fn verify_ed25519_signature( /// * `public_key_bytes` - Raw bytes of the ECDSA public key /// * `signature_bytes` - Signature bytes (DER or raw format) /// * `tbs_der` - To-be-signed certificate data in DER format +/// * `hash` - Hash algorithm declared by the certificate's signature +/// algorithm OID /// /// # Returns /// @@ -709,6 +723,7 @@ pub fn verify_ed25519_signature( /// # Example /// /// ```rust,no_run +/// use keetanetwork_crypto::prelude::HashAlgorithm; /// use keetanetwork_x509::utils::verify_ecdsa_signature; /// /// // This example shows the function signature but does not run @@ -720,23 +735,25 @@ pub fn verify_ed25519_signature( /// let result = verify_ecdsa_signature( /// public_key_bytes, /// signature_bytes, -/// tbs_data +/// tbs_data, +/// HashAlgorithm::Sha3_256 /// ); /// ``` pub fn verify_ecdsa_signature( public_key_bytes: impl AsRef<[u8]>, signature_bytes: impl AsRef<[u8]>, tbs_der: impl AsRef<[u8]>, + hash: HashAlgorithm, ) -> Result { // Try Secp256r1 first (more common in X.509) - if let Ok(result) = try_verify_ecdsa_secp256r1(&public_key_bytes, &signature_bytes, &tbs_der) { + if let Ok(result) = try_verify_ecdsa_secp256r1(&public_key_bytes, &signature_bytes, &tbs_der, hash) { if result { return Ok(true); } } // Try Secp256k1 if Secp256r1 failed - try_verify_ecdsa_secp256k1(public_key_bytes, signature_bytes, tbs_der) + try_verify_ecdsa_secp256k1(public_key_bytes, signature_bytes, tbs_der, hash) } #[cfg(test)] @@ -1292,7 +1309,7 @@ mod tests { ($curve_fn:ident) => { for (pub_key_len, signature_bytes, should_error) in &test_cases { let public_key_bytes = vec![0u8; *pub_key_len]; - let result = $curve_fn(&public_key_bytes, signature_bytes, tbs_der); + let result = $curve_fn(&public_key_bytes, signature_bytes, tbs_der, HashAlgorithm::Sha3_256); if *should_error { assert!(result.is_err() || matches!(result, Ok(false))); @@ -1314,11 +1331,11 @@ mod tests { let signature_bytes = vec![0u8; 64]; let tbs_der = b"test data to sign"; - let result = verify_ecdsa_signature(&public_key_bytes, &signature_bytes, tbs_der); + let result = verify_ecdsa_signature(&public_key_bytes, &signature_bytes, tbs_der, HashAlgorithm::Sha3_256); assert!(result.is_err()); let public_key_bytes = vec![0u8; 16]; - let result = verify_ecdsa_signature(&public_key_bytes, &signature_bytes, tbs_der); + let result = verify_ecdsa_signature(&public_key_bytes, &signature_bytes, tbs_der, HashAlgorithm::Sha3_256); assert!(result.is_err()); } @@ -1343,7 +1360,8 @@ mod tests { ]; for signature_bytes in test_cases { - let result = try_verify_ecdsa_secp256r1(&public_key_bytes, &signature_bytes, tbs_der); + let result = + try_verify_ecdsa_secp256r1(&public_key_bytes, &signature_bytes, tbs_der, HashAlgorithm::Sha3_256); assert!(result.is_err()); } } diff --git a/keetanetwork-x509/tests/builders.rs b/keetanetwork-x509/tests/builders.rs index fd33b5c..798d451 100644 --- a/keetanetwork-x509/tests/builders.rs +++ b/keetanetwork-x509/tests/builders.rs @@ -7,9 +7,10 @@ use keetanetwork_asn1::{BitString, SubjectPublicKeyInfo}; use keetanetwork_crypto::algorithms::secp256k1::Secp256k1Derivation; use keetanetwork_crypto::algorithms::secp256r1::Secp256r1Derivation; use keetanetwork_crypto::prelude::Algorithm; -use keetanetwork_crypto::prelude::{ExposeSecret, IntoSecret, KeyDerivation}; +use keetanetwork_crypto::prelude::{ExposeSecret, HashAlgorithm, IntoSecret, KeyDerivation}; use keetanetwork_crypto::utils::generate_random_seed; use keetanetwork_x509::certificates::*; +use keetanetwork_x509::error::CertificateError; use keetanetwork_x509::{oids, utils}; use keetanetwork_x509::{SerialNumber, SubjectPublicKeyInfoOwned, Version}; @@ -277,3 +278,73 @@ fn test_ecdsa_signature_der_encoding() -> Result<(), Box test_ecdsa_curve!("secp256r1", KeyECDSASECP256R1, Secp256r1Derivation); Ok(()) } + +#[test] +fn test_ecdsa_signature_hash_selection() -> Result<(), Box> { + // Macro to eliminate code duplication for testing different ECDSA curves + macro_rules! test_hash_selection { + ($curve_name:expr, $key_type:ty, $derivation:ty) => {{ + let seed = generate_random_seed()?; + let private_key = <$derivation>::derive_from_seed(seed.expose_secret().clone().into_secret())?; + let account = Account::<$key_type>::from(private_key); + let public_key = account.keypair.to_public_key(); + let public_key_info = SubjectPublicKeyInfo::from(public_key); + + let cn = format!("Test Certificate {}", $curve_name); + let subject_dn = utils::create_dn(&[(oids::CN, &cn)])?; + + let builder = keetanetwork_x509::builder::CertificateBuilder::new() + .with_subject_public_key(public_key_info.clone()) + .with_subject_dn(subject_dn.clone()) + .with_issuer_dn(subject_dn) + .with_serial_number(SerialNumber::from(1u64)) + .with_validity_days(365); + + // Default hash is SHA3-256, matching the TypeScript implementation + let default_cert = builder.clone().build(&account)?; + assert_eq!( + default_cert.signature_algorithm.oid.to_string(), + oids::ECDSA_WITH_SHA3_256, + "{} default signature algorithm OID should be ecdsa-with-SHA3-256", + $curve_name + ); + assert!( + default_cert.verify_signature(&public_key_info)?, + "{} SHA3-256 signed certificate should verify", + $curve_name + ); + + // SHA2-256 override stamps the matching OID and still verifies + let sha2_cert = builder + .clone() + .with_signature_hash(HashAlgorithm::Sha2_256) + .build(&account)?; + assert_eq!( + sha2_cert.signature_algorithm.oid.to_string(), + oids::ECDSA_WITH_SHA256, + "{} SHA2-256 override should stamp ecdsa-with-SHA256 OID", + $curve_name + ); + assert!( + sha2_cert.verify_signature(&public_key_info)?, + "{} SHA2-256 signed certificate should verify", + $curve_name + ); + + // Unsupported hash algorithms are rejected at build time + let sha512_result = builder + .clone() + .with_signature_hash(HashAlgorithm::Sha2_512) + .build(&account); + assert!( + matches!(sha512_result, Err(CertificateError::UnsupportedSignatureHash { .. })), + "{} SHA2-512 signature hash should be rejected", + $curve_name + ); + }}; + } + + test_hash_selection!("secp256k1", KeyECDSASECP256K1, Secp256k1Derivation); + test_hash_selection!("secp256r1", KeyECDSASECP256R1, Secp256r1Derivation); + Ok(()) +} From 580050ba44ddcea3a699c8b5ad2b7668056976aa Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Thu, 2 Jul 2026 01:13:55 -0700 Subject: [PATCH 2/4] chore(release): update versions --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- keetanetwork-crypto/Cargo.toml | 2 +- keetanetwork-x509/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eec60b9..74c071c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1380,7 +1380,7 @@ dependencies = [ [[package]] name = "keetanetwork-crypto" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aead", "aes", @@ -1476,7 +1476,7 @@ dependencies = [ [[package]] name = "keetanetwork-x509" -version = "0.2.1" +version = "0.3.0" dependencies = [ "base64", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 89e61fb..be2f6d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,7 @@ path = "keetanetwork-utils" default-features = false [workspace.dependencies.keetanetwork-crypto] -version = "0.2.0" +version = "0.3.0" path = "keetanetwork-crypto" default-features = false @@ -159,7 +159,7 @@ path = "keetanetwork-account" default-features = false [workspace.dependencies.keetanetwork-x509] -version = "0.2.1" +version = "0.3.0" path = "keetanetwork-x509" default-features = false diff --git a/keetanetwork-crypto/Cargo.toml b/keetanetwork-crypto/Cargo.toml index 17c9ab0..03ca643 100644 --- a/keetanetwork-crypto/Cargo.toml +++ b/keetanetwork-crypto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-crypto" -version.workspace = true +version = "0.3.0" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/keetanetwork-x509/Cargo.toml b/keetanetwork-x509/Cargo.toml index 66e071d..9deb3f2 100644 --- a/keetanetwork-x509/Cargo.toml +++ b/keetanetwork-x509/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-x509" -version = "0.2.1" +version = "0.3.0" edition.workspace = true authors.workspace = true license.workspace = true From 546686ed0e0245927b39704290c620acf77f8852 Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Thu, 2 Jul 2026 01:15:05 -0700 Subject: [PATCH 3/4] chore(release): update crate --- Cargo.lock | 2 +- Cargo.toml | 2 +- keetanetwork-bindings/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74c071c..cd44232 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1268,7 +1268,7 @@ dependencies = [ [[package]] name = "keetanetwork-bindings" -version = "0.3.0" +version = "0.3.1" dependencies = [ "chrono", "hex", diff --git a/Cargo.toml b/Cargo.toml index be2f6d8..679bf65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,7 +184,7 @@ path = "keetanetwork-client" default-features = false [workspace.dependencies.keetanetwork-bindings] -version = "0.3.0" +version = "0.3.1" path = "keetanetwork-bindings" default-features = false diff --git a/keetanetwork-bindings/Cargo.toml b/keetanetwork-bindings/Cargo.toml index 04ea5e8..e3dc7e2 100644 --- a/keetanetwork-bindings/Cargo.toml +++ b/keetanetwork-bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-bindings" -version = "0.3.0" +version = "0.3.1" edition.workspace = true authors.workspace = true license.workspace = true From e2df6deb2a72f49b4c133cbe5b6b870b6c722356 Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Thu, 2 Jul 2026 01:32:04 -0700 Subject: [PATCH 4/4] chore: cleanup --- .../src/algorithms/secp256k1.rs | 34 ++----------- .../src/algorithms/secp256r1.rs | 34 ++----------- .../src/operations/signature.rs | 48 +++++++++++++++++++ sonar-project.properties | 2 +- 4 files changed, 57 insertions(+), 61 deletions(-) diff --git a/keetanetwork-crypto/src/algorithms/secp256k1.rs b/keetanetwork-crypto/src/algorithms/secp256k1.rs index 622d08d..0d00646 100644 --- a/keetanetwork-crypto/src/algorithms/secp256k1.rs +++ b/keetanetwork-crypto/src/algorithms/secp256k1.rs @@ -38,8 +38,6 @@ use crate::operations::encryption::{AsymmetricEncryption, KeyExchange, KeyGenera #[cfg(feature = "encryption")] use crate::utils::generate_random_seed; -#[cfg(feature = "signature")] -use crate::hash::hash_default; #[cfg(feature = "signature")] use crate::operations::signature::{ CryptoSigner, CryptoSignerWithOptions, CryptoVerifier, CryptoVerifierWithOptions, SigningOptions, @@ -223,22 +221,10 @@ impl CryptoSignerWithOptions for Secp256k1PrivateKey { message: T, options: SigningOptions, ) -> Result { - let message = message.as_ref(); let signing_key = SigningKey::from(&self.inner); - if options.raw { - // For raw signing, treat the message as a pre-computed hash - // and use prehash signing to avoid double hashing - if message.len() != 32 { - return Err(::signature::Error::new()); - } - - signing_key.sign_prehash(message) - } else { - // For regular signing, use the default hash algorithm - let data = hash_default(message).to_vec(); - signing_key.sign_prehash(&data) - } + let digest = options.digest(message.as_ref())?; + signing_key.sign_prehash(&digest) } } @@ -335,22 +321,10 @@ impl CryptoVerifierWithOptions for Secp256k1PublicKey { signature: &Signature, options: SigningOptions, ) -> Result<(), ::signature::Error> { - let message = message.as_ref(); let verifying_key = VerifyingKey::from(&self.inner); - if options.raw { - // For raw verification, treat the message as a pre-computed hash - // and use prehash verification to avoid double hashing - if message.len() != 32 { - return Err(::signature::Error::new()); - } - - verifying_key.verify_prehash(message, signature) - } else { - // For regular verification, use the default hash algorithm - let data = hash_default(message).to_vec(); - verifying_key.verify_prehash(&data, signature) - } + let digest = options.digest(message.as_ref())?; + verifying_key.verify_prehash(&digest, signature) } } diff --git a/keetanetwork-crypto/src/algorithms/secp256r1.rs b/keetanetwork-crypto/src/algorithms/secp256r1.rs index 6052de2..0c294a5 100644 --- a/keetanetwork-crypto/src/algorithms/secp256r1.rs +++ b/keetanetwork-crypto/src/algorithms/secp256r1.rs @@ -31,8 +31,6 @@ use aead::KeyInit; #[cfg(feature = "encryption")] use p256::ecdh::diffie_hellman; -#[cfg(feature = "signature")] -use crate::hash::hash_default; #[cfg(feature = "signature")] use crate::operations::signature::{ CryptoSigner, CryptoSignerWithOptions, CryptoVerifier, CryptoVerifierWithOptions, SigningOptions, @@ -224,22 +222,10 @@ impl CryptoSignerWithOptions for Secp256r1PrivateKey { message: T, options: SigningOptions, ) -> Result { - let message = message.as_ref(); let signing_key = SigningKey::from(&self.inner); - if options.raw { - // For raw signing, treat the message as a pre-computed hash - // and use prehash signing to avoid double hashing - if message.len() != 32 { - return Err(::signature::Error::new()); - } - - signing_key.sign_prehash(message) - } else { - // For regular signing, use the default hash algorithm (SHA3-256) - let data = hash_default(message).to_vec(); - signing_key.sign_prehash(&data) - } + let digest = options.digest(message.as_ref())?; + signing_key.sign_prehash(&digest) } } @@ -335,22 +321,10 @@ impl CryptoVerifierWithOptions for Secp256r1PublicKey { signature: &Signature, options: SigningOptions, ) -> Result<(), ::signature::Error> { - let message = message.as_ref(); let verifying_key = VerifyingKey::from(&self.inner); - if options.raw { - // For raw verification, treat the message as a pre-computed hash - // and use prehash verification to avoid double hashing - if message.len() != 32 { - return Err(::signature::Error::new()); - } - - verifying_key.verify_prehash(message, signature) - } else { - // For regular verification, use the default hash algorithm - let data = hash_default(message).to_vec(); - verifying_key.verify_prehash(&data, signature) - } + let digest = options.digest(message.as_ref())?; + verifying_key.verify_prehash(&digest, signature) } } diff --git a/keetanetwork-crypto/src/operations/signature.rs b/keetanetwork-crypto/src/operations/signature.rs index 9f792aa..ddc2935 100644 --- a/keetanetwork-crypto/src/operations/signature.rs +++ b/keetanetwork-crypto/src/operations/signature.rs @@ -3,9 +3,11 @@ //! This module provides idiomatic traits and types for cryptographic signing //! and verification operations, leveraging the RustCrypto ecosystem. +use alloc::borrow::Cow; use alloc::vec::Vec; use crate::algorithms::CryptoAlgorithm; +use crate::hash::hash_default; // Re-export key RustCrypto signature traits for easier use pub use signature::{ @@ -66,6 +68,23 @@ impl SigningOptions { pub fn raw() -> Self { Self { raw: true } } + + /// Resolve `message` into the digest to sign or verify. + /// + /// - raw: `message` must already be a 32-byte digest (borrowed as-is) + /// - otherwise: pre-hash with the network default hash (SHA3-256) + pub fn digest<'a>(&self, message: &'a [u8]) -> Result, SignatureError> { + if self.raw { + if message.len() != 32 { + return Err(SignatureError::new()); + } + + return Ok(Cow::Borrowed(message)); + } + + let digest = hash_default(message); + Ok(Cow::Owned(digest.to_vec())) + } } /// Extended signing operations with configurable options. @@ -90,3 +109,32 @@ pub trait CryptoVerifierWithOptions: CryptoVerifier { options: SigningOptions, ) -> Result<(), SignatureError>; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_digest_raw_borrows_precomputed_hash() -> Result<(), SignatureError> { + let precomputed = [0x42u8; 32]; + let digest = SigningOptions::raw().digest(&precomputed)?; + assert!(matches!(digest, Cow::Borrowed(_))); + assert_eq!(digest.as_ref(), precomputed); + Ok(()) + } + + #[test] + fn test_digest_raw_rejects_non_digest_length() { + assert!(SigningOptions::raw().digest(b"too short").is_err()); + assert!(SigningOptions::raw().digest(&[0u8; 64]).is_err()); + } + + #[test] + fn test_digest_default_prehashes_with_network_default() -> Result<(), SignatureError> { + let message = b"message to hash"; + let digest = SigningOptions::default().digest(message)?; + assert!(matches!(digest, Cow::Owned(_))); + assert_eq!(digest.as_ref(), hash_default(message)); + Ok(()) + } +} diff --git a/sonar-project.properties b/sonar-project.properties index f5fae3c..1d06214 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -9,7 +9,7 @@ sonar.organization=keetapay # Source code settings # sonar.sources=. sonar.sources=. -sonar.exclusions=target/**,**/*.md,**/*.toml,**/*.yml,**/*.yaml,**/*.json,**/*.java +sonar.exclusions=target/**,scripts/**,**/*.md,**/*.toml,**/*.yml,**/*.yaml,**/*.json,**/*.java # Rust specific settings sonar.rust.lcov.reportPaths=coverage.lcov