diff --git a/native/rust/cose_openssl/src/cose.rs b/native/rust/cose_openssl/src/cose.rs index 5ea1ce31..d0bf5250 100644 --- a/native/rust/cose_openssl/src/cose.rs +++ b/native/rust/cose_openssl/src/cose.rs @@ -208,7 +208,10 @@ mod tests { let alg = phdr_with_alg.map_at_int(COSE_HEADER_ALG).unwrap(); assert_eq!(alg, &CborValue::Int(cose_alg(&key).unwrap())); - assert!(insert_alg_value(&key, phdr_with_alg).is_err()); + assert_eq!( + insert_alg_value(&key, phdr_with_alg).unwrap_err(), + "Algorithm already set in protected header" + ); } #[test] @@ -263,7 +266,10 @@ mod tests { #[test] fn cose_verify1_wrong_alg() { let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); - assert!(cose_verify1(&key, -35, b"", b"", b"").is_err()); + assert_eq!( + cose_verify1(&key, -35, b"", b"", b"").unwrap_err(), + "Algorithm mismatch between supplied alg and key" + ); } #[test] @@ -463,6 +469,97 @@ mod tests { ); } + // --------------------------------------------------------------- + // Negative tests: error propagation through cose_sign1/cose_verify1 + // --------------------------------------------------------------- + + #[test] + fn cose_sign1_rejects_non_map_phdr() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + assert_eq!( + cose_sign1( + &key, + CborValue::Int(0), + CborValue::Map(vec![]), + b"msg", + false + ) + .unwrap_err(), + "Protected header is not a CBOR map" + ); + } + + #[test] + fn cose_sign1_rejects_duplicate_alg() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let phdr = CborValue::Map(vec![( + CborValue::Int(COSE_HEADER_ALG), + CborValue::Int(-7), + )]); + assert_eq!( + cose_sign1(&key, phdr, CborValue::Map(vec![]), b"msg", false) + .unwrap_err(), + "Algorithm already set in protected header" + ); + } + + #[test] + fn cose_sign1_propagates_ossl_sign_error() { + // Null key triggers EVP_DigestSignInit failure. + let null_key = EvpKey { + key: std::ptr::null_mut(), + typ: KeyType::EC(WhichEC::P256), + }; + let err = cose_sign1( + &null_key, + CborValue::Map(vec![]), + CborValue::Map(vec![]), + b"msg", + false, + ) + .unwrap_err(); + assert!( + err.starts_with("EVP_DigestSignInit returned 0: error:"), + "unexpected error: {err}" + ); + } + + #[test] + fn cose_verify1_wrong_rsa_alg() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + assert_eq!( + cose_verify1(&key, -7, b"", b"", b"").unwrap_err(), + "-7 is not a COSE RSA-PSS algorithm" + ); + } + + #[test] + fn cose_verify1_ec_sig_wrong_length() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + // P-256 expects 64 byte fixed sig, pass 3 bytes. + assert_eq!( + cose_verify1(&pub_key, -7, b"", b"", &[0u8; 3]).unwrap_err(), + "Expected 64 byte ECDSA signature, got 3" + ); + } + + #[test] + fn cose_verify1_propagates_ossl_verify_error() { + // Null key triggers EVP_DigestVerifyInit failure. + let null_key = EvpKey { + key: std::ptr::null_mut(), + typ: KeyType::RSA(WhichRSA::PS256), + }; + let err = + cose_verify1(&null_key, -37, b"", b"", &[0u8; 256]).unwrap_err(); + assert!( + err.starts_with("EVP_DigestVerifyInit returned 0: error:"), + "unexpected error: {err}" + ); + } + #[cfg(feature = "pqc")] mod pqc_tests { use super::*; diff --git a/native/rust/cose_openssl/src/ossl_wrappers.rs b/native/rust/cose_openssl/src/ossl_wrappers.rs index 846b51d0..fb7f6b2c 100644 --- a/native/rust/cose_openssl/src/ossl_wrappers.rs +++ b/native/rust/cose_openssl/src/ossl_wrappers.rs @@ -16,6 +16,38 @@ unsafe extern "C" { name_sz: usize, gname_len: *mut usize, ) -> std::ffi::c_int; + + fn ERR_error_string_n( + e: std::ffi::c_ulong, + buf: *mut std::ffi::c_char, + len: usize, + ); +} + +/// Drain the OpenSSL error queue and return the last (newest) error. +pub(crate) fn ossl_err_string() -> String { + unsafe { + let mut last_code: std::ffi::c_ulong = 0; + loop { + let code = ossl::ERR_get_error(); + if code == 0 { + break; + } + last_code = code; + } + if last_code == 0 { + return "(no OpenSSL error)".to_string(); + } + let mut buf = [0u8; 256]; + ERR_error_string_n( + last_code, + buf.as_mut_ptr() as *mut std::ffi::c_char, + buf.len(), + ); + std::ffi::CStr::from_ptr(buf.as_ptr() as *const std::ffi::c_char) + .to_string_lossy() + .into_owned() + } } #[cfg(feature = "pqc")] @@ -131,7 +163,10 @@ impl EvpKey { }; if key.is_null() { - return Err("Failed to create signing key".to_string()); + return Err(format!( + "EVP_PKEY_Q_keygen failed: {}", + ossl_err_string() + )); } Ok(EvpKey { key, typ }) @@ -146,7 +181,10 @@ impl EvpKey { let key = ossl::d2i_PUBKEY(ptr::null_mut(), &mut ptr, der.len() as i64); if key.is_null() { - return Err("Failed to parse DER public key".to_string()); + return Err(format!( + "d2i_PUBKEY failed: {}", + ossl_err_string() + )); } key }; @@ -176,7 +214,10 @@ impl EvpKey { der.len() as i64, ); if key.is_null() { - return Err("Failed to parse DER private key".to_string()); + return Err(format!( + "d2i_AutoPrivateKey failed: {}", + ossl_err_string() + )); } key }; @@ -220,7 +261,10 @@ impl EvpKey { &mut len, ) != 1 { - return Err("Failed to get EC group name".to_string()); + return Err(format!( + "EVP_PKEY_get_group_name failed: {}", + ossl_err_string() + )); } let group = std::str::from_utf8(&buf[..len]) .map_err(|_| "EC group name is not UTF-8".to_string())?; @@ -253,8 +297,9 @@ impl EvpKey { if len <= 0 || der_ptr.is_null() { return Err(format!( - "Failed to encode public key to DER (rc={})", - len + "i2d_PUBKEY returned {}: {}", + len, + ossl_err_string() )); } @@ -279,8 +324,9 @@ impl EvpKey { if len <= 0 || der_ptr.is_null() { return Err(format!( - "Failed to encode private key to DER (rc={})", - len + "i2d_PrivateKey returned {}: {}", + len, + ossl_err_string() )); } @@ -305,7 +351,10 @@ impl EvpKey { unsafe { let bits = ossl::EVP_PKEY_bits(self.key); if bits <= 0 { - return Err("EVP_PKEY_bits failed".to_string()); + return Err(format!( + "EVP_PKEY_bits failed: {}", + ossl_err_string() + )); } Ok(((bits + 7) / 8) as usize) } @@ -363,7 +412,7 @@ pub fn ecdsa_der_to_fixed( der.len() as std::ffi::c_long, ); if sig.is_null() { - return Err("Failed to parse DER ECDSA signature".to_string()); + return Err(format!("d2i_ECDSA_SIG failed: {}", ossl_err_string())); } let mut r: *const ossl::BIGNUM = ptr::null(); @@ -386,7 +435,7 @@ pub fn ecdsa_der_to_fixed( if rc_r != field_size as std::ffi::c_int || rc_s != field_size as std::ffi::c_int { - return Err("BN_bn2binpad failed for ECDSA r or s".to_string()); + return Err(format!("BN_bn2binpad failed: {}", ossl_err_string())); } Ok(fixed) @@ -413,7 +462,7 @@ pub fn ecdsa_fixed_to_der( ptr::null_mut(), ); if r.is_null() { - return Err("BN_bin2bn failed for ECDSA r".to_string()); + return Err(format!("BN_bin2bn failed (r): {}", ossl_err_string())); } let s = ossl::BN_bin2bn( @@ -423,21 +472,24 @@ pub fn ecdsa_fixed_to_der( ); if s.is_null() { ossl::BN_free(r); - return Err("BN_bin2bn failed for ECDSA s".to_string()); + return Err(format!("BN_bin2bn failed (s): {}", ossl_err_string())); } let sig = ossl::ECDSA_SIG_new(); if sig.is_null() { ossl::BN_free(r); ossl::BN_free(s); - return Err("ECDSA_SIG_new failed".to_string()); + return Err(format!("ECDSA_SIG_new failed: {}", ossl_err_string())); } if ossl::ECDSA_SIG_set0(sig, r, s) != 1 { ossl::ECDSA_SIG_free(sig); ossl::BN_free(r); ossl::BN_free(s); - return Err("ECDSA_SIG_set0 failed".to_string()); + return Err(format!( + "ECDSA_SIG_set0 failed: {}", + ossl_err_string() + )); } // ECDSA_SIG_set0 takes ownership of r and s on success. @@ -446,7 +498,7 @@ pub fn ecdsa_fixed_to_der( ossl::ECDSA_SIG_free(sig); if len <= 0 || out_ptr.is_null() { - return Err("i2d_ECDSA_SIG failed".to_string()); + return Err(format!("i2d_ECDSA_SIG failed: {}", ossl_err_string())); } let der = std::slice::from_raw_parts(out_ptr, len as usize).to_vec(); @@ -546,17 +598,18 @@ impl EvpMdContext { let ctx = ossl::EVP_MD_CTX_new(); if ctx.is_null() { return Err(format!( - "Failed to create ctx for: {}", - T::purpose() + "EVP_MD_CTX_new failed: {}", + ossl_err_string() )); } let mut pctx: *mut ossl::EVP_PKEY_CTX = ptr::null_mut(); if let Err(err) = T::init(ctx, md, key.key, &mut pctx) { ossl::EVP_MD_CTX_free(ctx); return Err(format!( - "Failed to init context for {} with err {}", + "EVP_Digest{}Init returned {}: {}", T::purpose(), - err + err, + ossl_err_string() )); } // For RSA keys, configure PSS padding. @@ -568,7 +621,10 @@ impl EvpMdContext { ) != 1 { ossl::EVP_MD_CTX_free(ctx); - return Err("Failed to set RSA PSS padding".into()); + return Err(format!( + "EVP_PKEY_CTX_set_rsa_padding failed: {}", + ossl_err_string() + )); } if ossl::EVP_PKEY_CTX_set_rsa_pss_saltlen( pctx, @@ -576,7 +632,10 @@ impl EvpMdContext { ) != 1 { ossl::EVP_MD_CTX_free(ctx); - return Err("Failed to set RSA PSS salt length".into()); + return Err(format!( + "EVP_PKEY_CTX_set_rsa_pss_saltlen failed: {}", + ossl_err_string() + )); } } Ok(EvpMdContext { @@ -698,12 +757,22 @@ mod tests { #[test] fn from_der_rejects_garbage() { - assert!(EvpKey::from_der_public(&[0xde, 0xad, 0xbe, 0xef]).is_err()); + let err = + EvpKey::from_der_public(&[0xde, 0xad, 0xbe, 0xef]).unwrap_err(); + assert!( + err.starts_with("d2i_PUBKEY failed: error:"), + "unexpected error: {err}" + ); } #[test] fn from_der_private_rejects_garbage() { - assert!(EvpKey::from_der_private(&[0xde, 0xad, 0xbe, 0xef]).is_err()); + let err = + EvpKey::from_der_private(&[0xde, 0xad, 0xbe, 0xef]).unwrap_err(); + assert!( + err.starts_with("d2i_AutoPrivateKey failed: error:"), + "unexpected error: {err}" + ); } #[test] @@ -775,4 +844,48 @@ mod tests { let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); std::mem::forget(key); } + + // --------------------------------------------------------------- + // Tests verifying exact OpenSSL errors in Err values + // --------------------------------------------------------------- + + #[test] + fn from_der_public_error_has_ossl_detail() { + let err = + EvpKey::from_der_public(&[0xde, 0xad, 0xbe, 0xef]).unwrap_err(); + assert!( + err.starts_with("d2i_PUBKEY failed: error:"), + "unexpected error: {err}" + ); + } + + #[test] + fn from_der_private_error_has_ossl_detail() { + let err = + EvpKey::from_der_private(&[0xde, 0xad, 0xbe, 0xef]).unwrap_err(); + assert!( + err.starts_with("d2i_AutoPrivateKey failed: error:"), + "unexpected error: {err}" + ); + } + + #[test] + fn ecdsa_der_to_fixed_error() { + assert_eq!( + ecdsa_der_to_fixed(&[0xff, 0xff], 32).unwrap_err(), + "d2i_ECDSA_SIG failed: (no OpenSSL error)", + ); + } + + #[test] + fn sign_with_public_only_key_error_has_ossl_detail() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + let err = crate::sign::sign(&pub_key, b"test message").unwrap_err(); + assert!( + err.starts_with("EVP_DigestSign returned 0: error:"), + "unexpected error: {err}" + ); + } } diff --git a/native/rust/cose_openssl/src/sign.rs b/native/rust/cose_openssl/src/sign.rs index 32020189..556fc426 100644 --- a/native/rust/cose_openssl/src/sign.rs +++ b/native/rust/cose_openssl/src/sign.rs @@ -1,4 +1,4 @@ -use crate::ossl_wrappers::{EvpKey, EvpMdContext, SignOp}; +use crate::ossl_wrappers::{EvpKey, EvpMdContext, SignOp, ossl_err_string}; use openssl_sys as ossl; use std::ptr; @@ -33,7 +33,11 @@ fn sign_with_ctx( msg.len(), ); if res != 1 { - return Err(format!("Failed to get signature size, err: {}", res)); + return Err(format!( + "EVP_DigestSign (get size) returned {}: {}", + res, + ossl_err_string() + )); } let mut sig = vec![0u8; sig_size]; @@ -45,7 +49,11 @@ fn sign_with_ctx( msg.len(), ); if res != 1 { - return Err(format!("Failed to sign, err: {}", res)); + return Err(format!( + "EVP_DigestSign returned {}: {}", + res, + ossl_err_string() + )); } // Not always fixed size, e.g. for EC keys. More on this here: @@ -55,3 +63,62 @@ fn sign_with_ctx( Ok(sig) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ossl_wrappers::{EvpKey, KeyType, WhichEC, WhichRSA}; + + #[test] + fn sign_ec_succeeds() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let sig = sign(&key, b"hello"); + assert!(sig.is_ok()); + assert!(!sig.unwrap().is_empty()); + } + + #[test] + fn sign_rsa_succeeds() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let sig = sign(&key, b"hello"); + assert!(sig.is_ok()); + assert!(!sig.unwrap().is_empty()); + } + + #[test] + fn sign_with_public_only_ec_key_fails_with_ossl_detail() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + let err = sign(&pub_key, b"hello").unwrap_err(); + assert!( + err.starts_with("EVP_DigestSign returned 0: error:"), + "unexpected error: {err}" + ); + } + + #[test] + fn sign_with_public_only_rsa_key_fails_with_ossl_detail() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + let err = sign(&pub_key, b"hello").unwrap_err(); + assert!( + err.starts_with("EVP_DigestSign returned 0: error:"), + "unexpected error: {err}" + ); + } + + #[test] + fn sign_context_init_error_propagates() { + let null_key = EvpKey { + key: std::ptr::null_mut(), + typ: KeyType::EC(WhichEC::P256), + }; + let err = sign(&null_key, b"hello").unwrap_err(); + assert!( + err.starts_with("EVP_DigestSignInit returned 0: error:"), + "unexpected error: {err}" + ); + } +} diff --git a/native/rust/cose_openssl/src/verify.rs b/native/rust/cose_openssl/src/verify.rs index 5cdafcdb..5a09e831 100644 --- a/native/rust/cose_openssl/src/verify.rs +++ b/native/rust/cose_openssl/src/verify.rs @@ -1,4 +1,4 @@ -use crate::ossl_wrappers::{EvpKey, EvpMdContext, VerifyOp}; +use crate::ossl_wrappers::{EvpKey, EvpMdContext, VerifyOp, ossl_err_string}; use openssl_sys as ossl; @@ -34,7 +34,97 @@ fn verify_with_ctx( match res { 1 => Ok(true), 0 => Ok(false), - err => Err(format!("Failed to verify signature, err: {}", err)), + err => Err(format!( + "EVP_DigestVerify returned {}: {}", + err, + ossl_err_string() + )), } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ossl_wrappers::{EvpKey, KeyType, WhichEC, WhichRSA}; + use crate::sign; + + #[test] + fn verify_ec_valid_signature() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let msg = b"test message"; + let sig = sign::sign(&key, msg).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + assert_eq!(verify(&pub_key, &sig, msg).unwrap(), true); + } + + #[test] + fn verify_ec_wrong_message_returns_false() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let sig = sign::sign(&key, b"correct").unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + assert_eq!(verify(&pub_key, &sig, b"wrong").unwrap(), false); + } + + #[test] + fn verify_rsa_valid_signature() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let msg = b"test message"; + let sig = sign::sign(&key, msg).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + assert_eq!(verify(&pub_key, &sig, msg).unwrap(), true); + } + + #[test] + fn verify_rsa_wrong_message_returns_false() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let sig = sign::sign(&key, b"correct").unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + assert_eq!(verify(&pub_key, &sig, b"wrong").unwrap(), false); + } + + #[test] + fn verify_ec_garbage_signature_returns_false_or_err() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + match verify(&pub_key, &[0xde, 0xad], b"msg") { + Ok(valid) => assert!(!valid, "Garbage signature must not verify"), + Err(err) => assert!( + err.starts_with("EVP_DigestVerify returned"), + "unexpected error: {err}" + ), + } + } + + #[test] + fn verify_rsa_garbage_signature_returns_false_or_err() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let pub_der = key.to_der_public().unwrap(); + let pub_key = EvpKey::from_der_public(&pub_der).unwrap(); + match verify(&pub_key, &[0xde, 0xad], b"msg") { + Ok(valid) => assert!(!valid, "Garbage signature must not verify"), + Err(err) => assert!( + err.starts_with("EVP_DigestVerify returned"), + "unexpected error: {err}" + ), + } + } + + #[test] + fn verify_context_init_error_propagates() { + let null_key = EvpKey { + key: std::ptr::null_mut(), + typ: KeyType::EC(WhichEC::P256), + }; + let err = verify(&null_key, &[0xde], b"hello").unwrap_err(); + assert!( + err.starts_with("EVP_DigestVerifyInit returned 0: error:"), + "unexpected error: {err}" + ); + } +}