From f961db5429e9cce30c53a9c7bb5edee202b22285 Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Thu, 28 May 2026 20:11:17 +0300 Subject: [PATCH 01/10] dtls: keep transient parse errors internal --- src/dtls12/client.rs | 145 ++++++++++++++++++-------------- src/dtls12/engine.rs | 8 +- src/dtls12/incoming.rs | 18 ++-- src/dtls12/message/handshake.rs | 6 +- src/dtls12/server.rs | 101 ++++++++++++---------- src/dtls13/client.rs | 119 ++++++++++++++------------ src/dtls13/engine.rs | 14 +-- src/dtls13/incoming.rs | 24 +++--- src/dtls13/message/handshake.rs | 10 +-- src/dtls13/server.rs | 111 +++++++++++++----------- src/error.rs | 78 ++++++++++++----- src/lib.rs | 1 + 12 files changed, 363 insertions(+), 272 deletions(-) diff --git a/src/dtls12/client.rs b/src/dtls12/client.rs index a39a3bff..e9741da0 100644 --- a/src/dtls12/client.rs +++ b/src/dtls12/client.rs @@ -29,7 +29,7 @@ use crate::dtls12::message::{CompressionMethod, ContentType, Cookie}; use crate::dtls12::message::{DigitallySigned, Dtls12CipherSuite}; use crate::dtls12::message::{ExtensionType, KeyExchangeAlgorithm, MessageType, ProtocolVersion}; use crate::dtls12::message::{Random, SessionId, SignatureAndHashAlgorithm, UseSrtpExtension}; -use crate::{Config, DtlsCertificate, Error, KeyingMaterial, Output}; +use crate::{Config, DtlsCertificate, Error, InternalError, KeyingMaterial, Output}; /// DTLS client pub struct Client { @@ -193,8 +193,7 @@ impl Client { .and_then(|_| self.make_progress()) { Ok(()) => Ok(()), - Err(e) if e.is_transient() => Ok(()), - Err(e) => Err(e), + Err(e) => e.into_public_error().map_or(Ok(()), Err), } } @@ -212,8 +211,10 @@ impl Client { self.random = Some(Random::new_with_time(now, &mut self.engine.rng)); } self.engine.handle_timeout(now)?; - self.make_progress()?; - Ok(()) + match self.make_progress() { + Ok(()) => Ok(()), + Err(e) => e.into_public_error().map_or(Ok(()), Err), + } } /// Send application data when the client is in the Running state @@ -259,7 +260,7 @@ impl Client { Ok(()) } - fn make_progress(&mut self) -> Result<(), Error> { + fn make_progress(&mut self) -> Result<(), InternalError> { loop { let prev_state = self.state; @@ -319,7 +320,7 @@ impl State { } } - fn make_progress(self, client: &mut Client) -> Result { + fn make_progress(self, client: &mut Client) -> Result { match self { State::SendClientHello => self.send_client_hello(client), State::AwaitHelloVerifyRequest => self.await_hello_verify_request(client), @@ -341,7 +342,7 @@ impl State { } } - fn send_client_hello(self, client: &mut Client) -> Result { + fn send_client_hello(self, client: &mut Client) -> Result { let session_id = client.session_id.unwrap_or_else(SessionId::empty); let cookie = client.cookie.unwrap_or_else(Cookie::empty); // unwrap: is ok because we set the random in handle_timeout @@ -373,7 +374,7 @@ impl State { } } - fn await_hello_verify_request(self, client: &mut Client) -> Result { + fn await_hello_verify_request(self, client: &mut Client) -> Result { let has_hello = client .engine .has_complete_handshake(MessageType::ServerHello); @@ -402,10 +403,11 @@ impl State { if h.server_version != ProtocolVersion::DTLS1_2 && h.server_version != ProtocolVersion::DTLS1_0 { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Unsupported DTLS version in HelloVerifyRequest: {:?}", h.server_version - ))); + ))) + .into()); } debug!( @@ -425,7 +427,7 @@ impl State { Ok(Self::SendClientHello) } - fn await_server_hello(self, client: &mut Client) -> Result { + fn await_server_hello(self, client: &mut Client) -> Result { let maybe = client .engine .next_handshake(MessageType::ServerHello, &mut client.defragment_buffer)?; @@ -446,26 +448,28 @@ impl State { // Enforce DTLS version if server_hello.server_version != ProtocolVersion::DTLS1_2 { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Unsupported DTLS version from server: {:?}", server_hello.server_version - ))); + ))) + .into()); } // Enforce Null compression only if server_hello.compression_method != CompressionMethod::Null { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Unsupported compression from server: {:?}", server_hello.compression_method - ))); + ))) + .into()); } // Enforce cipher suite is known and allowed let cs = server_hello.cipher_suite; if matches!(cs, Dtls12CipherSuite::Unknown(_)) { - return Err(Error::SecurityError( - "Server selected unknown cipher suite".to_string(), - )); + return Err( + (Error::SecurityError("Server selected unknown cipher suite".to_string())).into(), + ); } // Enforce cipher suite is compatible with our private key and allowed by config @@ -475,17 +479,19 @@ impl State { .is_cipher_suite_compatible(cs); if !is_compatible { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Server selected incompatible cipher suite: {:?}", cs - ))); + ))) + .into()); } if !client.engine.is_cipher_suite_allowed(cs) { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Server selected disallowed cipher suite: {:?}", cs - ))); + ))) + .into()); } // Note: we keep offered suites local; we don't enforce echo here @@ -497,14 +503,15 @@ impl State { // Check for use_srtp and extended_master_secret extensions let Some(extensions) = &server_hello.extensions else { - return Err(Error::IncompleteServerHello); + return Err((Error::IncompleteServerHello).into()); }; for extension in extensions { if extension.extension_type == ExtensionType::UseSrtp { // Parse the use_srtp extension to get the selected profile let extension_data = extension.extension_data(&client.defragment_buffer); - let (_, use_srtp) = UseSrtpExtension::parse(extension_data).map_err(Error::from)?; + let (_, use_srtp) = + UseSrtpExtension::parse(extension_data).map_err(InternalError::from)?; // Store the first profile as our negotiated profile if !use_srtp.profiles.is_empty() { client.negotiated_srtp_profile = Some(use_srtp.profiles[0].into()); @@ -525,9 +532,10 @@ impl State { // Without extended master secret, in DTLS1.2 a security attack // reusing the same master secret is possible. if !extended_master_secret { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "Extended Master Secret not negotiated".to_string(), - )); + )) + .into()); } if let Some(profile) = client.negotiated_srtp_profile { @@ -543,7 +551,7 @@ impl State { } } - fn await_certificate(self, client: &mut Client) -> Result { + fn await_certificate(self, client: &mut Client) -> Result { let maybe = client .engine .next_handshake(MessageType::Certificate, &mut client.defragment_buffer)?; @@ -558,9 +566,9 @@ impl State { }; if certificate.certificate_list.is_empty() { - return Err(Error::CertificateError( - "No server certificate received".to_string(), - )); + return Err( + (Error::CertificateError("No server certificate received".to_string())).into(), + ); } debug!( @@ -587,7 +595,7 @@ impl State { Ok(Self::AwaitServerKeyExchange) } - fn await_server_key_exchange(self, client: &mut Client) -> Result { + fn await_server_key_exchange(self, client: &mut Client) -> Result { let cipher_suite = client .engine .cipher_suite() @@ -600,7 +608,7 @@ impl State { } } - fn await_server_key_exchange_ecdhe(self, client: &mut Client) -> Result { + fn await_server_key_exchange_ecdhe(self, client: &mut Client) -> Result { let maybe = client.engine.next_handshake( MessageType::ServerKeyExchange, &mut client.defragment_buffer, @@ -620,9 +628,10 @@ impl State { let Some(d_signed) = server_key_exchange.signature() else { // We do not support anonymous key exchange - return Err(Error::UnexpectedMessage( + return Err((Error::UnexpectedMessage( "ServerKeyExchange without signature".to_string(), - )); + )) + .into()); }; let signature_range = d_signed.signature_range.clone(); @@ -636,9 +645,10 @@ impl State { ecdh.public_key_range.clone(), ), ServerKeyExchangeParams::Psk(_) => { - return Err(Error::UnexpectedMessage( + return Err((Error::UnexpectedMessage( "PSK ServerKeyExchange in ECDHE path".to_string(), - )); + )) + .into()); } }; @@ -680,18 +690,20 @@ impl State { // Ensure the server's (hash, signature) pair was offered by the client let offered = SignatureAndHashAlgorithm::supported().contains(&signature_algorithm); if !offered { - return Err(Error::CryptoError( + return Err((Error::CryptoError( "Signature algorithm not offered by client".to_string(), - )); + )) + .into()); } // Ensure the signature algorithm is compatible with the cipher suite if let Some(expected_sig) = cipher_suite.signature_algorithm() { if signature_algorithm.signature != expected_sig { - return Err(Error::CryptoError(format!( + return Err((Error::CryptoError(format!( "Signature algorithm mismatch: {:?} != {:?}", signature_algorithm.signature, expected_sig - ))); + ))) + .into()); } } @@ -737,7 +749,7 @@ impl State { /// PSK ServerKeyExchange carries only an optional identity hint (no signature). /// Per RFC 4279 §2, ServerKeyExchange is omitted when the server has no hint. - fn await_server_key_exchange_psk(self, client: &mut Client) -> Result { + fn await_server_key_exchange_psk(self, client: &mut Client) -> Result { // If the server skipped ServerKeyExchange (no hint), go straight to ServerHelloDone let has_done = client .engine @@ -764,9 +776,10 @@ impl State { let hint_range = match &ske.params { ServerKeyExchangeParams::Psk(psk) => psk.hint_range.clone(), _ => { - return Err(Error::UnexpectedMessage( + return Err((Error::UnexpectedMessage( "ECDHE ServerKeyExchange in PSK path".to_string(), - )); + )) + .into()); } }; @@ -780,7 +793,7 @@ impl State { Ok(Self::AwaitServerHelloDone) } - fn await_certificate_request(self, client: &mut Client) -> Result { + fn await_certificate_request(self, client: &mut Client) -> Result { let has_done = client .engine .has_complete_handshake(MessageType::ServerHelloDone); @@ -813,10 +826,11 @@ impl State { .unwrap(); if !cr.supports_hash_algorithm(hash_algorithm) { - return Err(Error::CertificateError(format!( + return Err((Error::CertificateError(format!( "Unsupported hash algorithm: {:?}", hash_algorithm - ))); + ))) + .into()); } debug!( @@ -830,7 +844,7 @@ impl State { Ok(Self::AwaitServerHelloDone) } - fn await_server_hello_done(self, client: &mut Client) -> Result { + fn await_server_hello_done(self, client: &mut Client) -> Result { let maybe = client .engine .next_handshake(MessageType::ServerHelloDone, &mut client.defragment_buffer)?; @@ -858,9 +872,9 @@ impl State { // Validate the server certificate if client.server_certificates.is_empty() { - return Err(Error::CertificateError( - "No server certificate received".to_string(), - )); + return Err( + (Error::CertificateError("No server certificate received".to_string())).into(), + ); } // Send the server certificate as an event @@ -875,7 +889,7 @@ impl State { } } - fn send_certificate(self, client: &mut Client) -> Result { + fn send_certificate(self, client: &mut Client) -> Result { debug!("Sending Certificate"); // Start/restart flight timer for client Flight 5 @@ -889,7 +903,7 @@ impl State { Ok(Self::SendClientKeyExchange) } - fn send_client_key_exchange(self, client: &mut Client) -> Result { + fn send_client_key_exchange(self, client: &mut Client) -> Result { trace!("Sending ClientKeyExchange"); // Start/restart flight timer only if this flight did not start with Certificate @@ -925,7 +939,7 @@ impl State { } } - fn send_certificate_verify(self, client: &mut Client) -> Result { + fn send_certificate_verify(self, client: &mut Client) -> Result { debug!("Sending CertificateVerify"); // Send the certificate verify message @@ -937,7 +951,7 @@ impl State { Ok(Self::SendChangeCipherSpec) } - fn send_change_cipher_spec(self, client: &mut Client) -> Result { + fn send_change_cipher_spec(self, client: &mut Client) -> Result { Self::derive_keys(client)?; // Send change cipher spec @@ -1019,7 +1033,7 @@ impl State { Ok(()) } - fn send_finished(self, client: &mut Client) -> Result { + fn send_finished(self, client: &mut Client) -> Result { trace!("Sending Finished message to complete handshake"); client @@ -1038,7 +1052,7 @@ impl State { Ok(Self::AwaitChangeCipherSpec) } - fn await_change_cipher_spec(self, client: &mut Client) -> Result { + fn await_change_cipher_spec(self, client: &mut Client) -> Result { let maybe = client.engine.next_record(ContentType::ChangeCipherSpec); let Some(_) = maybe else { @@ -1057,7 +1071,7 @@ impl State { Ok(Self::AwaitNewSessionTicket) } - fn await_new_session_ticket(self, client: &mut Client) -> Result { + fn await_new_session_ticket(self, client: &mut Client) -> Result { let has_finished = client.engine.has_complete_handshake(MessageType::Finished); if has_finished { @@ -1084,7 +1098,7 @@ impl State { Ok(Self::AwaitFinished) } - fn await_finished(self, client: &mut Client) -> Result { + fn await_finished(self, client: &mut Client) -> Result { // Generate expected verify data based on current transcript. // This must be done before next_handshake() below since // it should not include Finished itself. @@ -1123,9 +1137,9 @@ impl State { // Use constant-time comparison to prevent timing attacks let is_eq: bool = verify_data.ct_eq(expected.as_slice()).into(); if !is_eq { - return Err(Error::SecurityError( - "Server Finished verification failed".to_string(), - )); + return Err( + (Error::SecurityError("Server Finished verification failed".to_string())).into(), + ); } trace!("Server Finished verified successfully"); @@ -1175,7 +1189,7 @@ impl State { Ok(Self::AwaitApplicationData) } - fn await_application_data(self, client: &mut Client) -> Result { + fn await_application_data(self, client: &mut Client) -> Result { if client.engine.close_notify_received() { // RFC 5246 §7.2.1: respond with a reciprocal close_notify and // close down immediately, discarding any pending writes. @@ -1431,7 +1445,10 @@ mod tests { .await_certificate(&mut client) .expect_err("empty server Certificate should fail"); - assert!(matches!(err, Error::CertificateError(_))); + assert!(matches!( + err, + crate::InternalError::Fatal(Error::CertificateError(_)) + )); } #[test] diff --git a/src/dtls12/engine.rs b/src/dtls12/engine.rs index 82a3d960..0fdbadc3 100644 --- a/src/dtls12/engine.rs +++ b/src/dtls12/engine.rs @@ -12,7 +12,7 @@ use crate::dtls12::message::{Body, HashAlgorithm, Header, MessageType, ProtocolV use crate::dtls12::message::{ContentType, DTLSRecord, Dtls12CipherSuite, Handshake}; use crate::timer::ExponentialBackoff; use crate::window::ReplayWindow; -use crate::{Config, Error, Output, SeededRng}; +use crate::{Config, Error, InternalError, Output, SeededRng}; const MAX_DEFRAGMENT_PACKETS: usize = 50; @@ -218,7 +218,7 @@ impl Engine { &mut self.crypto_context } - pub fn parse_packet(&mut self, packet: &[u8]) -> Result<(), Error> { + pub fn parse_packet(&mut self, packet: &[u8]) -> Result<(), InternalError> { let cs = self.cipher_suite; let incoming = Incoming::parse_packet(packet, self, cs)?; if let Some(incoming) = incoming { @@ -644,7 +644,7 @@ impl Engine { &mut self, wanted: MessageType, defragment_buffer: &mut Buf, - ) -> Result, Error> { + ) -> Result, InternalError> { if !self.has_complete_handshake(wanted) { return Ok(None); } @@ -1130,7 +1130,7 @@ impl Engine { self.cipher_suite = Some(cipher_suite); } - pub fn enable_peer_encryption(&mut self) -> Result<(), Error> { + pub fn enable_peer_encryption(&mut self) -> Result<(), InternalError> { debug!("Peer encryption enabled"); self.peer_encryption_enabled = true; diff --git a/src/dtls12/incoming.rs b/src/dtls12/incoming.rs index da13eade..ca21b6c0 100644 --- a/src/dtls12/incoming.rs +++ b/src/dtls12/incoming.rs @@ -4,10 +4,10 @@ use std::sync::atomic::{AtomicBool, Ordering}; use arrayvec::ArrayVec; use std::fmt; -use crate::Error; use crate::buffer::{Buf, TmpBuf}; use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::{ContentType, DTLSRecord, Dtls12CipherSuite, Handshake, Sequence}; +use crate::{Error, InternalError}; /// Holds both the UDP packet and the parsed result of that packet. pub struct Incoming { @@ -44,7 +44,7 @@ impl Incoming { packet: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result, Error> { + ) -> Result, InternalError> { // Parse records directly from packet, copying each record ONCE into its own buffer let records = Records::parse(packet, decrypt, cs)?; @@ -71,13 +71,13 @@ impl Records { mut packet: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result { + ) -> Result { let mut parsed_records: ArrayVec = ArrayVec::new(); // Find record boundaries and copy each record ONCE from the packet while !packet.is_empty() { if packet.len() < DTLSRecord::HEADER_LEN { - return Err(Error::ParseIncomplete); + return Err(InternalError::parse_incomplete()); } let length_bytes: [u8; 2] = packet[DTLSRecord::LENGTH_OFFSET].try_into().unwrap(); @@ -85,7 +85,7 @@ impl Records { let record_end = DTLSRecord::HEADER_LEN + length; if packet.len() < record_end { - return Err(Error::ParseIncomplete); + return Err(InternalError::parse_incomplete()); } // This is the ONLY copy: packet -> record buffer @@ -94,7 +94,7 @@ impl Records { Ok(record) => { if let Some(record) = record { if parsed_records.try_push(record).is_err() { - return Err(Error::TooManyRecords); + return Err(InternalError::too_many_records()); } } else { trace!("Discarding replayed rec"); @@ -141,7 +141,7 @@ impl Record { record_slice: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result, Error> { + ) -> Result, InternalError> { // ONLY COPY: UDP packet slice -> pooled buffer let mut buffer = Buf::new(); buffer.extend_from_slice(record_slice); @@ -200,7 +200,7 @@ impl Record { // This decrypts in place. if let Err(e) = decrypt.decrypt_data(&mut buffer, aad, nonce) { if !decrypt.can_discard_bad_protected_record() { - return Err(e); + return Err(e.into()); } trace!("Discarding record: decrypt failed: {}", e); @@ -276,7 +276,7 @@ impl ParsedRecord { input: &[u8], cipher_suite: Option, offset: usize, - ) -> Result { + ) -> Result { let (_, record) = DTLSRecord::parse(input, 0, offset)?; let handshakes = if record.content_type == ContentType::Handshake { diff --git a/src/dtls12/message/handshake.rs b/src/dtls12/message/handshake.rs index 6ccb45f7..65521159 100644 --- a/src/dtls12/message/handshake.rs +++ b/src/dtls12/message/handshake.rs @@ -144,7 +144,7 @@ impl Handshake { buffer: &mut Buf, cipher_suite: Option, transcript: Option<&mut Buf>, - ) -> Result { + ) -> Result { buffer.clear(); // Invariant is upheld by the caller. @@ -172,7 +172,7 @@ impl Handshake { if buffer.len() != first_handshake.header.length as usize { debug!("Defragmentation failed. Fragment length mismatch"); - return Err(crate::Error::ParseIncomplete); + return Err(crate::InternalError::parse_incomplete()); } // If transcript is provided, write the handshake header + body before parsing @@ -190,7 +190,7 @@ impl Handshake { if !rest.is_empty() && first_handshake.header.msg_type == MessageType::Finished { debug!("Defragmentation failed. Body::parse() did not consume the entire buffer"); - return Err(crate::Error::ParseIncomplete); + return Err(crate::InternalError::parse_incomplete()); } let handshake = Handshake { diff --git a/src/dtls12/server.rs b/src/dtls12/server.rs index 449d2e06..15035091 100644 --- a/src/dtls12/server.rs +++ b/src/dtls12/server.rs @@ -36,7 +36,7 @@ use crate::dtls12::message::{ServerHello, SessionId, SignatureAlgorithm}; use crate::dtls12::message::{SignatureAlgorithmsExtension, SignatureAndHashAlgorithm}; use crate::dtls12::message::{SignatureAndHashAlgorithmVec, SrtpProfileId}; use crate::dtls12::message::{SrtpProfileVec, SupportedGroupsExtension, UseSrtpExtension}; -use crate::{Config, Error, Output}; +use crate::{Config, Error, InternalError, Output}; /// Length of the random dummy PSK used when identity resolution fails. /// Fixed so handshake timing does not leak the failure, and long enough @@ -195,8 +195,7 @@ impl Server { .and_then(|_| self.make_progress()) { Ok(()) => Ok(()), - Err(e) if e.is_transient() => Ok(()), - Err(e) => Err(e), + Err(e) => e.into_public_error().map_or(Ok(()), Err), } } @@ -213,8 +212,10 @@ impl Server { self.random = Some(Random::new_with_time(now, &mut self.engine.rng)); } self.engine.handle_timeout(now)?; - self.make_progress()?; - Ok(()) + match self.make_progress() { + Ok(()) => Ok(()), + Err(e) => e.into_public_error().map_or(Ok(()), Err), + } } /// Send application data when the server is in the Running state @@ -257,7 +258,7 @@ impl Server { Ok(()) } - fn make_progress(&mut self) -> Result<(), Error> { + fn make_progress(&mut self) -> Result<(), InternalError> { loop { let prev_state = self.state; @@ -294,7 +295,7 @@ impl State { } } - fn make_progress(self, server: &mut Server) -> Result { + fn make_progress(self, server: &mut Server) -> Result { match self { State::AwaitClientHello => self.await_client_hello(server), State::SendServerHello => self.send_server_hello(server), @@ -314,7 +315,7 @@ impl State { } } - fn await_client_hello(self, server: &mut Server) -> Result { + fn await_client_hello(self, server: &mut Server) -> Result { let maybe = server .engine .next_handshake(MessageType::ClientHello, &mut server.defragment_buffer)?; @@ -330,18 +331,20 @@ impl State { // Enforce DTLS1.2 if ch.client_version != ProtocolVersion::DTLS1_2 { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Unsupported DTLS version from client: {:?}", ch.client_version - ))); + ))) + .into()); } // Enforce Null compression only (client must offer it) let has_null = ch.compression_methods.contains(&CompressionMethod::Null); if !has_null { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "Client did not offer Null compression".to_string(), - )); + )) + .into()); } trace!( @@ -400,9 +403,9 @@ impl State { } let Some(cs) = selected else { - return Err(Error::SecurityError( - "No mutually acceptable cipher suite".to_string(), - )); + return Err( + (Error::SecurityError("No mutually acceptable cipher suite".to_string())).into(), + ); }; server.engine.set_cipher_suite(cs); @@ -419,7 +422,8 @@ impl State { match ext.extension_type { ExtensionType::UseSrtp => { let ext_data = ext.extension_data(&server.defragment_buffer); - let (_, use_srtp) = UseSrtpExtension::parse(ext_data).map_err(Error::from)?; + let (_, use_srtp) = + UseSrtpExtension::parse(ext_data).map_err(InternalError::from)?; client_srtp_profiles = Some(use_srtp.profiles); } ExtensionType::ExtendedMasterSecret => { @@ -428,12 +432,13 @@ impl State { ExtensionType::SupportedGroups => { let ext_data = ext.extension_data(&server.defragment_buffer); let (_, groups) = - SupportedGroupsExtension::parse(ext_data).map_err(Error::from)?; + SupportedGroupsExtension::parse(ext_data).map_err(InternalError::from)?; client_supported_groups = Some(groups.groups); } ExtensionType::EcPointFormats => { let ext_data = ext.extension_data(&server.defragment_buffer); - let _ = ECPointFormatsExtension::parse(ext_data).map_err(Error::from)?; + let _ = + ECPointFormatsExtension::parse(ext_data).map_err(InternalError::from)?; } ExtensionType::SignatureAlgorithms => { let ext_data = ext.extension_data(&server.defragment_buffer); @@ -449,9 +454,10 @@ impl State { // EMS is mandatory if !client_offers_ems { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "Extended Master Secret not negotiated".to_string(), - )); + )) + .into()); } // Select SRTP profile according to server priority: AES256GCM, AES128GCM, then SHA1 @@ -483,7 +489,7 @@ impl State { Ok(Self::SendServerHello) } - fn send_server_hello(self, server: &mut Server) -> Result { + fn send_server_hello(self, server: &mut Server) -> Result { trace!("Sending ServerHello"); // Start/restart flight timer for server Flight 4 @@ -522,7 +528,7 @@ impl State { } } - fn send_certificate(self, server: &mut Server) -> Result { + fn send_certificate(self, server: &mut Server) -> Result { trace!("Sending Certificate"); server @@ -532,7 +538,7 @@ impl State { Ok(Self::SendServerKeyExchange) } - fn send_server_key_exchange(self, server: &mut Server) -> Result { + fn send_server_key_exchange(self, server: &mut Server) -> Result { trace!("Sending ServerKeyExchange"); let cs = server @@ -624,7 +630,7 @@ impl State { /// PSK ServerKeyExchange: send identity hint only (no ECDHE, no signature). /// Per RFC 4279 §2, the message is omitted entirely when no hint is configured. - fn send_server_key_exchange_psk(self, server: &mut Server) -> Result { + fn send_server_key_exchange_psk(self, server: &mut Server) -> Result { let Some(hint) = server .engine .config() @@ -645,7 +651,7 @@ impl State { Ok(Self::SendServerHelloDone) } - fn send_certificate_request(self, server: &mut Server) -> Result { + fn send_certificate_request(self, server: &mut Server) -> Result { debug!("Sending CertificateRequest"); // Select CertificateRequest.signature_algorithms as intersection of client's list and our supported let sig_algs = @@ -664,7 +670,7 @@ impl State { Ok(Self::SendServerHelloDone) } - fn send_server_hello_done(self, server: &mut Server) -> Result { + fn send_server_hello_done(self, server: &mut Server) -> Result { trace!("Sending ServerHelloDone"); server @@ -688,7 +694,7 @@ impl State { } } - fn await_certificate(self, server: &mut Server) -> Result { + fn await_certificate(self, server: &mut Server) -> Result { let maybe = server .engine .next_handshake(MessageType::Certificate, &mut server.defragment_buffer)?; @@ -735,7 +741,7 @@ impl State { Ok(Self::AwaitClientKeyExchange) } - fn await_client_key_exchange(self, server: &mut Server) -> Result { + fn await_client_key_exchange(self, server: &mut Server) -> Result { let maybe = server.engine.next_handshake( MessageType::ClientKeyExchange, &mut server.defragment_buffer, @@ -760,9 +766,10 @@ impl State { let identity_range = match &ckx.exchange_keys { ExchangeKeys::Psk(keys) => keys.identity_range.clone(), _ => { - return Err(Error::UnexpectedMessage( + return Err((Error::UnexpectedMessage( "ECDHE ClientKeyExchange in PSK path".to_string(), - )); + )) + .into()); } }; @@ -802,9 +809,10 @@ impl State { let public_key_range = match &ckx.exchange_keys { ExchangeKeys::Ecdh(keys) => keys.public_key_range.clone(), ExchangeKeys::Psk(_) => { - return Err(Error::UnexpectedMessage( + return Err((Error::UnexpectedMessage( "PSK ClientKeyExchange in ECDHE path".to_string(), - )); + )) + .into()); } }; @@ -888,7 +896,7 @@ impl State { } } - fn await_certificate_verify(self, server: &mut Server) -> Result { + fn await_certificate_verify(self, server: &mut Server) -> Result { // Get handshake data BEFORE processing CertificateVerify message // According to TLS spec, signature is over all handshake messages // up to but not including CertificateVerify @@ -921,9 +929,10 @@ impl State { let signature_bytes = &server.defragment_buffer[signature_range]; if server.client_certificates.is_empty() { - return Err(Error::CertificateError( + return Err((Error::CertificateError( "CertificateVerify received but no client certificate".to_string(), - )); + )) + .into()); } // Create temp DigitallySigned for verification @@ -950,7 +959,7 @@ impl State { Ok(Self::AwaitChangeCipherSpec) } - fn await_change_cipher_spec(self, server: &mut Server) -> Result { + fn await_change_cipher_spec(self, server: &mut Server) -> Result { let maybe = server.engine.next_record(ContentType::ChangeCipherSpec); let Some(_) = maybe else { @@ -969,7 +978,7 @@ impl State { Ok(Self::AwaitFinished) } - fn await_finished(self, server: &mut Server) -> Result { + fn await_finished(self, server: &mut Server) -> Result { // Generate expected verify data based on current transcript. // This must be done before next_handshake() below since // it should not include Finished itself. @@ -1003,9 +1012,9 @@ impl State { // Use constant-time comparison to prevent timing attacks let is_eq: bool = verify_data.ct_eq(expected.as_slice()).into(); if !is_eq { - return Err(Error::SecurityError( - "Client Finished verification failed".to_string(), - )); + return Err( + (Error::SecurityError("Client Finished verification failed".to_string())).into(), + ); } // Invariant: full PSK handshakes always set psk_valid in @@ -1030,9 +1039,9 @@ impl State { // skip ClientKeyExchange and therefore never set psk_valid — those // paths reuse a cached master_secret and don't consult the resolver. if server.psk_valid == Some(false) { - return Err(Error::SecurityError( - "Client Finished verification failed".to_string(), - )); + return Err( + (Error::SecurityError("Client Finished verification failed".to_string())).into(), + ); } trace!("Client Finished verified successfully"); @@ -1040,7 +1049,7 @@ impl State { Ok(Self::SendChangeCipherSpec) } - fn send_change_cipher_spec(self, server: &mut Server) -> Result { + fn send_change_cipher_spec(self, server: &mut Server) -> Result { trace!("Sending ChangeCipherSpec"); // Start/restart flight timer for server Flight 6 (CCS+Finished) @@ -1056,7 +1065,7 @@ impl State { Ok(Self::SendFinished) } - fn send_finished(self, server: &mut Server) -> Result { + fn send_finished(self, server: &mut Server) -> Result { trace!("Sending Finished message to complete handshake"); server @@ -1112,7 +1121,7 @@ impl State { Ok(Self::AwaitApplicationData) } - fn await_application_data(self, server: &mut Server) -> Result { + fn await_application_data(self, server: &mut Server) -> Result { if server.engine.close_notify_received() { // RFC 5246 §7.2.1: respond with a reciprocal close_notify and // close down immediately, discarding any pending writes. diff --git a/src/dtls13/client.rs b/src/dtls13/client.rs index 7c87632d..a23d7a3c 100644 --- a/src/dtls13/client.rs +++ b/src/dtls13/client.rs @@ -59,7 +59,7 @@ use crate::dtls13::message::SupportedVersionsClientHello; use crate::dtls13::message::SupportedVersionsServerHello; use crate::dtls13::message::UseSrtpExtension; use crate::dtls13::message::parse_cookie_extension; -use crate::{Error, KeyingMaterial, Output}; +use crate::{Error, InternalError, KeyingMaterial, Output}; /// DTLS 1.3 client pub struct Client { @@ -225,8 +225,7 @@ impl Client { .and_then(|_| self.make_progress()) { Ok(()) => Ok(()), - Err(e) if e.is_transient() => Ok(()), - Err(e) => Err(e), + Err(e) => e.into_public_error().map_or(Ok(()), Err), } } @@ -244,8 +243,10 @@ impl Client { self.random = Some(self.engine.random()); } self.engine.handle_timeout(now)?; - self.make_progress()?; - Ok(()) + match self.make_progress() { + Ok(()) => Ok(()), + Err(e) => e.into_public_error().map_or(Ok(()), Err), + } } fn initiate_key_update(&mut self) -> Result<(), Error> { @@ -298,7 +299,7 @@ impl Client { Ok(()) } - fn make_progress(&mut self) -> Result<(), Error> { + fn make_progress(&mut self) -> Result<(), InternalError> { loop { let prev_state = self.state; @@ -350,7 +351,7 @@ impl State { } } - fn make_progress(self, client: &mut Client) -> Result { + fn make_progress(self, client: &mut Client) -> Result { match self { State::SendClientHello => self.send_client_hello(client), State::AwaitServerHello => self.await_server_hello(client), @@ -368,7 +369,7 @@ impl State { } } - fn send_client_hello(self, client: &mut Client) -> Result { + fn send_client_hello(self, client: &mut Client) -> Result { // unwrap: is ok because we set the random in handle_timeout let random = client.random.unwrap(); @@ -437,7 +438,7 @@ impl State { Ok(Self::AwaitServerHello) } - fn await_server_hello(self, client: &mut Client) -> Result { + fn await_server_hello(self, client: &mut Client) -> Result { // Save transcript length so we can roll back if a stale HRR // retransmission arrives after we already processed one. let transcript_len_before = client.engine.transcript.len(); @@ -480,7 +481,7 @@ impl State { } ExtensionType::Cookie => { let ext_data = ext.extension_data(&client.defragment_buffer); - parse_cookie_extension(ext_data).map_err(Error::from)?; + parse_cookie_extension(ext_data).map_err(InternalError::from)?; let mut cookie = Buf::new(); cookie.extend_from_slice(ext_data); client.saved_cookie = Some(cookie); @@ -495,9 +496,9 @@ impl State { .engine .is_cipher_suite_allowed(server_hello.cipher_suite) { - return Err(Error::SecurityError( - "HRR selected disallowed cipher suite".into(), - )); + return Err( + (Error::SecurityError("HRR selected disallowed cipher suite".into())).into(), + ); } client.engine.set_cipher_suite(server_hello.cipher_suite); @@ -514,7 +515,7 @@ impl State { } } if !hrr_version_ok { - return Err(Error::SecurityError("HRR did not select DTLS 1.3".into())); + return Err((Error::SecurityError("HRR did not select DTLS 1.3".into())).into()); } // Replace transcript with message_hash per RFC 8446 Section 4.4.1. @@ -535,16 +536,17 @@ impl State { // Validate legacy_version (must be DTLS 1.2) if server_hello.legacy_version != ProtocolVersion::DTLS1_2 { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "ServerHello legacy_version must be DTLS 1.2".into(), - )); + )) + .into()); } // Validate legacy_compression_method (must be null) if server_hello.legacy_compression_method != CompressionMethod::Null { - return Err(Error::SecurityError( - "ServerHello compression must be null".into(), - )); + return Err( + (Error::SecurityError("ServerHello compression must be null".into())).into(), + ); } debug!( @@ -555,16 +557,17 @@ impl State { // Validate cipher suite let cs = server_hello.cipher_suite; if matches!(cs, Dtls13CipherSuite::Unknown(_)) { - return Err(Error::SecurityError( - "Server selected unknown cipher suite".to_string(), - )); + return Err( + (Error::SecurityError("Server selected unknown cipher suite".to_string())).into(), + ); } if !client.engine.is_cipher_suite_allowed(cs) { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Server selected disallowed cipher suite: {:?}", cs - ))); + ))) + .into()); } client.engine.set_cipher_suite(cs); @@ -575,7 +578,7 @@ impl State { let mut server_key_share: Option<(NamedGroup, std::ops::Range)> = None; let Some(ref extensions) = server_hello.extensions else { - return Err(Error::IncompleteServerHello); + return Err((Error::IncompleteServerHello).into()); }; for ext in extensions { @@ -606,15 +609,17 @@ impl State { } if !supported_version_ok { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "Server did not negotiate DTLS 1.3 via supported_versions".to_string(), - )); + )) + .into()); } let Some((server_group, ke_range)) = server_key_share else { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "Server did not provide key_share extension".to_string(), - )); + )) + .into()); }; // Complete ECDHE key exchange @@ -624,11 +629,12 @@ impl State { .ok_or_else(|| Error::InvalidState("No active key exchange".to_string()))?; if key_exchange.group() != server_group { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Server key share group mismatch: {:?} != {:?}", server_group, key_exchange.group() - ))); + ))) + .into()); } let peer_pub_key = &client.extension_data[ke_range]; @@ -665,7 +671,7 @@ impl State { Ok(Self::AwaitEncryptedExtensions) } - fn await_encrypted_extensions(self, client: &mut Client) -> Result { + fn await_encrypted_extensions(self, client: &mut Client) -> Result { let maybe = client.engine.next_handshake( MessageType::EncryptedExtensions, &mut client.defragment_buffer, @@ -683,7 +689,8 @@ impl State { for ext in &ee.extensions { if ext.extension_type == ExtensionType::UseSrtp { let ext_data = ext.extension_data(&client.defragment_buffer); - let (_, use_srtp) = UseSrtpExtension::parse(ext_data).map_err(Error::from)?; + let (_, use_srtp) = + UseSrtpExtension::parse(ext_data).map_err(InternalError::from)?; if !use_srtp.profiles.is_empty() { client.negotiated_srtp_profile = Some(use_srtp.profiles[0].into()); trace!( @@ -698,7 +705,7 @@ impl State { Ok(Self::AwaitCertificateRequest) } - fn await_certificate_request(self, client: &mut Client) -> Result { + fn await_certificate_request(self, client: &mut Client) -> Result { // CertificateRequest is optional. Check if Certificate is available instead. let has_cert = client .engine @@ -737,7 +744,7 @@ impl State { Ok(Self::AwaitCertificate) } - fn await_certificate(self, client: &mut Client) -> Result { + fn await_certificate(self, client: &mut Client) -> Result { let maybe = client .engine .next_handshake(MessageType::Certificate, &mut client.defragment_buffer)?; @@ -751,15 +758,15 @@ impl State { }; if !certificate.context_range.is_empty() { - return Err(Error::SecurityError( - "Server certificate context must be empty".into(), - )); + return Err( + (Error::SecurityError("Server certificate context must be empty".into())).into(), + ); } if certificate.certificate_list.is_empty() { - return Err(Error::CertificateError( - "No server certificate received".to_string(), - )); + return Err( + (Error::CertificateError("No server certificate received".to_string())).into(), + ); } debug!( @@ -792,7 +799,7 @@ impl State { Ok(Self::AwaitCertificateVerify) } - fn await_certificate_verify(self, client: &mut Client) -> Result { + fn await_certificate_verify(self, client: &mut Client) -> Result { // Compute transcript hash BEFORE consuming CertificateVerify. // Per RFC 8446 §4.4.3, the hash covers all messages up to but NOT // including CertificateVerify. next_handshake() will append CV to @@ -819,10 +826,11 @@ impl State { // RFC 8446 §4.4.3: The receiver MUST verify that the signature algorithm // is one that was offered in the signature_algorithms extension. if !SignatureScheme::supported().contains(&scheme) { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Server used un-offered signature scheme: {:?}", scheme - ))); + ))) + .into()); } // Build the signed content per RFC 8446 Section 4.4.3: @@ -864,7 +872,7 @@ impl State { Ok(Self::AwaitFinished) } - fn await_finished(self, client: &mut Client) -> Result { + fn await_finished(self, client: &mut Client) -> Result { // Compute expected verify_data BEFORE consuming Finished // (verify_data uses transcript hash up to but not including Finished) let server_hs_secret = client @@ -896,9 +904,9 @@ impl State { // Constant-time comparison let is_eq: bool = verify_data.ct_eq(&*expected_verify_data).into(); if !is_eq { - return Err(Error::SecurityError( - "Server Finished verification failed".to_string(), - )); + return Err( + (Error::SecurityError("Server Finished verification failed".to_string())).into(), + ); } trace!("Server Finished verified successfully"); @@ -931,7 +939,7 @@ impl State { } } - fn send_certificate(self, client: &mut Client) -> Result { + fn send_certificate(self, client: &mut Client) -> Result { debug!("Sending Certificate"); // Start a new flight for the client's Certificate/CertificateVerify/Finished. @@ -975,7 +983,7 @@ impl State { } } - fn send_certificate_verify(self, client: &mut Client) -> Result { + fn send_certificate_verify(self, client: &mut Client) -> Result { debug!("Sending CertificateVerify"); client @@ -991,7 +999,7 @@ impl State { Ok(Self::SendFinished) } - fn send_finished(self, client: &mut Client) -> Result { + fn send_finished(self, client: &mut Client) -> Result { trace!("Sending Finished message to complete handshake"); // When the server did NOT request client authentication, we skip @@ -1047,7 +1055,7 @@ impl State { Ok(Self::AwaitApplicationData) } - fn await_application_data(self, client: &mut Client) -> Result { + fn await_application_data(self, client: &mut Client) -> Result { // Auto-trigger KeyUpdate when AEAD encryption limit is reached if client.engine.needs_key_update() && !client.engine.is_key_update_in_flight() { client.initiate_key_update()?; @@ -1111,7 +1119,7 @@ impl State { Ok(self) } - fn half_closed_local(self, client: &mut Client) -> Result { + fn half_closed_local(self, client: &mut Client) -> Result { // Write half is closed: drain incoming KeyUpdate to keep recv keys in sync, // but do not send our own KeyUpdate response. if client.engine.has_complete_handshake(MessageType::KeyUpdate) { @@ -1583,6 +1591,9 @@ mod tests { .await_certificate(&mut client) .expect_err("empty server Certificate should fail"); - assert!(matches!(err, Error::CertificateError(_))); + assert!(matches!( + err, + crate::InternalError::Fatal(Error::CertificateError(_)) + )); } } diff --git a/src/dtls13/engine.rs b/src/dtls13/engine.rs index 39ef14cc..9729d804 100644 --- a/src/dtls13/engine.rs +++ b/src/dtls13/engine.rs @@ -28,7 +28,7 @@ use crate::dtls13::message::Sequence; use crate::timer::ExponentialBackoff; use crate::types::{HashAlgorithm, Random}; use crate::window::ReplayWindow; -use crate::{Config, DtlsCertificate, Error, Output, SeededRng}; +use crate::{Config, DtlsCertificate, Error, InternalError, Output, SeededRng}; const MAX_DEFRAGMENT_PACKETS: usize = 50; @@ -329,7 +329,7 @@ impl Engine { &mut *self.signing_key } - pub fn parse_packet(&mut self, packet: &[u8]) -> Result<(), Error> { + pub fn parse_packet(&mut self, packet: &[u8]) -> Result<(), InternalError> { let cs = self.cipher_suite; let incoming = Incoming::parse_packet(packet, self, cs)?; if let Some(incoming) = incoming { @@ -781,14 +781,14 @@ impl Engine { &mut self, wanted: MessageType, defragment_buffer: &mut Buf, - ) -> Result, Error> { + ) -> Result, InternalError> { self.next_handshake_with_options(wanted, defragment_buffer, false) } pub(crate) fn next_client_hello_for_auto_sense( &mut self, defragment_buffer: &mut Buf, - ) -> Result, Error> { + ) -> Result, InternalError> { self.next_handshake_with_options(MessageType::ClientHello, defragment_buffer, true) } @@ -797,7 +797,7 @@ impl Engine { wanted: MessageType, defragment_buffer: &mut Buf, allow_unknown_client_hello_suites: bool, - ) -> Result, Error> { + ) -> Result, InternalError> { if !self.has_complete_handshake(wanted) { return Ok(None); } @@ -835,7 +835,7 @@ impl Engine { &mut self, wanted: MessageType, defragment_buffer: &mut Buf, - ) -> Result, Error> { + ) -> Result, InternalError> { if !self.has_complete_handshake(wanted) { return Ok(None); } @@ -2102,7 +2102,7 @@ impl Engine { // Peer Encryption Management // ========================================================================= - pub fn enable_peer_encryption(&mut self) -> Result<(), Error> { + pub fn enable_peer_encryption(&mut self) -> Result<(), InternalError> { debug!("Peer encryption enabled"); self.peer_encryption_enabled = true; diff --git a/src/dtls13/incoming.rs b/src/dtls13/incoming.rs index 240cc830..f617a416 100644 --- a/src/dtls13/incoming.rs +++ b/src/dtls13/incoming.rs @@ -4,9 +4,9 @@ use std::sync::atomic::{AtomicBool, Ordering}; use arrayvec::ArrayVec; use std::fmt; -use crate::Error; use crate::buffer::{Buf, TmpBuf}; use crate::dtls13::message::{ContentType, Dtls13CipherSuite, Dtls13Record, Handshake, Sequence}; +use crate::{Error, InternalError}; /// Holds both the UDP packet and the parsed result of that packet. pub struct Incoming { @@ -43,7 +43,7 @@ impl Incoming { packet: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result, Error> { + ) -> Result, InternalError> { // Parse records directly from packet, copying each record ONCE into its own buffer let records = Records::parse(packet, decrypt, cs)?; @@ -70,7 +70,7 @@ impl Records { mut packet: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result { + ) -> Result { let mut parsed_records: ArrayVec = ArrayVec::new(); // Find record boundaries and copy each record ONCE from the packet @@ -84,7 +84,7 @@ impl Records { // Unified header: variable length if packet.len() < 2 { - return Err(Error::ParseIncomplete); + return Err(InternalError::parse_incomplete()); } let flags = packet[0]; @@ -95,7 +95,7 @@ impl Records { let header_len = 1 + seq_len + len_len; if packet.len() < header_len { - return Err(Error::ParseIncomplete); + return Err(InternalError::parse_incomplete()); } if l_flag { @@ -112,7 +112,7 @@ impl Records { } else { // Plaintext: fixed 13-byte header if packet.len() < Dtls13Record::PLAINTEXT_HEADER_LEN { - return Err(Error::ParseIncomplete); + return Err(InternalError::parse_incomplete()); } // unwrap: PLAINTEXT_HEADER_LEN check above ensures 2 bytes at offset @@ -124,7 +124,7 @@ impl Records { }; if packet.len() < record_end { - return Err(Error::ParseIncomplete); + return Err(InternalError::parse_incomplete()); } // This is the ONLY copy: packet -> record buffer @@ -133,7 +133,7 @@ impl Records { Ok(record) => { if let Some(record) = record { if parsed_records.try_push(record).is_err() { - return Err(Error::TooManyRecords); + return Err(InternalError::too_many_records()); } } else { trace!("Discarding replayed rec"); @@ -180,7 +180,7 @@ impl Record { record_slice: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result, Error> { + ) -> Result, InternalError> { // ONLY COPY: UDP packet slice -> pooled buffer let mut buffer = Buf::new(); buffer.extend_from_slice(record_slice); @@ -360,7 +360,7 @@ impl ParsedRecord { pub fn parse( input: &[u8], cipher_suite: Option, - ) -> Result { + ) -> Result { let (_, record) = Dtls13Record::parse(input, 0)?; let handshakes = if record.content_type == ContentType::Handshake { @@ -464,14 +464,14 @@ fn parse_handshakes( /// /// The format is: `content || ContentType || zeros*` /// Scan backward past zero padding to find the content type byte. -fn recover_inner_content_type(decrypted: &[u8]) -> Result<(ContentType, usize), Error> { +fn recover_inner_content_type(decrypted: &[u8]) -> Result<(ContentType, usize), InternalError> { let mut i = decrypted.len(); // Skip zero padding while i > 0 && decrypted[i - 1] == 0 { i -= 1; } if i == 0 { - return Err(Error::ParseError(nom::error::ErrorKind::Fail)); + return Err(InternalError::parse(nom::error::ErrorKind::Fail)); } // The byte before padding is the content type i -= 1; diff --git a/src/dtls13/message/handshake.rs b/src/dtls13/message/handshake.rs index d9aca37a..612e195d 100644 --- a/src/dtls13/message/handshake.rs +++ b/src/dtls13/message/handshake.rs @@ -136,7 +136,7 @@ impl Handshake { buffer: &mut Buf, cipher_suite: Option, transcript: Option<&mut Buf>, - ) -> Result { + ) -> Result { Self::defragment_with_options(iter, buffer, cipher_suite, transcript, false) } @@ -145,7 +145,7 @@ impl Handshake { buffer: &mut Buf, cipher_suite: Option, transcript: Option<&mut Buf>, - ) -> Result { + ) -> Result { Self::defragment_with_options(iter, buffer, cipher_suite, transcript, true) } @@ -155,7 +155,7 @@ impl Handshake { cipher_suite: Option, transcript: Option<&mut Buf>, allow_unknown_client_hello_suites: bool, - ) -> Result { + ) -> Result { buffer.clear(); // Invariant is upheld by the caller. @@ -196,7 +196,7 @@ impl Handshake { if buffer.len() != first_handshake.header.length as usize { debug!("Defragmentation failed. Fragment length mismatch"); - return Err(crate::Error::ParseIncomplete); + return Err(crate::InternalError::parse_incomplete()); } // If transcript is provided, write the TLS 1.3-style header + body. @@ -221,7 +221,7 @@ impl Handshake { if !rest.is_empty() && first_handshake.header.msg_type == MessageType::Finished { debug!("Defragmentation failed. Body::parse() did not consume the entire buffer"); - return Err(crate::Error::ParseIncomplete); + return Err(crate::InternalError::parse_incomplete()); } let handshake = Handshake { diff --git a/src/dtls13/server.rs b/src/dtls13/server.rs index 1fb7f964..3986797c 100644 --- a/src/dtls13/server.rs +++ b/src/dtls13/server.rs @@ -67,7 +67,7 @@ use crate::dtls13::message::SupportedVersionsClientHello; use crate::dtls13::message::SupportedVersionsServerHello; use crate::dtls13::message::UseSrtpExtension; use crate::dtls13::message::parse_cookie_extension; -use crate::{Config, DtlsCertificate, Error, Output}; +use crate::{Config, DtlsCertificate, Error, InternalError, Output}; /// Magic random value indicating HelloRetryRequest (RFC 8446 Section 4.1.3). const HRR_RANDOM: [u8; 32] = [ @@ -254,8 +254,12 @@ impl Server { .and_then(|_| self.make_progress()) { Ok(()) => {} - Err(e) if e.is_transient() => return Ok(()), - Err(e) => return Err(e), + Err(e) => { + if let Some(err) = e.into_public_error() { + return Err(err); + } + return Ok(()); + } } // Once past AwaitClientHello, DTLS 1.3 is committed — free the buffer. @@ -281,8 +285,10 @@ impl Server { self.random = Some(self.engine.random()); } self.engine.handle_timeout(now)?; - self.make_progress()?; - Ok(()) + match self.make_progress() { + Ok(()) => Ok(()), + Err(e) => e.into_public_error().map_or(Ok(()), Err), + } } fn initiate_key_update(&mut self) -> Result<(), Error> { @@ -335,7 +341,7 @@ impl Server { Ok(()) } - fn make_progress(&mut self) -> Result<(), Error> { + fn make_progress(&mut self) -> Result<(), InternalError> { loop { let prev_state = self.state; @@ -370,7 +376,7 @@ impl State { } } - fn make_progress(self, server: &mut Server) -> Result { + fn make_progress(self, server: &mut Server) -> Result { match self { State::AwaitClientHello => self.await_client_hello(server), State::SendServerHello => self.send_server_hello(server), @@ -388,7 +394,7 @@ impl State { } } - fn await_client_hello(self, server: &mut Server) -> Result { + fn await_client_hello(self, server: &mut Server) -> Result { // Save transcript length so we can roll back if a stale ClientHello // arrives after HelloRetryRequest (no cookie → must discard). let transcript_len_before = server.engine.transcript.len(); @@ -413,9 +419,10 @@ impl State { // Validate legacy_version if client_hello.legacy_version != ProtocolVersion::DTLS1_2 { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "ClientHello legacy_version must be DTLS 1.2".to_string(), - )); + )) + .into()); } // Validate null compression is offered @@ -423,9 +430,10 @@ impl State { .legacy_compression_methods .contains(&CompressionMethod::Null); if !has_null_compression { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "ClientHello must offer null compression".to_string(), - )); + )) + .into()); } // Parse extensions @@ -442,8 +450,8 @@ impl State { match ext.extension_type { ExtensionType::SupportedVersions => { let ext_data = ext.extension_data(&server.defragment_buffer); - let (_, sv) = - SupportedVersionsClientHello::parse(ext_data).map_err(Error::from)?; + let (_, sv) = SupportedVersionsClientHello::parse(ext_data) + .map_err(InternalError::from)?; for v in &sv.versions { if *v == ProtocolVersion::DTLS1_3 { supported_versions_ok = true; @@ -454,18 +462,21 @@ impl State { let ext_data = ext.extension_data(&server.defragment_buffer); let ext_data_start = ext.extension_data_range.start; let (_, ks) = KeyShareClientHello::parse(ext_data, ext_data_start) - .map_err(Error::from)?; + .map_err(InternalError::from)?; let mut entries = ArrayVec::new(); for entry in &ks.entries { entries .try_push((entry.group, entry.key_exchange_range.clone())) - .map_err(|_| Error::ParseError(nom::error::ErrorKind::LengthValue))?; + .map_err(|_| { + InternalError::parse(nom::error::ErrorKind::LengthValue) + })?; } client_key_shares = Some(entries); } ExtensionType::SupportedGroups => { let ext_data = ext.extension_data(&server.defragment_buffer); - let (_, sg) = SupportedGroupsExtension::parse(ext_data).map_err(Error::from)?; + let (_, sg) = + SupportedGroupsExtension::parse(ext_data).map_err(InternalError::from)?; client_supported_groups = Some(sg.groups); } ExtensionType::SignatureAlgorithms => { @@ -475,12 +486,14 @@ impl State { } ExtensionType::UseSrtp => { let ext_data = ext.extension_data(&server.defragment_buffer); - let (_, use_srtp) = UseSrtpExtension::parse(ext_data).map_err(Error::from)?; + let (_, use_srtp) = + UseSrtpExtension::parse(ext_data).map_err(InternalError::from)?; client_srtp_profiles = Some(use_srtp.profiles); } ExtensionType::Cookie => { let ext_data = ext.extension_data(&server.defragment_buffer); - let (_, cookie) = parse_cookie_extension(ext_data).map_err(Error::from)?; + let (_, cookie) = + parse_cookie_extension(ext_data).map_err(InternalError::from)?; client_cookie_data = Some(ArrayVec::try_from(cookie).map_err(|_| { Error::SecurityError("Invalid cookie in ClientHello".to_string()) })?); @@ -491,11 +504,12 @@ impl State { if !supported_versions_ok { if server.auto_mode { - return Err(Error::Dtls12Fallback); + return Err((Error::Dtls12Fallback).into()); } - return Err(Error::SecurityError( + return Err((Error::SecurityError( "ClientHello must include DTLS 1.3 in supported_versions".to_string(), - )); + )) + .into()); } // Select cipher suite: first from client's list that is in our provider @@ -554,9 +568,10 @@ impl State { // Validate the cookie let is_valid: bool = cookie_bytes.as_slice().ct_eq(&expected_cookie).into(); if !is_valid { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "Invalid cookie in ClientHello".to_string(), - )); + )) + .into()); } debug!("Cookie validated successfully"); } else { @@ -603,9 +618,9 @@ impl State { ); let is_valid: bool = cookie_bytes.as_slice().ct_eq(&expected_cookie).into(); if !is_valid { - return Err(Error::SecurityError( - "Invalid cookie in ClientHello".to_string(), - )); + return Err( + (Error::SecurityError("Invalid cookie in ClientHello".to_string())).into(), + ); } } @@ -628,9 +643,10 @@ impl State { return if let Some(group) = common_group { // Need HRR for key exchange if server.hello_retry { - return Err(Error::SecurityError( + return Err((Error::SecurityError( "Cannot send second HelloRetryRequest".to_string(), - )); + )) + .into()); } debug!( @@ -663,9 +679,7 @@ impl State { Ok(Self::AwaitClientHello) } else { - Err(Error::SecurityError( - "No common key exchange group".to_string(), - )) + Err(Error::SecurityError("No common key exchange group".to_string()).into()) }; }; @@ -746,7 +760,7 @@ impl State { Ok(Self::SendServerHello) } - fn send_server_hello(self, server: &mut Server) -> Result { + fn send_server_hello(self, server: &mut Server) -> Result { // unwrap: is ok because we set the random in handle_timeout let random = server.random.unwrap(); @@ -821,7 +835,7 @@ impl State { Ok(Self::SendEncryptedExtensions) } - fn send_encrypted_extensions(self, server: &mut Server) -> Result { + fn send_encrypted_extensions(self, server: &mut Server) -> Result { debug!("Sending EncryptedExtensions"); let negotiated_srtp = server.negotiated_srtp_profile; @@ -839,7 +853,7 @@ impl State { } } - fn send_certificate_request(self, server: &mut Server) -> Result { + fn send_certificate_request(self, server: &mut Server) -> Result { debug!("Sending CertificateRequest"); server @@ -853,7 +867,7 @@ impl State { Ok(Self::SendCertificate) } - fn send_certificate(self, server: &mut Server) -> Result { + fn send_certificate(self, server: &mut Server) -> Result { debug!("Sending Certificate"); server @@ -865,7 +879,7 @@ impl State { Ok(Self::SendCertificateVerify) } - fn send_certificate_verify(self, server: &mut Server) -> Result { + fn send_certificate_verify(self, server: &mut Server) -> Result { debug!("Sending CertificateVerify"); server @@ -881,7 +895,7 @@ impl State { Ok(Self::SendFinished) } - fn send_finished(self, server: &mut Server) -> Result { + fn send_finished(self, server: &mut Server) -> Result { trace!("Sending server Finished message"); let server_hs_secret = server.server_hs_traffic_secret.as_ref().ok_or_else(|| { @@ -922,7 +936,7 @@ impl State { } } - fn await_certificate(self, server: &mut Server) -> Result { + fn await_certificate(self, server: &mut Server) -> Result { let maybe = server .engine .next_handshake(MessageType::Certificate, &mut server.defragment_buffer)?; @@ -982,7 +996,7 @@ impl State { Ok(Self::AwaitCertificateVerify) } - fn await_certificate_verify(self, server: &mut Server) -> Result { + fn await_certificate_verify(self, server: &mut Server) -> Result { // Compute transcript hash BEFORE consuming CertificateVerify. // Per RFC 8446 §4.4.3, the hash covers all messages up to but NOT // including CertificateVerify. next_handshake() will append CV to @@ -1009,10 +1023,11 @@ impl State { // RFC 8446 §4.4.3: The signature algorithm MUST be one offered in // the signature_algorithms field of the CertificateRequest message. if !SignatureScheme::supported().contains(&scheme) { - return Err(Error::SecurityError(format!( + return Err((Error::SecurityError(format!( "Client used un-offered signature scheme: {:?}", scheme - ))); + ))) + .into()); } // Build the signed content per RFC 8446 Section 4.4.3: @@ -1054,7 +1069,7 @@ impl State { Ok(Self::AwaitFinished) } - fn await_finished(self, server: &mut Server) -> Result { + fn await_finished(self, server: &mut Server) -> Result { // Compute expected verify_data BEFORE consuming Finished // (verify_data uses transcript hash up to but not including Finished) let client_hs_secret = server @@ -1086,9 +1101,9 @@ impl State { // Constant-time comparison let is_eq: bool = verify_data.ct_eq(&*expected_verify_data).into(); if !is_eq { - return Err(Error::SecurityError( - "Client Finished verification failed".to_string(), - )); + return Err( + (Error::SecurityError("Client Finished verification failed".to_string())).into(), + ); } trace!("Client Finished verified successfully"); @@ -1129,7 +1144,7 @@ impl State { Ok(Self::AwaitApplicationData) } - fn await_application_data(self, server: &mut Server) -> Result { + fn await_application_data(self, server: &mut Server) -> Result { // Auto-trigger KeyUpdate when AEAD encryption limit is reached if server.engine.needs_key_update() && !server.engine.is_key_update_in_flight() { server.initiate_key_update()?; @@ -1193,7 +1208,7 @@ impl State { Ok(self) } - fn half_closed_local(self, server: &mut Server) -> Result { + fn half_closed_local(self, server: &mut Server) -> Result { // Write half is closed: drain incoming KeyUpdate to keep recv keys in sync, // but do not send our own KeyUpdate response. if server.engine.has_complete_handshake(MessageType::KeyUpdate) { diff --git a/src/error.rs b/src/error.rs index e9146263..c4f4889b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,38 +48,79 @@ pub enum Error { /// value to communicate from dtls13/server.rs to lib.rs #[doc(hidden)] Dtls12Fallback, - /// Parser requested more data - #[doc(hidden)] +} + +#[derive(Debug)] +pub(crate) enum InternalError { + Transient(TransientError), + Fatal(Error), +} + +#[derive(Debug)] +pub(crate) enum TransientError { ParseIncomplete, - /// Parser encountered an error kind from nom - #[doc(hidden)] - ParseError(nom::error::ErrorKind), - /// Too many records in a single packet - #[doc(hidden)] + Parse(nom::error::ErrorKind), TooManyRecords, } -impl Error { - pub(crate) fn is_transient(&self) -> bool { - matches!( - self, - Error::ParseIncomplete | Error::ParseError(_) | Error::TooManyRecords - ) +impl InternalError { + pub(crate) fn parse_incomplete() -> Self { + Self::Transient(TransientError::ParseIncomplete) + } + + pub(crate) fn parse(kind: nom::error::ErrorKind) -> Self { + Self::Transient(TransientError::Parse(kind)) + } + + pub(crate) fn too_many_records() -> Self { + Self::Transient(TransientError::TooManyRecords) + } + + pub(crate) fn into_public_error(self) -> Option { + match self { + Self::Transient(_) => None, + Self::Fatal(err) => Some(err), + } + } +} + +impl From for InternalError { + fn from(value: Error) -> Self { + Self::Fatal(value) } } -impl<'a> From>> for Error { +impl<'a> From>> for InternalError { fn from(value: nom::Err>) -> Self { match value { - nom::Err::Incomplete(_) => Error::ParseIncomplete, - nom::Err::Error(x) => Error::ParseError(x.code), - nom::Err::Failure(x) => Error::ParseError(x.code), + nom::Err::Incomplete(_) => InternalError::parse_incomplete(), + nom::Err::Error(x) => InternalError::parse(x.code), + nom::Err::Failure(x) => InternalError::parse(x.code), } } } impl std::error::Error for Error {} +impl std::fmt::Display for InternalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InternalError::Transient(err) => err.fmt(f), + InternalError::Fatal(err) => err.fmt(f), + } + } +} + +impl std::fmt::Display for TransientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TransientError::ParseIncomplete => write!(f, "parse incomplete"), + TransientError::Parse(kind) => write!(f, "parse error: {:?}", kind), + TransientError::TooManyRecords => write!(f, "too many records in packet"), + } + } +} + impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -103,9 +144,6 @@ impl std::fmt::Display for Error { Error::Dtls12Fallback => { write!(f, "dtls 1.2 fallback (internal)") } - Error::ParseIncomplete => write!(f, "parse incomplete"), - Error::ParseError(kind) => write!(f, "parse error: {:?}", kind), - Error::TooManyRecords => write!(f, "too many records in packet"), } } } diff --git a/src/lib.rs b/src/lib.rs index 5c70f8a9..1013661f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,6 +228,7 @@ mod util; mod error; pub use error::Error; +pub(crate) use error::InternalError; mod config; pub use config::{Config, ConfigBuilder, Psk, PskResolver}; From a11a5d432baaee1bf1576de39548a5ca9fbd449c Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Fri, 29 May 2026 12:50:35 +0300 Subject: [PATCH 02/10] error: use structured public error payloads --- src/auto.rs | 15 +- src/config.rs | 76 +- src/crypto/aws_lc_rs/cipher_suite.rs | 78 +- src/crypto/aws_lc_rs/hmac.rs | 7 +- src/crypto/aws_lc_rs/kx_group.rs | 22 +- src/crypto/aws_lc_rs/random.rs | 5 +- src/crypto/aws_lc_rs/sign.rs | 61 +- src/crypto/ccm_cipher.rs | 41 +- src/crypto/prf_hkdf.rs | 21 +- src/crypto/provider.rs | 66 +- src/crypto/rust_crypto/cipher_suite.rs | 118 +-- src/crypto/rust_crypto/hmac.rs | 9 +- src/crypto/rust_crypto/kx_group.rs | 23 +- src/crypto/rust_crypto/random.rs | 3 +- src/crypto/rust_crypto/sign.rs | 88 +- src/crypto/validation/mod.rs | 196 +++-- src/dtls12/client.rs | 202 +++-- src/dtls12/context.rs | 71 +- src/dtls12/engine.rs | 69 +- src/dtls12/server.rs | 157 ++-- src/dtls13/client.rs | 189 ++-- src/dtls13/engine.rs | 107 ++- src/dtls13/server.rs | 121 +-- src/error.rs | 1099 +++++++++++++++++++++++- src/lib.rs | 7 +- tests/auto/handshake.rs | 6 +- 26 files changed, 1982 insertions(+), 875 deletions(-) diff --git a/src/auto.rs b/src/auto.rs index e1ac4cc8..410b4bd4 100644 --- a/src/auto.rs +++ b/src/auto.rs @@ -29,7 +29,7 @@ use crate::dtls13::message::SignatureAlgorithmsExtension; use crate::dtls13::message::SupportedGroupsExtension; use crate::dtls13::message::UseSrtpExtension; use crate::types::NamedGroup; -use crate::{Config, DtlsCertificate, Error, Output, SeededRng}; +use crate::{Config, CryptoError, DtlsCertificate, Error, Output, SeededRng, TimeoutError}; // Extension type constants const EXT_SUPPORTED_GROUPS: u16 = 0x000A; const EXT_EC_POINT_FORMATS: u16 = 0x000B; @@ -80,14 +80,11 @@ impl HybridClientHello { let random = Random::new(&mut rng); // Start ECDHE key exchange with the first supported group (filtered) - let group = config - .kx_groups() - .next() - .ok_or_else(|| Error::CryptoError("No supported key exchange groups".into()))?; + let group = config.kx_groups().next().ok_or(Error::CryptoError( + CryptoError::NoSupportedKeyExchangeGroups, + ))?; let kx_buf = Buf::new(); - let key_exchange = group - .start_exchange(kx_buf) - .map_err(|e| Error::CryptoError(format!("Failed to start key exchange: {}", e)))?; + let key_exchange = group.start_exchange(kx_buf).map_err(Error::CryptoError)?; // ---- Build the ClientHello body ---- let mut ch_body = Buf::new(); @@ -304,7 +301,7 @@ impl ClientPending { if let Some(deadline) = self.retransmit_at { if now >= deadline { if self.retransmit_count >= self.config.flight_retries() { - return Err(Error::Timeout("hybrid ClientHello")); + return Err(Error::Timeout(TimeoutError::HybridClientHello)); } self.retransmit_count += 1; self.needs_send = true; diff --git a/src/config.rs b/src/config.rs index 02a14c01..9ae2b4c1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,11 +3,11 @@ use std::panic::{RefUnwindSafe, UnwindSafe}; use std::sync::Arc; use std::time::Duration; -use crate::Error; use crate::crypto::{CryptoProvider, SupportedDtls12CipherSuite}; use crate::crypto::{SupportedDtls13CipherSuite, SupportedKxGroup}; use crate::dtls12::message::Dtls12CipherSuite; use crate::types::{Dtls13CipherSuite, NamedGroup}; +use crate::{ConfigError, Error}; /// Callback for resolving PSK identities to shared secrets. /// @@ -507,17 +507,15 @@ impl ConfigBuilder { // Validate MTU: must be large enough for DTLS record + handshake headers if self.mtu < 64 { - return Err(Error::ConfigError(format!( - "MTU {} is too small (minimum 64)", - self.mtu - ))); + return Err(Error::ConfigError(ConfigError::MtuTooSmall { + mtu: self.mtu, + minimum: 64, + })); } // Validate aead_encryption_limit: must be at least 1 if self.aead_encryption_limit == 0 { - return Err(Error::ConfigError( - "aead_encryption_limit must be at least 1".to_string(), - )); + return Err(Error::ConfigError(ConfigError::AeadEncryptionLimitTooSmall)); } // Validate cipher suite filters: at least one version must have suites. @@ -544,9 +542,7 @@ impl ConfigBuilder { }; if dtls12_count + dtls13_count == 0 { return Err(Error::ConfigError( - "No cipher suites remain after filtering. \ - At least one DTLS 1.2 or DTLS 1.3 cipher suite must be available." - .to_string(), + ConfigError::NoCipherSuitesAfterFiltering, )); } @@ -557,10 +553,7 @@ impl ConfigBuilder { // by CryptoContext::is_cipher_suite_compatible. if has_psk && !dtls12_suites.iter().any(|cs| cs.suite().is_psk()) { return Err(Error::ConfigError( - "PSK is configured but no PSK cipher suite remains after filtering \ - DTLS 1.2 suites. Include at least one PSK suite in \ - dtls12_cipher_suites." - .to_string(), + ConfigError::PskConfiguredWithoutPskCipherSuite, )); } @@ -585,9 +578,7 @@ impl ConfigBuilder { .count(); if dtls12_kx_count == 0 { return Err(Error::ConfigError( - "DTLS 1.2 cipher suites are enabled but no compatible key exchange \ - groups remain after filtering." - .to_string(), + ConfigError::NoDtls12KeyExchangeGroupsAfterFiltering, )); } } @@ -598,9 +589,7 @@ impl ConfigBuilder { .count(); if kx_count == 0 { return Err(Error::ConfigError( - "DTLS 1.3 cipher suites are enabled but no key exchange groups \ - remain after filtering." - .to_string(), + ConfigError::NoDtls13KeyExchangeGroupsAfterFiltering, )); } } @@ -707,8 +696,9 @@ mod tests { #[test] fn rejects_zero_mtu() { match Config::builder().mtu(0).build() { - Err(Error::ConfigError(msg)) => { - assert!(msg.contains("MTU"), "error should mention MTU: {msg}") + Err(Error::ConfigError(ConfigError::MtuTooSmall { mtu, minimum })) => { + assert_eq!(mtu, 0); + assert_eq!(minimum, 64); } Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!("expected error for MTU=0"), @@ -718,8 +708,9 @@ mod tests { #[test] fn rejects_small_mtu() { match Config::builder().mtu(32).build() { - Err(Error::ConfigError(msg)) => { - assert!(msg.contains("MTU"), "error should mention MTU: {msg}") + Err(Error::ConfigError(ConfigError::MtuTooSmall { mtu, minimum })) => { + assert_eq!(mtu, 32); + assert_eq!(minimum, 64); } Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!("expected error for MTU=32"), @@ -737,10 +728,7 @@ mod tests { #[test] fn rejects_zero_aead_limit() { match Config::builder().aead_encryption_limit(0).build() { - Err(Error::ConfigError(msg)) => assert!( - msg.contains("aead_encryption_limit"), - "error should mention aead_encryption_limit: {msg}" - ), + Err(Error::ConfigError(ConfigError::AeadEncryptionLimitTooSmall)) => {} Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!("expected error for aead_encryption_limit=0"), } @@ -811,12 +799,7 @@ mod tests { .dtls13_cipher_suites(&[]) .build() { - Err(Error::ConfigError(msg)) => { - assert!( - msg.contains("No cipher suites"), - "error should mention cipher suites: {msg}" - ) - } + Err(Error::ConfigError(ConfigError::NoCipherSuitesAfterFiltering)) => {} Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!("expected error when both versions are empty"), } @@ -825,12 +808,10 @@ mod tests { #[test] fn empty_kx_groups_filter_rejected() { match Config::builder().kx_groups(&[]).build() { - Err(Error::ConfigError(msg)) => { - assert!( - msg.contains("key exchange"), - "error should mention key exchange: {msg}" - ) - } + Err(Error::ConfigError( + ConfigError::NoDtls12KeyExchangeGroupsAfterFiltering + | ConfigError::NoDtls13KeyExchangeGroupsAfterFiltering, + )) => {} Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!("expected error for empty kx groups"), } @@ -928,9 +909,7 @@ mod tests { .dtls13_cipher_suites(&[]) .build(); match result { - Err(Error::ConfigError(msg)) => { - assert!(msg.contains("PSK"), "error should mention PSK: {msg}") - } + Err(Error::ConfigError(ConfigError::PskConfiguredWithoutPskCipherSuite)) => {} Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!("expected error for PSK config with only non-PSK suites"), } @@ -954,9 +933,7 @@ mod tests { .dtls12_cipher_suites(&[Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256]) .build(); match result { - Err(Error::ConfigError(msg)) => { - assert!(msg.contains("PSK"), "error should mention PSK: {msg}") - } + Err(Error::ConfigError(ConfigError::PskConfiguredWithoutPskCipherSuite)) => {} Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!( "expected error for PSK config with only non-PSK DTLS 1.2 suites, \ @@ -988,10 +965,7 @@ mod tests { .kx_groups(&[]) .build(); match result { - Err(Error::ConfigError(msg)) => assert!( - msg.contains("key exchange"), - "error should mention key exchange groups: {msg}" - ), + Err(Error::ConfigError(ConfigError::NoDtls12KeyExchangeGroupsAfterFiltering)) => {} Err(other) => panic!("expected ConfigError, got: {other:?}"), Ok(_) => panic!( "expected error when a cert-based DTLS 1.2 suite is enabled \ diff --git a/src/crypto/aws_lc_rs/cipher_suite.rs b/src/crypto/aws_lc_rs/cipher_suite.rs index 3bc02c62..c775cfd8 100644 --- a/src/crypto/aws_lc_rs/cipher_suite.rs +++ b/src/crypto/aws_lc_rs/cipher_suite.rs @@ -10,6 +10,7 @@ use crate::buffer::{Buf, TmpBuf}; use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::Dtls12CipherSuite; use crate::types::{Dtls13CipherSuite, HashAlgorithm}; +use crate::{CryptoError, CryptoOperation}; /// AES-GCM cipher implementation using aws-lc-rs. struct AesGcm { @@ -23,15 +24,15 @@ impl std::fmt::Debug for AesGcm { } impl AesGcm { - fn new(key: &[u8]) -> Result { + fn new(key: &[u8]) -> Result { let algorithm = match key.len() { 16 => &AES_128_GCM, 32 => &AES_256_GCM, - _ => return Err(format!("Invalid key size for AES-GCM: {}", key.len())), + _ => return Err(CryptoError::InvalidAesGcmKeySize { actual: key.len() }), }; let unbound_key = UnboundKey::new(algorithm, key) - .map_err(|_| "Failed to create AES-GCM cipher".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::CreateCipher))?; Ok(AesGcm { key: LessSafeKey::new(unbound_key), @@ -40,33 +41,41 @@ impl AesGcm { } impl Cipher for AesGcm { - fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { let aws_nonce = - AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| "Invalid nonce".to_string())?; + AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| CryptoError::InvalidNonce)?; let aws_aad = AwsAad::from(&aad[..]); self.key .seal_in_place_append_tag(aws_nonce, aws_aad, plaintext) - .map_err(|_| "AES-GCM encryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Encrypt))?; Ok(()) } - fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn decrypt( + &mut self, + ciphertext: &mut TmpBuf, + aad: Aad, + nonce: Nonce, + ) -> Result<(), CryptoError> { if ciphertext.len() < 16 { - return Err(format!("Ciphertext too short: {}", ciphertext.len())); + return Err(CryptoError::CiphertextTooShort { + minimum: 16, + actual: ciphertext.len(), + }); } let aws_nonce = - AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| "Invalid nonce".to_string())?; + AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| CryptoError::InvalidNonce)?; let aws_aad = AwsAad::from(&aad[..]); let plaintext = self .key .open_in_place(aws_nonce, aws_aad, ciphertext.as_mut()) - .map_err(|_| "AES-GCM decryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Decrypt))?; let plaintext_len = plaintext.len(); ciphertext.truncate(plaintext_len); @@ -88,17 +97,14 @@ impl std::fmt::Debug for ChaCha20Poly1305Cipher { } impl ChaCha20Poly1305Cipher { - fn new(key: &[u8]) -> Result { + fn new(key: &[u8]) -> Result { // The UnboundKey::new call also validates length, but doesnt give us // a reasonable error message. This makes it equivalent to RustCrypto if key.len() != 32 { - return Err(format!( - "Invalid key size for CHACHA20-POLY1305: {}", - key.len() - )); + return Err(CryptoError::InvalidChacha20Poly1305KeySize { actual: key.len() }); } let unbound_key = UnboundKey::new(&CHACHA20_POLY1305, key) - .map_err(|_| "Failed to create ChaCha20-Poly1305 cipher".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::CreateCipher))?; Ok(ChaCha20Poly1305Cipher { key: LessSafeKey::new(unbound_key), @@ -107,33 +113,41 @@ impl ChaCha20Poly1305Cipher { } impl Cipher for ChaCha20Poly1305Cipher { - fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { let aws_nonce = - AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| "Invalid nonce".to_string())?; + AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| CryptoError::InvalidNonce)?; let aws_aad = AwsAad::from(&aad[..]); self.key .seal_in_place_append_tag(aws_nonce, aws_aad, plaintext) - .map_err(|_| "ChaCha20-Poly1305 encryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Encrypt))?; Ok(()) } - fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn decrypt( + &mut self, + ciphertext: &mut TmpBuf, + aad: Aad, + nonce: Nonce, + ) -> Result<(), CryptoError> { if ciphertext.len() < 16 { - return Err(format!("Ciphertext too short: {}", ciphertext.len())); + return Err(CryptoError::CiphertextTooShort { + minimum: 16, + actual: ciphertext.len(), + }); } let aws_nonce = - AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| "Invalid nonce".to_string())?; + AwsNonce::try_assume_unique_for_key(&nonce).map_err(|_| CryptoError::InvalidNonce)?; let aws_aad = AwsAad::from(&aad[..]); let plaintext = self .key .open_in_place(aws_nonce, aws_aad, ciphertext.as_mut()) - .map_err(|_| "ChaCha20-Poly1305 decryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Decrypt))?; let plaintext_len = plaintext.len(); ciphertext.truncate(plaintext_len); @@ -167,7 +181,7 @@ impl SupportedDtls12CipherSuite for Aes128GcmSha256 { 16 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } } @@ -197,7 +211,7 @@ impl SupportedDtls12CipherSuite for Aes256GcmSha384 { 16 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } } @@ -227,7 +241,7 @@ impl SupportedDtls12CipherSuite for ChaCha20Poly1305Sha256 { 16 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(ChaCha20Poly1305Cipher::new(key)?)) } } @@ -257,7 +271,7 @@ impl SupportedDtls12CipherSuite for PskAes128Ccm8 { 8 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(crate::crypto::ccm_cipher::AesCcm8Cipher::new( key, )?)) @@ -307,7 +321,7 @@ impl SupportedDtls13CipherSuite for Tls13Aes128GcmSha256 { 16 // GCM tag } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } @@ -341,7 +355,7 @@ impl SupportedDtls13CipherSuite for Tls13Aes256GcmSha384 { 16 // GCM tag } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } @@ -375,7 +389,7 @@ impl SupportedDtls13CipherSuite for Tls13ChaCha20Poly1305Sha256 { 16 // Poly1305 tag } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(ChaCha20Poly1305Cipher::new(key)?)) } @@ -430,8 +444,8 @@ mod test { // incorrect length (should be 32) let result = ChaCha20Poly1305Cipher::new(&[0, 1, 2, 3, 4, 5]); assert_eq!( - "Invalid key size for CHACHA20-POLY1305: 6", - &result.unwrap_err() + CryptoError::InvalidChacha20Poly1305KeySize { actual: 6 }, + result.unwrap_err() ); } } diff --git a/src/crypto/aws_lc_rs/hmac.rs b/src/crypto/aws_lc_rs/hmac.rs index eaadc3ce..728f019f 100644 --- a/src/crypto/aws_lc_rs/hmac.rs +++ b/src/crypto/aws_lc_rs/hmac.rs @@ -3,14 +3,15 @@ use aws_lc_rs::hmac; use super::super::HmacProvider; +use crate::CryptoError; use crate::types::HashAlgorithm; /// Get HMAC algorithm from hash algorithm. -fn hmac_algorithm(hash: HashAlgorithm) -> Result { +fn hmac_algorithm(hash: HashAlgorithm) -> Result { match hash { HashAlgorithm::SHA256 => Ok(hmac::HMAC_SHA256), HashAlgorithm::SHA384 => Ok(hmac::HMAC_SHA384), - _ => Err(format!("Unsupported HMAC hash algorithm: {:?}", hash)), + _ => Err(CryptoError::UnsupportedHmacHash(hash)), } } @@ -25,7 +26,7 @@ impl HmacProvider for AwsLcHmacProvider { key: &[u8], data: &[u8], out: &mut [u8], - ) -> Result { + ) -> Result { let algorithm = hmac_algorithm(hash)?; let hmac_key = hmac::Key::new(algorithm, key); let tag = hmac::sign(&hmac_key, data); diff --git a/src/crypto/aws_lc_rs/kx_group.rs b/src/crypto/aws_lc_rs/kx_group.rs index 968f15e1..21e38ebf 100644 --- a/src/crypto/aws_lc_rs/kx_group.rs +++ b/src/crypto/aws_lc_rs/kx_group.rs @@ -6,6 +6,7 @@ use aws_lc_rs::agreement::{EphemeralPrivateKey, agree_ephemeral}; use super::super::{ActiveKeyExchange, SupportedKxGroup}; use crate::buffer::Buf; use crate::types::NamedGroup; +use crate::{CryptoError, CryptoOperation}; /// ECDHE key exchange implementation. struct EcdhKeyExchange { @@ -24,21 +25,21 @@ impl std::fmt::Debug for EcdhKeyExchange { } impl EcdhKeyExchange { - fn new(group: NamedGroup, mut buf: Buf) -> Result { + fn new(group: NamedGroup, mut buf: Buf) -> Result { let algorithm = match group { NamedGroup::X25519 => &X25519, NamedGroup::Secp256r1 => &ECDH_P256, NamedGroup::Secp384r1 => &ECDH_P384, - _ => return Err("Unsupported group".to_string()), + _ => return Err(CryptoError::UnsupportedKeyExchangeGroup(group)), }; let rng = aws_lc_rs::rand::SystemRandom::new(); let private_key = EphemeralPrivateKey::generate(algorithm, &rng) - .map_err(|_| "Failed to generate ephemeral key".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::GenerateEphemeralKey))?; let pk = private_key .compute_public_key() - .map_err(|_| "Failed to compute public key".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::ComputePublicKey))?; buf.clear(); buf.extend_from_slice(pk.as_ref()); @@ -65,7 +66,7 @@ impl ActiveKeyExchange for EcdhKeyExchange { &self.public_key } - fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), String> { + fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), CryptoError> { let algorithm = self.algorithm(); let peer_key = UnparsedPublicKey::new(algorithm, peer_pub); @@ -80,7 +81,10 @@ impl ActiveKeyExchange for EcdhKeyExchange { Ok(()) }, ) - .map_err(|e| e.to_string()) + .map_err(|e| CryptoError::ProviderFailure { + operation: CryptoOperation::CompleteKeyExchange, + reason: e.to_string(), + }) } fn group(&self) -> NamedGroup { @@ -97,7 +101,7 @@ impl SupportedKxGroup for X25519Kx { NamedGroup::X25519 } - fn start_exchange(&self, buf: Buf) -> Result, String> { + fn start_exchange(&self, buf: Buf) -> Result, CryptoError> { Ok(Box::new(EcdhKeyExchange::new(NamedGroup::X25519, buf)?)) } } @@ -111,7 +115,7 @@ impl SupportedKxGroup for P256 { NamedGroup::Secp256r1 } - fn start_exchange(&self, buf: Buf) -> Result, String> { + fn start_exchange(&self, buf: Buf) -> Result, CryptoError> { Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp256r1, buf)?)) } } @@ -125,7 +129,7 @@ impl SupportedKxGroup for P384 { NamedGroup::Secp384r1 } - fn start_exchange(&self, buf: Buf) -> Result, String> { + fn start_exchange(&self, buf: Buf) -> Result, CryptoError> { Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp384r1, buf)?)) } } diff --git a/src/crypto/aws_lc_rs/random.rs b/src/crypto/aws_lc_rs/random.rs index 8da15b50..41737d75 100644 --- a/src/crypto/aws_lc_rs/random.rs +++ b/src/crypto/aws_lc_rs/random.rs @@ -1,17 +1,18 @@ //! Secure random number generation using aws-lc-rs. use super::super::SecureRandom; +use crate::{CryptoError, CryptoOperation}; /// Secure random number generator implementation. #[derive(Debug)] pub(super) struct AwsLcSecureRandom; impl SecureRandom for AwsLcSecureRandom { - fn fill(&self, buf: &mut [u8]) -> Result<(), String> { + fn fill(&self, buf: &mut [u8]) -> Result<(), CryptoError> { use aws_lc_rs::rand::SecureRandom as _; let rng = aws_lc_rs::rand::SystemRandom::new(); rng.fill(buf) - .map_err(|_| "Failed to generate random bytes".to_string()) + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::FillRandom)) } } diff --git a/src/crypto/aws_lc_rs/sign.rs b/src/crypto/aws_lc_rs/sign.rs index d1cc2b88..256c0b5b 100644 --- a/src/crypto/aws_lc_rs/sign.rs +++ b/src/crypto/aws_lc_rs/sign.rs @@ -15,6 +15,7 @@ use super::super::{KeyProvider, SignatureVerifier, SigningKey, check_verify_sche use super::super::{OID_P256, OID_P384}; use crate::buffer::Buf; use crate::types::{HashAlgorithm, NamedGroup, SignatureAlgorithm}; +use crate::{CryptoError, CryptoOperation}; /// ECDSA signing key implementation. struct EcdsaSigningKey { @@ -31,19 +32,24 @@ impl std::fmt::Debug for EcdsaSigningKey { } impl SigningKey for EcdsaSigningKey { - fn sign(&mut self, data: &[u8], hash_alg: HashAlgorithm, buf: &mut Buf) -> Result<(), String> { + fn sign( + &mut self, + data: &[u8], + hash_alg: HashAlgorithm, + buf: &mut Buf, + ) -> Result<(), CryptoError> { let key_hash = self.hash_algorithm(); if hash_alg != key_hash { - return Err(format!( - "aws-lc-rs ECDSA key is locked to {:?} but {:?} was requested", - key_hash, hash_alg - )); + return Err(CryptoError::SigningKeyHashMismatch { + key_hash, + requested: hash_alg, + }); } let rng = aws_lc_rs::rand::SystemRandom::new(); let signature = self .key_pair .sign(&rng, data) - .map_err(|_| "Signing failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Sign))?; buf.clear(); buf.extend_from_slice(signature.as_ref()); Ok(()) @@ -80,7 +86,7 @@ impl SigningKey for EcdsaSigningKey { pub(super) struct AwsLcKeyProvider; impl KeyProvider for AwsLcKeyProvider { - fn load_private_key(&self, key_der: &[u8]) -> Result, String> { + fn load_private_key(&self, key_der: &[u8]) -> Result, CryptoError> { // Try PKCS#8 DER format first (most common) if let Ok(key_pair) = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, key_der) { return Ok(Box::new(EcdsaSigningKey { @@ -115,9 +121,9 @@ impl KeyProvider for AwsLcKeyProvider { let ec_alg_oid = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); let curve_params_der = curve_oid .to_der() - .map_err(|_| "Failed to encode curve OID".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::EncodeKey))?; let curve_params_any = der::asn1::AnyRef::try_from(curve_params_der.as_slice()) - .map_err(|_| "Failed to create AnyRef".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::EncodeKey))?; let algorithm = spki::AlgorithmIdentifierRef { oid: ec_alg_oid, @@ -132,7 +138,7 @@ impl KeyProvider for AwsLcKeyProvider { let pkcs8_der = pkcs8 .to_der() - .map_err(|_| "Failed to encode PKCS#8".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::EncodeKey))?; let p256_curve = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7"); if curve_oid == p256_curve { @@ -169,7 +175,7 @@ impl KeyProvider for AwsLcKeyProvider { } } - Err("Failed to parse private key in any supported format".to_string()) + Err(CryptoError::InvalidPrivateKey) } } @@ -185,42 +191,42 @@ impl SignatureVerifier for AwsLcSignatureVerifier { signature: &[u8], hash_alg: HashAlgorithm, sig_alg: SignatureAlgorithm, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { if sig_alg != SignatureAlgorithm::ECDSA { - return Err(format!("Unsupported signature algorithm: {:?}", sig_alg)); + return Err(CryptoError::UnsupportedSignatureAlgorithm(sig_alg)); } - let cert = X509Certificate::from_der(cert_der) - .map_err(|e| format!("Failed to parse certificate: {e}"))?; + let cert = + X509Certificate::from_der(cert_der).map_err(|e| CryptoError::ProviderFailure { + operation: CryptoOperation::VerifySignature, + reason: e.to_string(), + })?; let spki = &cert.tbs_certificate.subject_public_key_info; const OID_EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); if spki.algorithm.oid != OID_EC_PUBLIC_KEY { - return Err(format!( - "Unsupported public key algorithm: {}", - spki.algorithm.oid - )); + return Err(CryptoError::UnsupportedPublicKeyAlgorithm); } let pubkey_bytes = spki .subject_public_key .as_bytes() - .ok_or_else(|| "Invalid EC subject_public_key bitstring".to_string())?; + .ok_or(CryptoError::InvalidSubjectPublicKey)?; let curve_oid: ObjectIdentifier = spki .algorithm .parameters .as_ref() - .ok_or("Missing EC curve parameter in certificate")? + .ok_or(CryptoError::MissingEcCurveParameter)? .decode_as() - .map_err(|_| "Invalid EC curve parameter in certificate".to_string())?; + .map_err(|_| CryptoError::InvalidEcCurveParameter)?; let group = match curve_oid { OID_P256 => NamedGroup::Secp256r1, OID_P384 => NamedGroup::Secp384r1, - _ => return Err(format!("Unsupported EC curve: {}", curve_oid)), + _ => return Err(CryptoError::UnsupportedEcCurve(curve_oid.to_string())), }; check_verify_scheme(sig_alg, hash_alg, group)?; @@ -235,12 +241,9 @@ impl SignatureVerifier for AwsLcSignatureVerifier { }; let public_key = UnparsedPublicKey::new(algorithm, pubkey_bytes); - public_key.verify(data, signature).map_err(|_| { - format!( - "ECDSA signature verification failed for {:?} {:?}", - hash_alg, group - ) - }) + public_key + .verify(data, signature) + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::VerifySignature)) } } diff --git a/src/crypto/ccm_cipher.rs b/src/crypto/ccm_cipher.rs index d5837ace..be2c8600 100644 --- a/src/crypto/ccm_cipher.rs +++ b/src/crypto/ccm_cipher.rs @@ -9,6 +9,7 @@ use ccm::consts::{U8, U12}; use super::{Aad, Cipher, Nonce}; use crate::buffer::{Buf, TmpBuf}; +use crate::{CryptoError, CryptoOperation}; /// AES-128-CCM with 8-byte tag, 12-byte nonce. type Aes128Ccm8 = ccm::Ccm; @@ -25,12 +26,12 @@ impl std::fmt::Debug for AesCcm8Cipher { } impl AesCcm8Cipher { - pub fn new(key: &[u8]) -> Result { + pub fn new(key: &[u8]) -> Result { if key.len() != 16 { - return Err(format!("Invalid key size for AES-128-CCM-8: {}", key.len())); + return Err(CryptoError::InvalidAes128Ccm8KeySize { actual: key.len() }); } let cipher = Aes128Ccm8::new_from_slice(key) - .map_err(|_| "Failed to create AES-128-CCM-8 cipher".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::CreateCipher))?; Ok(AesCcm8Cipher { cipher: Box::new(cipher), }) @@ -38,19 +39,19 @@ impl AesCcm8Cipher { } impl Cipher for AesCcm8Cipher { - fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + }); } let ccm_nonce = ccm::aead::generic_array::GenericArray::from_slice(&nonce[..12]); let tag = self .cipher .encrypt_in_place_detached(ccm_nonce, &aad[..], plaintext.as_mut()) - .map_err(|_| "AES-128-CCM-8 encryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Encrypt))?; // Append the 8-byte tag plaintext.extend_from_slice(&tag); @@ -58,16 +59,24 @@ impl Cipher for AesCcm8Cipher { Ok(()) } - fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn decrypt( + &mut self, + ciphertext: &mut TmpBuf, + aad: Aad, + nonce: Nonce, + ) -> Result<(), CryptoError> { if ciphertext.len() < 8 { - return Err(format!("Ciphertext too short: {}", ciphertext.len())); + return Err(CryptoError::CiphertextTooShort { + minimum: 8, + actual: ciphertext.len(), + }); } if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + }); } let ccm_nonce = ccm::aead::generic_array::GenericArray::from_slice(&nonce[..12]); @@ -83,7 +92,7 @@ impl Cipher for AesCcm8Cipher { self.cipher .decrypt_in_place_detached(ccm_nonce, &aad[..], ciphertext.as_mut(), &tag) - .map_err(|_| "AES-128-CCM-8 decryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Decrypt))?; Ok(()) } diff --git a/src/crypto/prf_hkdf.rs b/src/crypto/prf_hkdf.rs index 050210a2..81eb22d6 100644 --- a/src/crypto/prf_hkdf.rs +++ b/src/crypto/prf_hkdf.rs @@ -4,6 +4,7 @@ //! This module provides generic implementations so that crypto backends only //! need to implement [`HmacProvider`] — no separate PRF or HKDF providers. +use crate::CryptoError; use crate::buffer::Buf; use crate::types::HashAlgorithm; @@ -29,7 +30,7 @@ pub fn prf_tls12( output_len: usize, scratch: &mut Buf, hash: HashAlgorithm, -) -> Result<(), String> { +) -> Result<(), CryptoError> { let mut hmac_a = [0u8; MAX_HASH_LEN]; // Build label + seed @@ -79,7 +80,7 @@ pub fn hkdf_extract( salt: &[u8], ikm: &[u8], out: &mut Buf, -) -> Result<(), String> { +) -> Result<(), CryptoError> { out.clear(); let hash_len = hash.output_len(); @@ -105,11 +106,11 @@ pub fn hkdf_expand( info: &[u8], out: &mut Buf, output_len: usize, -) -> Result<(), String> { +) -> Result<(), CryptoError> { let hash_len = hash.output_len(); let n = output_len.div_ceil(hash_len); if n > 255 { - return Err("HKDF output too long".into()); + return Err(CryptoError::HkdfOutputTooLong); } let mut t_prev = [0u8; MAX_HASH_LEN]; @@ -143,7 +144,7 @@ pub fn hkdf_expand_label( context: &[u8], out: &mut Buf, output_len: usize, -) -> Result<(), String> { +) -> Result<(), CryptoError> { let info = build_hkdf_label(b"tls13 ", label, context, output_len)?; hkdf_expand(hmac, hash, secret, &info, out, output_len) } @@ -159,7 +160,7 @@ pub fn hkdf_expand_label_dtls13( context: &[u8], out: &mut Buf, output_len: usize, -) -> Result<(), String> { +) -> Result<(), CryptoError> { let info = build_hkdf_label(b"dtls13", label, context, output_len)?; hkdf_expand(hmac, hash, secret, &info, out, output_len) } @@ -178,17 +179,17 @@ fn build_hkdf_label( label: &[u8], context: &[u8], output_len: usize, -) -> Result, String> { +) -> Result, CryptoError> { let full_label_len = prefix.len() + label.len(); if full_label_len > 255 { - return Err("Label too long for HKDF-Expand-Label".into()); + return Err(CryptoError::HkdfLabelTooLong); } if context.len() > 255 { - return Err("Context too long for HKDF-Expand-Label".into()); + return Err(CryptoError::HkdfContextTooLong); } if output_len > 65535 { - return Err("Output length too large for HKDF-Expand-Label".into()); + return Err(CryptoError::HkdfOutputLengthTooLarge); } let info_len = 2 + 1 + full_label_len + 1 + context.len(); diff --git a/src/crypto/provider.rs b/src/crypto/provider.rs index 04cdee0d..cdff2992 100644 --- a/src/crypto/provider.rs +++ b/src/crypto/provider.rs @@ -70,6 +70,7 @@ //! ## Example: Custom Cipher Suite //! //! ``` +//! use dimpl::CryptoError; //! use dimpl::crypto::{SupportedDtls12CipherSuite, Cipher, Dtls12CipherSuite, HashAlgorithm}; //! use dimpl::crypto::{Buf, TmpBuf}; //! use dimpl::crypto::{Aad, Nonce}; @@ -78,16 +79,16 @@ //! struct MyCipher; //! //! impl MyCipher { -//! fn new(_key: &[u8]) -> Result { +//! fn new(_key: &[u8]) -> Result { //! Ok(Self) //! } //! } //! //! impl Cipher for MyCipher { -//! fn encrypt(&mut self, _: &mut Buf, _: Aad, _: Nonce) -> Result<(), String> { +//! fn encrypt(&mut self, _: &mut Buf, _: Aad, _: Nonce) -> Result<(), CryptoError> { //! Ok(()) //! } -//! fn decrypt(&mut self, _: &mut TmpBuf, _: Aad, _: Nonce) -> Result<(), String> { +//! fn decrypt(&mut self, _: &mut TmpBuf, _: Aad, _: Nonce) -> Result<(), CryptoError> { //! Ok(()) //! } //! } @@ -116,7 +117,7 @@ //! 16 // 128-bit authentication tag //! } //! -//! fn create_cipher(&self, key: &[u8]) -> Result, String> { +//! fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { //! // Create your cipher implementation here //! Ok(Box::new(MyCipher::new(key)?)) //! } @@ -149,6 +150,7 @@ use crate::buffer::{Buf, TmpBuf}; use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::Dtls12CipherSuite; use crate::types::{Dtls13CipherSuite, HashAlgorithm, NamedGroup, SignatureAlgorithm}; +use crate::{CertificateError, CryptoError}; /// OID for the P-256 elliptic curve (secp256r1 / prime256v1). #[cfg(feature = "_crypto-common")] @@ -183,10 +185,15 @@ impl CryptoSafe for T {} /// AEAD cipher for in-place encryption/decryption. pub trait Cipher: CryptoSafe { /// Encrypt plaintext in-place, appending authentication tag. - fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String>; + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError>; /// Decrypt ciphertext in-place, verifying and removing authentication tag. - fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String>; + fn decrypt( + &mut self, + ciphertext: &mut TmpBuf, + aad: Aad, + nonce: Nonce, + ) -> Result<(), CryptoError>; } /// Stateful hash context for incremental hashing. @@ -202,7 +209,12 @@ pub trait HashContext: CryptoSafe { /// Signing key for generating digital signatures. pub trait SigningKey: CryptoSafe { /// Sign data using the specified hash algorithm and return the signature. - fn sign(&mut self, data: &[u8], hash_alg: HashAlgorithm, out: &mut Buf) -> Result<(), String>; + fn sign( + &mut self, + data: &[u8], + hash_alg: HashAlgorithm, + out: &mut Buf, + ) -> Result<(), CryptoError>; /// Signature algorithm used by this key. fn algorithm(&self) -> SignatureAlgorithm; @@ -225,7 +237,7 @@ pub trait ActiveKeyExchange: CryptoSafe { fn pub_key(&self) -> &[u8]; /// Complete exchange with peer's public key, returning shared secret. - fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), String>; + fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), CryptoError>; /// Get the named group for this exchange. fn group(&self) -> NamedGroup; @@ -265,7 +277,7 @@ pub trait SupportedDtls12CipherSuite: CryptoSafe { } /// Create a cipher instance with the given key. - fn create_cipher(&self, key: &[u8]) -> Result, String>; + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError>; } /// Key exchange group support (factory for ActiveKeyExchange). @@ -275,7 +287,7 @@ pub trait SupportedKxGroup: CryptoSafe { /// Start a new key exchange, generating ephemeral keypair. /// The provided `buf` will be used to store the public key. - fn start_exchange(&self, buf: Buf) -> Result, String>; + fn start_exchange(&self, buf: Buf) -> Result, CryptoError>; } /// Signature verification against certificates. @@ -288,7 +300,7 @@ pub trait SignatureVerifier: CryptoSafe { signature: &[u8], hash_alg: HashAlgorithm, sig_alg: SignatureAlgorithm, - ) -> Result<(), String>; + ) -> Result<(), CryptoError>; } /// Allow-list of supported (signature, hash, curve) combinations for @@ -331,17 +343,18 @@ pub fn check_verify_scheme( sig_alg: SignatureAlgorithm, hash_alg: HashAlgorithm, group: NamedGroup, -) -> Result<(), String> { +) -> Result<(), CryptoError> { if SUPPORTED_VERIFY_SCHEMES .iter() .any(|(s, h, g)| *s == sig_alg && *h == hash_alg && *g == group) { Ok(()) } else { - Err(format!( - "Unsupported signature verification: {:?} + {:?} + {:?}", - sig_alg, hash_alg, group - )) + Err(CryptoError::UnsupportedSignatureVerification { + signature: sig_alg, + hash: hash_alg, + group, + }) } } @@ -350,40 +363,39 @@ pub fn check_verify_scheme( /// Used by DTLS 1.3 to verify that the [`SignatureScheme`](crate::types::SignatureScheme) /// in `CertificateVerify` is consistent with the peer's certificate key. #[cfg(feature = "_crypto-common")] -pub fn cert_named_group(cert_der: &[u8]) -> Result { +pub fn cert_named_group(cert_der: &[u8]) -> Result { use der::Decode; use spki::ObjectIdentifier; use x509_cert::Certificate as X509Certificate; - let cert = X509Certificate::from_der(cert_der) - .map_err(|e| format!("Failed to parse certificate: {e}"))?; + let cert = X509Certificate::from_der(cert_der).map_err(|_| CertificateError::ParseFailed)?; let spki = &cert.tbs_certificate.subject_public_key_info; let curve_oid: ObjectIdentifier = spki .algorithm .parameters .as_ref() - .ok_or("Missing EC curve parameter in certificate")? + .ok_or(CertificateError::MissingEcCurveParameter)? .decode_as() - .map_err(|_| "Invalid EC curve parameter in certificate".to_string())?; + .map_err(|_| CertificateError::InvalidEcCurveParameter)?; match curve_oid { OID_P256 => Ok(NamedGroup::Secp256r1), OID_P384 => Ok(NamedGroup::Secp384r1), - _ => Err(format!("Unsupported EC curve: {}", curve_oid)), + _ => Err(CertificateError::UnsupportedEcCurve(curve_oid.to_string())), } } /// Private key parser (factory for SigningKey). pub trait KeyProvider: CryptoSafe { /// Parse and load a private key from DER/PEM bytes. - fn load_private_key(&self, key_der: &[u8]) -> Result, String>; + fn load_private_key(&self, key_der: &[u8]) -> Result, CryptoError>; } /// Secure random number generator. pub trait SecureRandom: CryptoSafe { /// Fill buffer with cryptographically secure random bytes. - fn fill(&self, buf: &mut [u8]) -> Result<(), String>; + fn fill(&self, buf: &mut [u8]) -> Result<(), CryptoError>; } /// Hash provider (factory for HashContext). @@ -395,7 +407,7 @@ pub trait HashProvider: CryptoSafe { /// HMAC provider for computing HMAC signatures. pub trait HmacProvider: CryptoSafe { /// Compute HMAC-SHA256(key, data) and return the result. - fn hmac_sha256(&self, key: &[u8], data: &[u8]) -> Result<[u8; 32], String> { + fn hmac_sha256(&self, key: &[u8], data: &[u8]) -> Result<[u8; 32], CryptoError> { let mut out = [0u8; 32]; self.hmac(HashAlgorithm::SHA256, key, data, &mut out)?; Ok(out) @@ -410,7 +422,7 @@ pub trait HmacProvider: CryptoSafe { key: &[u8], data: &[u8], out: &mut [u8], - ) -> Result; + ) -> Result; } // ============================================================================ @@ -446,7 +458,7 @@ pub trait SupportedDtls13CipherSuite: CryptoSafe { } /// Create a cipher instance with the given key. - fn create_cipher(&self, key: &[u8]) -> Result, String>; + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError>; /// Compute a mask for record number encryption (RFC 9147 Section 4.2.3). /// diff --git a/src/crypto/rust_crypto/cipher_suite.rs b/src/crypto/rust_crypto/cipher_suite.rs index dc4ab0db..acd9b51b 100644 --- a/src/crypto/rust_crypto/cipher_suite.rs +++ b/src/crypto/rust_crypto/cipher_suite.rs @@ -11,6 +11,7 @@ use crate::buffer::{Buf, TmpBuf}; use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::Dtls12CipherSuite; use crate::types::{Dtls13CipherSuite, HashAlgorithm}; +use crate::{CryptoError, CryptoOperation}; /// AES-GCM cipher implementation using RustCrypto. enum AesGcm { @@ -28,7 +29,7 @@ impl std::fmt::Debug for AesGcm { } impl AesGcm { - fn new(key: &[u8]) -> Result { + fn new(key: &[u8]) -> Result { match key.len() { 16 => { let key = Key::::from_slice(key); @@ -38,23 +39,25 @@ impl AesGcm { let key = Key::::from_slice(key); Ok(AesGcm::Aes256(Box::new(Aes256Gcm::new(key)))) } - _ => Err(format!("Invalid key size for AES-GCM: {}", key.len())), + _ => Err(CryptoError::InvalidAesGcmKeySize { actual: key.len() }), } } } impl Cipher for AesGcm { - fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { // AES-GCM nonce is 12 bytes if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + }); } // Create nonce from the provided nonce bytes - let nonce_array: [u8; 12] = nonce[..12].try_into().map_err(|_| "Invalid nonce")?; + let nonce_array: [u8; 12] = nonce[..12] + .try_into() + .map_err(|_| CryptoError::InvalidNonce)?; match self { AesGcm::Aes128(cipher) => { @@ -63,7 +66,7 @@ impl Cipher for AesGcm { let aes_nonce = GenericArray::::clone_from_slice(&nonce_array); cipher .encrypt_in_place(&aes_nonce, &aad, data) - .map_err(|_| "AES-GCM encryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Encrypt))?; } AesGcm::Aes256(cipher) => { // Create nonce from fixed-size array - AesNonce is GenericArray @@ -71,28 +74,38 @@ impl Cipher for AesGcm { let aes_nonce = GenericArray::::clone_from_slice(&nonce_array); cipher .encrypt_in_place(&aes_nonce, &aad, data) - .map_err(|_| "AES-GCM encryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Encrypt))?; } } Ok(()) } - fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn decrypt( + &mut self, + ciphertext: &mut TmpBuf, + aad: Aad, + nonce: Nonce, + ) -> Result<(), CryptoError> { if ciphertext.len() < 16 { - return Err(format!("Ciphertext too short: {}", ciphertext.len())); + return Err(CryptoError::CiphertextTooShort { + minimum: 16, + actual: ciphertext.len(), + }); } // AES-GCM nonce is 12 bytes if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + }); } // Create nonce from the provided nonce bytes - let nonce_array: [u8; 12] = nonce[..12].try_into().map_err(|_| "Invalid nonce")?; + let nonce_array: [u8; 12] = nonce[..12] + .try_into() + .map_err(|_| CryptoError::InvalidNonce)?; match self { AesGcm::Aes128(cipher) => { @@ -101,7 +114,7 @@ impl Cipher for AesGcm { let aes_nonce = GenericArray::::clone_from_slice(&nonce_array); cipher .decrypt_in_place(&aes_nonce, &aad, ciphertext) - .map_err(|_| "AES-GCM decryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Decrypt))?; } AesGcm::Aes256(cipher) => { // Create nonce from fixed-size array - AesNonce is GenericArray @@ -109,7 +122,7 @@ impl Cipher for AesGcm { let aes_nonce = GenericArray::::clone_from_slice(&nonce_array); cipher .decrypt_in_place(&aes_nonce, &aad, ciphertext) - .map_err(|_| "AES-GCM decryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Decrypt))?; } } @@ -133,13 +146,10 @@ impl std::fmt::Debug for ChaCha20Poly1305Cipher { } impl ChaCha20Poly1305Cipher { - fn new(key: &[u8]) -> Result { + fn new(key: &[u8]) -> Result { use chacha20poly1305::KeyInit; if key.len() != 32 { - return Err(format!( - "Invalid key size for CHACHA20-POLY1305: {}", - key.len() - )); + return Err(CryptoError::InvalidChacha20Poly1305KeySize { actual: key.len() }); } let key = chacha20poly1305::Key::from_slice(key); Ok(ChaCha20Poly1305Cipher { @@ -149,44 +159,56 @@ impl ChaCha20Poly1305Cipher { } impl Cipher for ChaCha20Poly1305Cipher { - fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + }); } - let nonce_array: [u8; 12] = nonce[..12].try_into().map_err(|_| "Invalid nonce")?; + let nonce_array: [u8; 12] = nonce[..12] + .try_into() + .map_err(|_| CryptoError::InvalidNonce)?; use generic_array::{GenericArray, typenum::U12}; let chacha_nonce = GenericArray::::clone_from_slice(&nonce_array); self.cipher .encrypt_in_place(&chacha_nonce, &aad, data) - .map_err(|_| "ChaCha20-Poly1305 encryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Encrypt))?; Ok(()) } - fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + fn decrypt( + &mut self, + ciphertext: &mut TmpBuf, + aad: Aad, + nonce: Nonce, + ) -> Result<(), CryptoError> { if ciphertext.len() < 16 { - return Err(format!("Ciphertext too short: {}", ciphertext.len())); + return Err(CryptoError::CiphertextTooShort { + minimum: 16, + actual: ciphertext.len(), + }); } if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + }); } - let nonce_array: [u8; 12] = nonce[..12].try_into().map_err(|_| "Invalid nonce")?; + let nonce_array: [u8; 12] = nonce[..12] + .try_into() + .map_err(|_| CryptoError::InvalidNonce)?; use generic_array::{GenericArray, typenum::U12}; let chacha_nonce = GenericArray::::clone_from_slice(&nonce_array); self.cipher .decrypt_in_place(&chacha_nonce, &aad, ciphertext) - .map_err(|_| "ChaCha20-Poly1305 decryption failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Decrypt))?; Ok(()) } @@ -217,7 +239,7 @@ impl SupportedDtls12CipherSuite for Aes128GcmSha256 { 16 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } } @@ -247,7 +269,7 @@ impl SupportedDtls12CipherSuite for Aes256GcmSha384 { 16 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } } @@ -277,7 +299,7 @@ impl SupportedDtls12CipherSuite for ChaCha20Poly1305Sha256 { 16 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(ChaCha20Poly1305Cipher::new(key)?)) } } @@ -307,7 +329,7 @@ impl SupportedDtls12CipherSuite for PskAes128Ccm8 { 8 } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(crate::crypto::ccm_cipher::AesCcm8Cipher::new( key, )?)) @@ -357,7 +379,7 @@ impl SupportedDtls13CipherSuite for Tls13Aes128GcmSha256 { 16 // GCM tag } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } @@ -395,7 +417,7 @@ impl SupportedDtls13CipherSuite for Tls13Aes256GcmSha384 { 16 // GCM tag } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(AesGcm::new(key)?)) } @@ -433,7 +455,7 @@ impl SupportedDtls13CipherSuite for Tls13ChaCha20Poly1305Sha256 { 16 // Poly1305 tag } - fn create_cipher(&self, key: &[u8]) -> Result, String> { + fn create_cipher(&self, key: &[u8]) -> Result, CryptoError> { Ok(Box::new(ChaCha20Poly1305Cipher::new(key)?)) } @@ -478,8 +500,8 @@ mod test { // incorrect length (should be 32) let result = ChaCha20Poly1305Cipher::new(&[0, 1, 2, 3, 4, 5]); assert_eq!( - "Invalid key size for CHACHA20-POLY1305: 6", - &result.unwrap_err() + CryptoError::InvalidChacha20Poly1305KeySize { actual: 6 }, + result.unwrap_err() ); } } diff --git a/src/crypto/rust_crypto/hmac.rs b/src/crypto/rust_crypto/hmac.rs index 16650efd..516060d2 100644 --- a/src/crypto/rust_crypto/hmac.rs +++ b/src/crypto/rust_crypto/hmac.rs @@ -5,6 +5,7 @@ use sha2::{Sha256, Sha384}; use super::super::HmacProvider; use crate::types::HashAlgorithm; +use crate::{CryptoError, CryptoOperation}; /// HMAC provider implementation. #[derive(Debug)] @@ -17,11 +18,11 @@ impl HmacProvider for RustCryptoHmacProvider { key: &[u8], data: &[u8], out: &mut [u8], - ) -> Result { + ) -> Result { match hash { HashAlgorithm::SHA256 => { let mut mac = Hmac::::new_from_slice(key) - .map_err(|_| "Invalid HMAC key".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::ComputeHmac))?; mac.update(data); let result = mac.finalize().into_bytes(); let len = result.len(); @@ -30,14 +31,14 @@ impl HmacProvider for RustCryptoHmacProvider { } HashAlgorithm::SHA384 => { let mut mac = Hmac::::new_from_slice(key) - .map_err(|_| "Invalid HMAC key".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::ComputeHmac))?; mac.update(data); let result = mac.finalize().into_bytes(); let len = result.len(); out[..len].copy_from_slice(&result); Ok(len) } - _ => Err(format!("Unsupported HMAC hash algorithm: {:?}", hash)), + _ => Err(CryptoError::UnsupportedHmacHash(hash)), } } } diff --git a/src/crypto/rust_crypto/kx_group.rs b/src/crypto/rust_crypto/kx_group.rs index 0374e70a..03ef91d7 100644 --- a/src/crypto/rust_crypto/kx_group.rs +++ b/src/crypto/rust_crypto/kx_group.rs @@ -6,6 +6,7 @@ use p384::{PublicKey as P384PublicKey, ecdh::EphemeralSecret as P384EphemeralSec use super::super::{ActiveKeyExchange, SupportedKxGroup}; use crate::buffer::Buf; use crate::types::NamedGroup; +use crate::{CryptoError, CryptoOperation}; /// ECDHE key exchange implementation. enum EcdhKeyExchange { @@ -43,7 +44,7 @@ impl std::fmt::Debug for EcdhKeyExchange { } impl EcdhKeyExchange { - fn new(group: NamedGroup, mut buf: Buf) -> Result { + fn new(group: NamedGroup, mut buf: Buf) -> Result { match group { NamedGroup::X25519 => { use rand_core::OsRng; @@ -80,7 +81,7 @@ impl EcdhKeyExchange { public_key: buf, }) } - _ => Err("Unsupported group".to_string()), + _ => Err(CryptoError::UnsupportedKeyExchangeGroup(group)), } } } @@ -94,17 +95,19 @@ impl ActiveKeyExchange for EcdhKeyExchange { } } - fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), String> { + fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), CryptoError> { match *self { EcdhKeyExchange::X25519 { secret, .. } => { let peer_bytes: [u8; 32] = peer_pub .try_into() - .map_err(|_| "Invalid X25519 public key length".to_string())?; + .map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::X25519))?; let peer_key = x25519_dalek::PublicKey::from(peer_bytes); let shared_secret = secret.diffie_hellman(&peer_key); // RFC 7748 §6.1: check the shared secret is not zero (low-order point) if !shared_secret.was_contributory() { - return Err("X25519 shared secret is zero (non-contributory)".to_string()); + return Err(CryptoError::OperationFailed( + CryptoOperation::CompleteKeyExchange, + )); } out.clear(); out.extend_from_slice(shared_secret.as_bytes()); @@ -112,7 +115,7 @@ impl ActiveKeyExchange for EcdhKeyExchange { } EcdhKeyExchange::P256 { secret, .. } => { let peer_key = P256PublicKey::from_sec1_bytes(peer_pub) - .map_err(|_| "Invalid P-256 public key".to_string())?; + .map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::Secp256r1))?; let shared_secret = secret.diffie_hellman(&peer_key); out.clear(); out.extend_from_slice(shared_secret.raw_secret_bytes().as_slice()); @@ -120,7 +123,7 @@ impl ActiveKeyExchange for EcdhKeyExchange { } EcdhKeyExchange::P384 { secret, .. } => { let peer_key = P384PublicKey::from_sec1_bytes(peer_pub) - .map_err(|_| "Invalid P-384 public key".to_string())?; + .map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::Secp384r1))?; let shared_secret = secret.diffie_hellman(&peer_key); out.clear(); out.extend_from_slice(shared_secret.raw_secret_bytes().as_slice()); @@ -147,7 +150,7 @@ impl SupportedKxGroup for X25519Kx { NamedGroup::X25519 } - fn start_exchange(&self, buf: Buf) -> Result, String> { + fn start_exchange(&self, buf: Buf) -> Result, CryptoError> { Ok(Box::new(EcdhKeyExchange::new(NamedGroup::X25519, buf)?)) } } @@ -161,7 +164,7 @@ impl SupportedKxGroup for P256 { NamedGroup::Secp256r1 } - fn start_exchange(&self, buf: Buf) -> Result, String> { + fn start_exchange(&self, buf: Buf) -> Result, CryptoError> { Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp256r1, buf)?)) } } @@ -175,7 +178,7 @@ impl SupportedKxGroup for P384 { NamedGroup::Secp384r1 } - fn start_exchange(&self, buf: Buf) -> Result, String> { + fn start_exchange(&self, buf: Buf) -> Result, CryptoError> { Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp384r1, buf)?)) } } diff --git a/src/crypto/rust_crypto/random.rs b/src/crypto/rust_crypto/random.rs index 8a1ef9b4..de9cd7e6 100644 --- a/src/crypto/rust_crypto/random.rs +++ b/src/crypto/rust_crypto/random.rs @@ -1,13 +1,14 @@ //! Secure random number generation using RustCrypto. use super::super::SecureRandom; +use crate::CryptoError; /// Secure random number generator implementation. #[derive(Debug)] pub(super) struct RustCryptoSecureRandom; impl SecureRandom for RustCryptoSecureRandom { - fn fill(&self, buf: &mut [u8]) -> Result<(), String> { + fn fill(&self, buf: &mut [u8]) -> Result<(), CryptoError> { use rand_core::OsRng; use rand_core::RngCore; OsRng.fill_bytes(buf); diff --git a/src/crypto/rust_crypto/sign.rs b/src/crypto/rust_crypto/sign.rs index 42f01f17..bd6bc848 100644 --- a/src/crypto/rust_crypto/sign.rs +++ b/src/crypto/rust_crypto/sign.rs @@ -15,6 +15,7 @@ use super::super::{KeyProvider, SignatureVerifier, SigningKey as SigningKeyTrait use super::super::{OID_P256, OID_P384}; use crate::buffer::Buf; use crate::types::{HashAlgorithm, NamedGroup, SignatureAlgorithm}; +use crate::{CryptoError, CryptoOperation}; /// ECDSA signing key implementation. enum EcdsaSigningKey { @@ -32,7 +33,12 @@ impl std::fmt::Debug for EcdsaSigningKey { } impl SigningKeyTrait for EcdsaSigningKey { - fn sign(&mut self, data: &[u8], hash_alg: HashAlgorithm, out: &mut Buf) -> Result<(), String> { + fn sign( + &mut self, + data: &[u8], + hash_alg: HashAlgorithm, + out: &mut Buf, + ) -> Result<(), CryptoError> { use ecdsa::signature::hazmat::PrehashSigner; use sha2::Digest; @@ -48,13 +54,13 @@ impl SigningKeyTrait for EcdsaSigningKey { key.sign_prehash(&hash) } _ => { - return Err(format!( - "P-256 key does not support hash algorithm {:?}", - hash_alg - )); + return Err(CryptoError::SigningKeyUnsupportedHash { + group: NamedGroup::Secp256r1, + hash: hash_alg, + }); } } - .map_err(|_| "Signing failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Sign))?; out.clear(); out.extend_from_slice(signature.to_der().as_bytes()); Ok(()) @@ -70,13 +76,13 @@ impl SigningKeyTrait for EcdsaSigningKey { key.sign_prehash(&hash) } _ => { - return Err(format!( - "P-384 key does not support hash algorithm {:?}", - hash_alg - )); + return Err(CryptoError::SigningKeyUnsupportedHash { + group: NamedGroup::Secp384r1, + hash: hash_alg, + }); } } - .map_err(|_| "Signing failed".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::Sign))?; out.clear(); out.extend_from_slice(signature.to_der().as_bytes()); Ok(()) @@ -106,7 +112,7 @@ impl SigningKeyTrait for EcdsaSigningKey { pub(super) struct RustCryptoKeyProvider; impl KeyProvider for RustCryptoKeyProvider { - fn load_private_key(&self, key_der: &[u8]) -> Result, String> { + fn load_private_key(&self, key_der: &[u8]) -> Result, CryptoError> { // Try PKCS#8 DER format first (most common) if let Ok(key) = SigningKey::::from_pkcs8_der(key_der) { return Ok(Box::new(EcdsaSigningKey::P256(key))); @@ -135,9 +141,9 @@ impl KeyProvider for RustCryptoKeyProvider { let ec_alg_oid = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); let curve_params_der = curve_oid .to_der() - .map_err(|_| "Failed to encode curve OID".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::EncodeKey))?; let curve_params_any = der::asn1::AnyRef::try_from(curve_params_der.as_slice()) - .map_err(|_| "Failed to create AnyRef".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::EncodeKey))?; let algorithm = spki::AlgorithmIdentifierRef { oid: ec_alg_oid, @@ -152,7 +158,7 @@ impl KeyProvider for RustCryptoKeyProvider { let pkcs8_der = pkcs8 .to_der() - .map_err(|_| "Failed to encode PKCS#8".to_string())?; + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::EncodeKey))?; let p256_curve = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7"); if curve_oid == p256_curve { @@ -179,7 +185,7 @@ impl KeyProvider for RustCryptoKeyProvider { } } - Err("Failed to parse private key in any supported format".to_string()) + Err(CryptoError::InvalidPrivateKey) } } @@ -195,42 +201,42 @@ impl SignatureVerifier for RustCryptoSignatureVerifier { signature: &[u8], hash_alg: HashAlgorithm, sig_alg: SignatureAlgorithm, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { if sig_alg != SignatureAlgorithm::ECDSA { - return Err(format!("Unsupported signature algorithm: {:?}", sig_alg)); + return Err(CryptoError::UnsupportedSignatureAlgorithm(sig_alg)); } - let cert = X509Certificate::from_der(cert_der) - .map_err(|e| format!("Failed to parse certificate: {e}"))?; + let cert = + X509Certificate::from_der(cert_der).map_err(|e| CryptoError::ProviderFailure { + operation: CryptoOperation::VerifySignature, + reason: e.to_string(), + })?; let spki = &cert.tbs_certificate.subject_public_key_info; const OID_EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); if spki.algorithm.oid != OID_EC_PUBLIC_KEY { - return Err(format!( - "Unsupported public key algorithm: {}", - spki.algorithm.oid - )); + return Err(CryptoError::UnsupportedPublicKeyAlgorithm); } let pubkey_bytes = spki .subject_public_key .as_bytes() - .ok_or_else(|| "Invalid EC subject_public_key bitstring".to_string())?; + .ok_or(CryptoError::InvalidSubjectPublicKey)?; let curve_oid: ObjectIdentifier = spki .algorithm .parameters .as_ref() - .ok_or("Missing EC curve parameter in certificate")? + .ok_or(CryptoError::MissingEcCurveParameter)? .decode_as() - .map_err(|_| "Invalid EC curve parameter in certificate".to_string())?; + .map_err(|_| CryptoError::InvalidEcCurveParameter)?; let group = match curve_oid { OID_P256 => NamedGroup::Secp256r1, OID_P384 => NamedGroup::Secp384r1, - _ => return Err(format!("Unsupported EC curve: {}", curve_oid)), + _ => return Err(CryptoError::UnsupportedEcCurve(curve_oid.to_string())), }; check_verify_scheme(sig_alg, hash_alg, group)?; @@ -249,27 +255,21 @@ impl SignatureVerifier for RustCryptoSignatureVerifier { match group { NamedGroup::Secp256r1 => { let verifying_key = VerifyingKey::::from_sec1_bytes(pubkey_bytes) - .map_err(|_| "Invalid P-256 public key".to_string())?; + .map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::Secp256r1))?; let sig = Signature::::from_der(signature) - .map_err(|_| "Invalid signature format".to_string())?; - verifying_key.verify_prehash(&hash, &sig).map_err(|_| { - format!( - "ECDSA signature verification failed for {:?} {:?}", - hash_alg, group - ) - }) + .map_err(|_| CryptoError::InvalidSignatureFormat)?; + verifying_key + .verify_prehash(&hash, &sig) + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::VerifySignature)) } NamedGroup::Secp384r1 => { let verifying_key = VerifyingKey::::from_sec1_bytes(pubkey_bytes) - .map_err(|_| "Invalid P-384 public key".to_string())?; + .map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::Secp384r1))?; let sig = Signature::::from_der(signature) - .map_err(|_| "Invalid signature format".to_string())?; - verifying_key.verify_prehash(&hash, &sig).map_err(|_| { - format!( - "ECDSA signature verification failed for {:?} {:?}", - hash_alg, group - ) - }) + .map_err(|_| CryptoError::InvalidSignatureFormat)?; + verifying_key + .verify_prehash(&hash, &sig) + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::VerifySignature)) } // unreachable: OID match above only produces Secp256r1/Secp384r1 _ => unreachable!(), diff --git a/src/crypto/validation/mod.rs b/src/crypto/validation/mod.rs index 7d4a7bea..465e4941 100644 --- a/src/crypto/validation/mod.rs +++ b/src/crypto/validation/mod.rs @@ -6,9 +6,13 @@ use arrayvec::ArrayVec; use super::{Aad, CryptoProvider, Nonce, SupportedDtls12CipherSuite, SupportedKxGroup}; -use crate::Error; use crate::buffer::{Buf, TmpBuf}; use crate::types::{Dtls13CipherSuite, HashAlgorithm, NamedGroup, SignatureAlgorithm}; +use crate::{ConfigError, CryptoProviderValidationError, Error}; + +fn provider_error(error: CryptoProviderValidationError) -> Error { + Error::ConfigError(ConfigError::CryptoProvider(error)) +} impl CryptoProvider { /// Returns an iterator over validated cipher suites supported by dimpl. @@ -86,8 +90,8 @@ impl CryptoProvider { fn validate_cipher_suites(&self) -> Result<(), Error> { let cipher_count = self.supported_cipher_suites().count(); if cipher_count == 0 { - return Err(Error::ConfigError( - "CryptoProvider has no cipher suites supported by dimpl.".to_string(), + return Err(provider_error( + CryptoProviderValidationError::NoCipherSuites, )); } Ok(()) @@ -98,9 +102,8 @@ impl CryptoProvider { if self.has_ecdh() { let kx_count = self.supported_kx_groups().count(); if kx_count == 0 { - return Err(Error::ConfigError( - "CryptoProvider has ECDH cipher suites but no supported key exchange groups." - .to_string(), + return Err(provider_error( + CryptoProviderValidationError::EcdhCipherSuitesWithoutKeyExchangeGroups, )); } } @@ -132,17 +135,15 @@ impl CryptoProvider { .map(|(_, v)| v); let Some(expected) = maybe_expected else { - return Err(Error::ConfigError(format!( - "No expected hash data for hash algorithm: {:?}", - hash_alg - ))); + return Err(provider_error( + CryptoProviderValidationError::MissingHashTestVector(*hash_alg), + )); }; if result.as_ref() != *expected { - return Err(Error::ConfigError(format!( - "Hash provider {:?} produced incorrect result", - hash_alg - ))); + return Err(provider_error( + CryptoProviderValidationError::HashProviderIncorrect(*hash_alg), + )); } } @@ -169,15 +170,21 @@ impl CryptoProvider { &mut scratch, hash_alg, ) - .map_err(|e| Error::ConfigError(format!("PRF failed for {:?}: {}", hash_alg, e)))?; + .map_err(|e| { + provider_error(CryptoProviderValidationError::PrfFailed { + hash: hash_alg, + source: e, + }) + })?; if result.len() != output_len { - return Err(Error::ConfigError(format!( - "PRF {:?} returned wrong length: expected {}, got {}", - hash_alg, - output_len, - result.len() - ))); + return Err(provider_error( + CryptoProviderValidationError::PrfWrongLength { + hash: hash_alg, + expected: output_len, + actual: result.len(), + }, + )); } let maybe_expected = PRF_TEST_VECTORS @@ -186,16 +193,14 @@ impl CryptoProvider { .map(|(_, v)| v); let Some(expected) = maybe_expected else { - return Err(Error::ConfigError(format!( - "No expected PRF data for hash algorithm: {:?}", - hash_alg - ))); + return Err(provider_error( + CryptoProviderValidationError::MissingPrfTestVector(hash_alg), + )); }; if result.as_ref() != *expected { - return Err(Error::ConfigError(format!( - "PRF {:?} produced incorrect result", - hash_alg + return Err(provider_error(CryptoProviderValidationError::PrfIncorrect( + hash_alg, ))); } } @@ -229,10 +234,12 @@ impl CryptoProvider { VALIDATION_TEST_DATA, ), _ => { - return Err(Error::ConfigError(format!( - "No validation test vectors for {:?} + {:?}", - hash_alg, sig_alg - ))); + return Err(provider_error( + CryptoProviderValidationError::NoSignatureValidationVector { + hash: hash_alg, + signature: sig_alg, + }, + )); } }; @@ -240,10 +247,11 @@ impl CryptoProvider { self.signature_verification .verify_signature(cert_der, test_data, signature, hash_alg, sig_alg) .map_err(|e| { - Error::ConfigError(format!( - "Signature verification failed for {:?} + {:?}: {}", - hash_alg, sig_alg, e - )) + provider_error(CryptoProviderValidationError::SignatureVerificationFailed { + hash: hash_alg, + signature: sig_alg, + source: e, + }) })?; } @@ -253,8 +261,8 @@ impl CryptoProvider { /// Validate that DTLS 1.3 cipher suites and HKDF provider are configured. fn validate_dtls13_cipher_suites(&self) -> Result<(), Error> { if self.dtls13_cipher_suites.is_empty() { - return Err(Error::ConfigError( - "CryptoProvider has no DTLS 1.3 cipher suites.".to_string(), + return Err(provider_error( + CryptoProviderValidationError::NoDtls13CipherSuites, )); } @@ -267,17 +275,15 @@ impl CryptoProvider { let mut out = Buf::new(); super::prf_hkdf::hkdf_extract(self.hmac_provider, hash, zeros, zeros, &mut out) .map_err(|e| { - Error::ConfigError(format!( - "HKDF failed for DTLS 1.3 suite {:?}: {}", - cs.suite(), - e - )) + provider_error(CryptoProviderValidationError::HkdfFailed { + suite: cs.suite(), + source: e, + }) })?; if out.is_empty() { - return Err(Error::ConfigError(format!( - "HKDF returned empty output for {:?}", - cs.suite() - ))); + return Err(provider_error( + CryptoProviderValidationError::HkdfEmptyOutput(cs.suite()), + )); } } @@ -292,10 +298,7 @@ impl CryptoProvider { .iter() .find(|tv| tv.suite == suite) .ok_or_else(|| { - Error::ConfigError(format!( - "No AEAD test vector for DTLS 1.3 suite {:?}", - suite - )) + provider_error(CryptoProviderValidationError::NoAeadTestVector(suite)) })?; let nonce = Nonce(tv.nonce); @@ -306,34 +309,38 @@ impl CryptoProvider { // Encrypt let mut cipher = cs.create_cipher(tv.key).map_err(|e| { - Error::ConfigError(format!("Failed to create cipher for {:?}: {}", suite, e)) + provider_error(CryptoProviderValidationError::AeadCreateFailed { suite, source: e }) })?; let mut buf = Buf::new(); buf.extend_from_slice(tv.plaintext); cipher.encrypt(&mut buf, aad.clone(), nonce).map_err(|e| { - Error::ConfigError(format!("AEAD encrypt failed for {:?}: {}", suite, e)) + provider_error(CryptoProviderValidationError::AeadEncryptFailed { + suite, + source: e, + }) })?; if buf.as_ref() != tv.ciphertext_tag { - return Err(Error::ConfigError(format!( - "AEAD encrypt produced wrong output for {:?}", - suite - ))); + return Err(provider_error( + CryptoProviderValidationError::AeadEncryptWrongOutput(suite), + )); } // Decrypt with a fresh cipher instance let mut cipher = cs.create_cipher(tv.key).map_err(|e| { - Error::ConfigError(format!("Failed to create cipher for {:?}: {}", suite, e)) + provider_error(CryptoProviderValidationError::AeadCreateFailed { suite, source: e }) })?; let mut ct = Vec::from(tv.ciphertext_tag); let mut tmp = TmpBuf::new(&mut ct); cipher.decrypt(&mut tmp, aad, nonce).map_err(|e| { - Error::ConfigError(format!("AEAD decrypt failed for {:?}: {}", suite, e)) + provider_error(CryptoProviderValidationError::AeadDecryptFailed { + suite, + source: e, + }) })?; if tmp.as_ref() != tv.plaintext { - return Err(Error::ConfigError(format!( - "AEAD decrypt produced wrong output for {:?}", - suite - ))); + return Err(provider_error( + CryptoProviderValidationError::AeadDecryptWrongOutput(suite), + )); } } Ok(()) @@ -347,18 +354,16 @@ impl CryptoProvider { .iter() .find(|tv| tv.suite == suite) .ok_or_else(|| { - Error::ConfigError(format!( - "No encrypt_sn test vector for DTLS 1.3 suite {:?}", - suite - )) + provider_error( + CryptoProviderValidationError::NoRecordNumberEncryptionTestVector(suite), + ) })?; let mask = cs.encrypt_sn(tv.sn_key, &tv.sample); if mask[..tv.check_len] != tv.expected_mask[..tv.check_len] { - return Err(Error::ConfigError(format!( - "encrypt_sn produced wrong mask for {:?}", - suite - ))); + return Err(provider_error( + CryptoProviderValidationError::RecordNumberEncryptionWrongMask(suite), + )); } } Ok(()) @@ -370,10 +375,16 @@ impl CryptoProvider { let group = kx.name(); let alice = kx.start_exchange(Buf::new()).map_err(|e| { - Error::ConfigError(format!("Key exchange start failed for {:?}: {}", group, e)) + provider_error(CryptoProviderValidationError::KeyExchangeStartFailed { + group, + source: e, + }) })?; let bob = kx.start_exchange(Buf::new()).map_err(|e| { - Error::ConfigError(format!("Key exchange start failed for {:?}: {}", group, e)) + provider_error(CryptoProviderValidationError::KeyExchangeStartFailed { + group, + source: e, + }) })?; let alice_pub = alice.pub_key().to_vec(); @@ -381,25 +392,24 @@ impl CryptoProvider { let mut alice_secret = Buf::new(); alice.complete(&bob_pub, &mut alice_secret).map_err(|e| { - Error::ConfigError(format!( - "Key exchange complete failed for {:?}: {}", - group, e - )) + provider_error(CryptoProviderValidationError::KeyExchangeCompleteFailed { + group, + source: e, + }) })?; let mut bob_secret = Buf::new(); bob.complete(&alice_pub, &mut bob_secret).map_err(|e| { - Error::ConfigError(format!( - "Key exchange complete failed for {:?}: {}", - group, e - )) + provider_error(CryptoProviderValidationError::KeyExchangeCompleteFailed { + group, + source: e, + }) })?; if alice_secret.as_ref() != bob_secret.as_ref() { - return Err(Error::ConfigError(format!( - "Key exchange produced different secrets for {:?}", - group - ))); + return Err(provider_error( + CryptoProviderValidationError::KeyExchangeMismatchedSharedSecret(group), + )); } } Ok(()) @@ -417,23 +427,23 @@ impl CryptoProvider { let result = self .hmac_provider .hmac_sha256(key, data) - .map_err(|e| Error::ConfigError(format!("HMAC provider failed: {}", e)))?; + .map_err(|e| provider_error(CryptoProviderValidationError::HmacFailed(e)))?; // Verify the result matches expected HMAC-SHA256 output // Expected: HMAC-SHA256("key", "The quick brown fox jumps over the lazy dog") // This is a standard test vector for HMAC-SHA256 if result.len() != 32 { - return Err(Error::ConfigError(format!( - "HMAC provider returned wrong length: expected 32 bytes, got {}", - result.len() - ))); + return Err(provider_error( + CryptoProviderValidationError::HmacWrongLength { + expected: 32, + actual: result.len(), + }, + )); } // Verify against known HMAC-SHA256 test vector if result.as_slice() != HMAC_SHA256_TEST_VECTOR { - return Err(Error::ConfigError( - "HMAC provider produced incorrect result for HMAC-SHA256".to_string(), - )); + return Err(provider_error(CryptoProviderValidationError::HmacIncorrect)); } Ok(()) diff --git a/src/dtls12/client.rs b/src/dtls12/client.rs index e9741da0..6bdd19c7 100644 --- a/src/dtls12/client.rs +++ b/src/dtls12/client.rs @@ -403,10 +403,9 @@ impl State { if h.server_version != ProtocolVersion::DTLS1_2 && h.server_version != ProtocolVersion::DTLS1_0 { - return Err((Error::SecurityError(format!( - "Unsupported DTLS version in HelloVerifyRequest: {:?}", - h.server_version - ))) + return Err(Error::SecurityError( + crate::SecurityError::UnsupportedHelloVerifyRequestVersion(h.server_version), + ) .into()); } @@ -448,28 +447,31 @@ impl State { // Enforce DTLS version if server_hello.server_version != ProtocolVersion::DTLS1_2 { - return Err((Error::SecurityError(format!( - "Unsupported DTLS version from server: {:?}", - server_hello.server_version - ))) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::UnsupportedServerVersion( + server_hello.server_version, + )) + .into(), + ); } // Enforce Null compression only if server_hello.compression_method != CompressionMethod::Null { - return Err((Error::SecurityError(format!( - "Unsupported compression from server: {:?}", - server_hello.compression_method - ))) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::UnsupportedServerCompression( + server_hello.compression_method, + )) + .into(), + ); } // Enforce cipher suite is known and allowed let cs = server_hello.cipher_suite; if matches!(cs, Dtls12CipherSuite::Unknown(_)) { - return Err( - (Error::SecurityError("Server selected unknown cipher suite".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ServerSelectedUnknownCipherSuite, + )) + .into()); } // Enforce cipher suite is compatible with our private key and allowed by config @@ -479,18 +481,16 @@ impl State { .is_cipher_suite_compatible(cs); if !is_compatible { - return Err((Error::SecurityError(format!( - "Server selected incompatible cipher suite: {:?}", - cs - ))) + return Err(Error::SecurityError( + crate::SecurityError::ServerSelectedIncompatibleCipherSuite(cs), + ) .into()); } if !client.engine.is_cipher_suite_allowed(cs) { - return Err((Error::SecurityError(format!( - "Server selected disallowed cipher suite: {:?}", - cs - ))) + return Err(Error::SecurityError( + crate::SecurityError::ServerSelectedDisallowedCipherSuite(cs), + ) .into()); } @@ -532,9 +532,9 @@ impl State { // Without extended master secret, in DTLS1.2 a security attack // reusing the same master secret is possible. if !extended_master_secret { - return Err((Error::SecurityError( - "Extended Master Secret not negotiated".to_string(), - )) + return Err(Error::SecurityError( + crate::SecurityError::ExtendedMasterSecretNotNegotiated, + ) .into()); } @@ -566,9 +566,10 @@ impl State { }; if certificate.certificate_list.is_empty() { - return Err( - (Error::CertificateError("No server certificate received".to_string())).into(), - ); + return Err((Error::CertificateError( + crate::CertificateError::NoServerCertificateReceived, + )) + .into()); } debug!( @@ -596,10 +597,9 @@ impl State { } fn await_server_key_exchange(self, client: &mut Client) -> Result { - let cipher_suite = client - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let cipher_suite = client.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; if cipher_suite.is_psk() { self.await_server_key_exchange_psk(client) @@ -628,9 +628,9 @@ impl State { let Some(d_signed) = server_key_exchange.signature() else { // We do not support anonymous key exchange - return Err((Error::UnexpectedMessage( - "ServerKeyExchange without signature".to_string(), - )) + return Err(Error::UnexpectedMessage( + crate::UnexpectedMessageError::ServerKeyExchangeWithoutSignature, + ) .into()); }; @@ -645,9 +645,9 @@ impl State { ecdh.public_key_range.clone(), ), ServerKeyExchangeParams::Psk(_) => { - return Err((Error::UnexpectedMessage( - "PSK ServerKeyExchange in ECDHE path".to_string(), - )) + return Err(Error::UnexpectedMessage( + crate::UnexpectedMessageError::PskServerKeyExchangeInEcdhePath, + ) .into()); } }; @@ -682,28 +682,29 @@ impl State { signed_data.push(public_key_vec.len() as u8); signed_data.extend_from_slice(public_key_vec); - let cipher_suite = client - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let cipher_suite = client.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; // Ensure the server's (hash, signature) pair was offered by the client let offered = SignatureAndHashAlgorithm::supported().contains(&signature_algorithm); if !offered { - return Err((Error::CryptoError( - "Signature algorithm not offered by client".to_string(), - )) + return Err(Error::CryptoError( + crate::CryptoError::SignatureAlgorithmNotOfferedByClient, + ) .into()); } // Ensure the signature algorithm is compatible with the cipher suite if let Some(expected_sig) = cipher_suite.signature_algorithm() { if signature_algorithm.signature != expected_sig { - return Err((Error::CryptoError(format!( - "Signature algorithm mismatch: {:?} != {:?}", - signature_algorithm.signature, expected_sig - ))) - .into()); + return Err( + Error::CryptoError(crate::CryptoError::SignatureAlgorithmMismatch { + expected: expected_sig, + actual: signature_algorithm.signature, + }) + .into(), + ); } } @@ -720,12 +721,7 @@ impl State { .engine .crypto_context_mut() .verify_signature(&signed_data, &temp_signed, signature_bytes, cert_der) - .map_err(|e| { - Error::CryptoError(format!( - "Failed to verify server key exchange signature: {}", - e - )) - })?; + .map_err(Error::CryptoError)?; trace!( "ServerKeyExchange signature verified: {:?}", @@ -739,9 +735,7 @@ impl State { .engine .crypto_context_mut() .process_ecdh_params(named_group, public_key_vec, &mut kx_buf) - .map_err(|e| { - Error::CryptoError(format!("Failed to process server key exchange: {}", e)) - })?; + .map_err(Error::CryptoError)?; client.engine.push_buffer(kx_buf); Ok(Self::AwaitCertificateRequest) @@ -776,9 +770,9 @@ impl State { let hint_range = match &ske.params { ServerKeyExchangeParams::Psk(psk) => psk.hint_range.clone(), _ => { - return Err((Error::UnexpectedMessage( - "ECDHE ServerKeyExchange in PSK path".to_string(), - )) + return Err(Error::UnexpectedMessage( + crate::UnexpectedMessageError::EcdheServerKeyExchangeInPskPath, + ) .into()); } }; @@ -826,10 +820,9 @@ impl State { .unwrap(); if !cr.supports_hash_algorithm(hash_algorithm) { - return Err((Error::CertificateError(format!( - "Unsupported hash algorithm: {:?}", - hash_algorithm - ))) + return Err(Error::CertificateError( + crate::CertificateError::UnsupportedHashAlgorithm(hash_algorithm), + ) .into()); } @@ -860,10 +853,9 @@ impl State { trace!("Received ServerHelloDone"); - let cipher_suite = client - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let cipher_suite = client.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; if cipher_suite.is_psk() { // PSK: no certificates involved @@ -872,9 +864,10 @@ impl State { // Validate the server certificate if client.server_certificates.is_empty() { - return Err( - (Error::CertificateError("No server certificate received".to_string())).into(), - ); + return Err((Error::CertificateError( + crate::CertificateError::NoServerCertificateReceived, + )) + .into()); } // Send the server certificate as an event @@ -922,10 +915,9 @@ impl State { // ServerKeyExchange, CertificateRequest, ServerHelloDone, Certificate, ClientKeyExchange // This is correct per RFC 7627 - session hash should include messages // up to and including ClientKeyExchange - let cipher_suite = client - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let cipher_suite = client.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; let suite_hash = cipher_suite.hash_algorithm(); let mut buf = Buf::new(); @@ -969,14 +961,16 @@ impl State { fn derive_keys(client: &mut Client) -> Result<(), Error> { trace!("Deriving keys"); let Some(cipher_suite) = client.engine.cipher_suite() else { - return Err(Error::InvalidState("No cipher suite selected".to_string())); + return Err(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + )); }; trace!("Using cipher suite for key derivation: {:?}", cipher_suite); let Some(server_random) = &client.server_random else { return Err(Error::InvalidState( - "No server random available".to_string(), + crate::InvalidStateError::NoServerRandom, )); }; @@ -995,11 +989,12 @@ impl State { let suite_hash = cipher_suite.hash_algorithm(); // Use the captured session hash from when ServerHelloDone was received - let session_hash = client.captured_session_hash.as_ref().ok_or_else(|| { - Error::InvalidState( - "Extended Master Secret negotiated but session hash not captured".to_string(), - ) - })?; + let session_hash = client + .captured_session_hash + .as_ref() + .ok_or(Error::InvalidState(crate::InvalidStateError::Other( + "Extended Master Secret negotiated but session hash not captured", + )))?; trace!( "Using captured session hash for Extended Master Secret (length: {})", session_hash.len() @@ -1010,9 +1005,7 @@ impl State { .engine .crypto_context_mut() .derive_extended_master_secret(session_hash, suite_hash, &mut out, &mut scratch) - .map_err(|e| { - Error::CryptoError(format!("Failed to derive extended master secret: {}", e)) - })?; + .map_err(Error::CryptoError)?; // Derive the encryption/decryption keys client @@ -1025,7 +1018,7 @@ impl State { &mut out, &mut scratch, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive keys: {}", e)))?; + .map_err(Error::CryptoError)?; client.engine.push_buffer(out); client.engine.push_buffer(scratch); @@ -1137,9 +1130,10 @@ impl State { // Use constant-time comparison to prevent timing attacks let is_eq: bool = verify_data.ct_eq(expected.as_slice()).into(); if !is_eq { - return Err( - (Error::SecurityError("Server Finished verification failed".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ServerFinishedVerificationFailed, + )) + .into()); } trace!("Server Finished verified successfully"); @@ -1274,7 +1268,9 @@ fn handshake_create_certificate(body: &mut Buf, engine: &mut Engine) -> Result<( fn handshake_create_client_key_exchange(body: &mut Buf, engine: &mut Engine) -> Result<(), Error> { // Just check that a cipher suite exists without binding to unused variable let Some(cipher_suite) = engine.cipher_suite() else { - return Err(Error::InvalidState("No cipher suite selected".to_string())); + return Err(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + )); }; let key_exchange_algorithm = cipher_suite.as_key_exchange_algorithm(); @@ -1298,9 +1294,7 @@ fn handshake_create_client_key_exchange(body: &mut Buf, engine: &mut Engine) -> let public_key = engine .crypto_context_mut() .maybe_init_key_exchange() - .map_err(|e| { - Error::CryptoError(format!("Failed to generate key exchange: {}", e)) - })?; + .map_err(Error::CryptoError)?; trace!("Generated public key size: {} bytes", public_key.len()); ClientKeyExchange::serialize_from_bytes(public_key, body); @@ -1309,29 +1303,29 @@ fn handshake_create_client_key_exchange(body: &mut Buf, engine: &mut Engine) -> let identity = engine .config() .psk_identity() - .ok_or_else(|| Error::PskError("No PSK identity configured".to_string()))? + .ok_or(Error::PskError(crate::PskError::NoPskIdentityConfigured))? .to_vec(); // Resolve the PSK via the configured resolver let psk = engine .config() .psk_resolver() - .ok_or_else(|| Error::PskError("No PSK resolver configured".to_string()))? + .ok_or(Error::PskError(crate::PskError::NoPskResolverConfigured))? .resolve(&identity) - .ok_or_else(|| Error::PskError("PSK resolver returned no key".to_string()))?; + .ok_or(Error::PskError(crate::PskError::ResolverReturnedNoKey))?; // Set the PSK and compute pre-master secret let crypto = engine.crypto_context_mut(); crypto.set_psk(psk); crypto .compute_psk_pre_master_secret() - .map_err(|e| Error::CryptoError(format!("Failed to compute PSK PMS: {}", e)))?; + .map_err(Error::CryptoError)?; ClientPskKeys::serialize_from_bytes(&identity, body); } _ => { return Err(Error::SecurityError( - "Unsupported key exchange algorithm".to_string(), + crate::SecurityError::UnsupportedKeyExchangeAlgorithm, )); } } @@ -1367,7 +1361,7 @@ fn handshake_create_certificate_verify(body: &mut Buf, engine: &mut Engine) -> R engine .crypto_context .sign_data(handshake_data, hash_alg, &mut signature) - .map_err(|e| Error::CryptoError(format!("Failed to sign handshake messages: {}", e)))?; + .map_err(Error::CryptoError)?; debug!("Generated signature size: {} bytes", signature.len()); diff --git a/src/dtls12/context.rs b/src/dtls12/context.rs index 9b425112..30502c8f 100644 --- a/src/dtls12/context.rs +++ b/src/dtls12/context.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use arrayvec::ArrayVec; +use crate::CryptoError; use crate::buffer::{Buf, TmpBuf, ToBuf}; use crate::crypto; use crate::crypto::SrtpProfile; @@ -114,7 +115,7 @@ impl CryptoContext { } /// Generate key exchange public key - pub fn maybe_init_key_exchange(&mut self) -> Result<&[u8], String> { + pub fn maybe_init_key_exchange(&mut self) -> Result<&[u8], CryptoError> { // If we already have the public key stored, return it if let Some(ref pk) = self.key_exchange_public_key { return Ok(pk); @@ -129,7 +130,7 @@ impl CryptoContext { self.key_exchange_group = Some(group); Ok(self.key_exchange_public_key.as_ref().unwrap()) } - None => Err("Key exchange not initialized".to_string()), + None => Err(CryptoError::KeyExchangeNotInitialized), } } @@ -138,11 +139,11 @@ impl CryptoContext { &mut self, peer_public_key: &[u8], buf: &mut Buf, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { let ke = self .key_exchange .take() - .ok_or_else(|| "Key exchange not initialized".to_string())?; + .ok_or(CryptoError::KeyExchangeNotInitialized)?; ke.complete(peer_public_key, buf)?; self.pre_master_secret = Some(core::mem::take(buf)); // Note: we keep key_exchange_public_key since it may be needed later @@ -158,8 +159,8 @@ impl CryptoContext { /// /// Format: `uint16(N) || zeros(N) || uint16(N) || PSK(N)` /// where N is the PSK length. - pub fn compute_psk_pre_master_secret(&mut self) -> Result<(), String> { - let psk = self.psk.as_ref().ok_or("PSK not set")?; + pub fn compute_psk_pre_master_secret(&mut self) -> Result<(), CryptoError> { + let psk = self.psk.as_ref().ok_or(CryptoError::PskNotSet)?; let n = psk.len(); // Total: 2 + N + 2 + N = 2N + 4 let mut pms = Buf::new(); @@ -176,13 +177,13 @@ impl CryptoContext { &mut self, named_group: NamedGroup, kx_buf: &mut Buf, - ) -> Result<&[u8], String> { + ) -> Result<&[u8], CryptoError> { // Find the matching key exchange group from the provider let kx_group = self .provider() .supported_kx_groups() .find(|g| g.name() == named_group) - .ok_or_else(|| format!("Unsupported ECDHE named group: {:?}", named_group))?; + .ok_or(CryptoError::UnsupportedEcdheNamedGroup(named_group))?; kx_buf.clear(); self.key_exchange = Some(kx_group.start_exchange(core::mem::take(kx_buf))?); @@ -195,13 +196,13 @@ impl CryptoContext { group: NamedGroup, server_public: &[u8], kx_buf: &mut Buf, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { // Find the matching key exchange group from the provider let kx_group = self .provider() .supported_kx_groups() .find(|g| g.name() == group) - .ok_or_else(|| format!("Unsupported ECDHE named group: {:?}", group))?; + .ok_or(CryptoError::UnsupportedEcdheNamedGroup(group))?; // Create a new ECDH key exchange kx_buf.clear(); @@ -223,10 +224,10 @@ impl CryptoContext { hash: HashAlgorithm, out: &mut Buf, scratch: &mut Buf, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { trace!("Deriving extended master secret"); let Some(pms) = &self.pre_master_secret else { - return Err("Pre-master secret not available".to_string()); + return Err(CryptoError::PreMasterSecretNotAvailable); }; crypto::prf_hkdf::prf_tls12( self.provider().hmac_provider, @@ -241,7 +242,7 @@ impl CryptoContext { let mut master_secret = ArrayVec::new(); master_secret .try_extend_from_slice(out) - .map_err(|_| "Master secret too long".to_string())?; + .map_err(|_| CryptoError::MasterSecretTooLong)?; self.master_secret = Some(master_secret); // Clear pre-master secret after use (security measure) self.pre_master_secret = None; @@ -256,9 +257,9 @@ impl CryptoContext { server_random: &[u8], key_block: &mut Buf, scratch: &mut Buf, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { let Some(master_secret) = &self.master_secret else { - return Err("Master secret not available".to_string()); + return Err(CryptoError::MasterSecretNotAvailable); }; // Store the randoms for later SRTP key export (RFC 5705) @@ -280,7 +281,7 @@ impl CryptoContext { .cipher_suites .iter() .find(|cs| cs.suite() == cipher_suite) - .ok_or_else(|| format!("Unsupported cipher suite: {:?}", cipher_suite))?; + .ok_or(CryptoError::UnsupportedCipherSuite(cipher_suite))?; // Get key sizes from the provider let (mac_key_len, enc_key_len, fixed_iv_len) = supported_cipher_suite.key_lengths(); @@ -343,10 +344,10 @@ impl CryptoContext { plaintext: &mut Buf, aad: Aad, nonce: Nonce, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { match &mut self.client_cipher { Some(cipher) => cipher.encrypt(plaintext, aad, nonce), - None => Err("Client cipher not initialized".to_string()), + None => Err(CryptoError::ClientCipherNotInitialized), } } @@ -356,10 +357,10 @@ impl CryptoContext { ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { match &mut self.server_cipher { Some(cipher) => cipher.decrypt(ciphertext, aad, nonce), - None => Err("Server cipher not initialized".to_string()), + None => Err(CryptoError::ServerCipherNotInitialized), } } @@ -369,10 +370,10 @@ impl CryptoContext { plaintext: &mut Buf, aad: Aad, nonce: Nonce, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { match &mut self.server_cipher { Some(cipher) => cipher.encrypt(plaintext, aad, nonce), - None => Err("Server cipher not initialized".to_string()), + None => Err(CryptoError::ServerCipherNotInitialized), } } @@ -382,10 +383,10 @@ impl CryptoContext { ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { match &mut self.client_cipher { Some(cipher) => cipher.decrypt(ciphertext, aad, nonce), - None => Err("Client cipher not initialized".to_string()), + None => Err(CryptoError::ClientCipherNotInitialized), } } @@ -424,9 +425,9 @@ impl CryptoContext { data: &[u8], hash_alg: HashAlgorithm, out: &mut Buf, - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { let AuthMode::Certificate { private_key, .. } = &mut self.auth else { - return Err("No private key configured (PSK mode)".to_string()); + return Err(CryptoError::NoPrivateKeyConfigured); }; private_key.sign(data, hash_alg, out) } @@ -439,10 +440,10 @@ impl CryptoContext { hash: HashAlgorithm, out: &mut Buf, scratch: &mut Buf, - ) -> Result, String> { + ) -> Result, CryptoError> { let master_secret = match &self.master_secret { Some(ms) => ms, - None => return Err("No master secret available".to_string()), + None => return Err(CryptoError::MasterSecretNotAvailable), }; let label = if is_client { @@ -465,7 +466,7 @@ impl CryptoContext { let mut verify_data = ArrayVec::new(); verify_data .try_extend_from_slice(out) - .map_err(|_| "Verify data too long".to_string())?; + .map_err(|_| CryptoError::VerifyDataTooLong)?; Ok(verify_data) } @@ -477,22 +478,22 @@ impl CryptoContext { hash: HashAlgorithm, out: &mut Buf, scratch: &mut Buf, - ) -> Result, String> { + ) -> Result, CryptoError> { const DTLS_SRTP_KEY_LABEL: &str = "EXTRACTOR-dtls_srtp"; let master_secret = match &self.master_secret { Some(ms) => ms, - None => return Err("No master secret available".to_string()), + None => return Err(CryptoError::MasterSecretNotAvailable), }; let client_random = match &self.client_random { Some(cr) => cr, - None => return Err("No client random available".to_string()), + None => return Err(CryptoError::ClientRandomNotAvailable), }; let server_random = match &self.server_random { Some(sr) => sr, - None => return Err("No server random available".to_string()), + None => return Err(CryptoError::ServerRandomNotAvailable), }; // Per RFC 5705, the exporter uses: PRF(master_secret, label, client_random + server_random) @@ -516,7 +517,7 @@ impl CryptoContext { let mut keying_material = ArrayVec::new(); keying_material .try_extend_from_slice(out) - .map_err(|_| "Keying material too long".to_string())?; + .map_err(|_| CryptoError::KeyingMaterialTooLong)?; Ok(keying_material) } @@ -598,7 +599,7 @@ impl CryptoContext { signature: &DigitallySigned, signature_buf: &[u8], cert_der: &[u8], - ) -> Result<(), String> { + ) -> Result<(), CryptoError> { self.provider().signature_verification.verify_signature( cert_der, data, diff --git a/src/dtls12/engine.rs b/src/dtls12/engine.rs index 0fdbadc3..69a31331 100644 --- a/src/dtls12/engine.rs +++ b/src/dtls12/engine.rs @@ -382,7 +382,7 @@ impl Engine { // The connect timeout is the overall timeout for establishing the connection if let Timeout::Armed(connect_timeout) = self.connect_timeout { if now >= connect_timeout { - return Err(Error::Timeout("connect")); + return Err(Error::Timeout(crate::TimeoutError::Connect)); } } @@ -402,7 +402,7 @@ impl Engine { self.flight_timeout = Timeout::Armed(timeout); self.flight_resend("flight timeout")?; } else { - return Err(Error::Timeout("handshake")); + return Err(Error::Timeout(crate::TimeoutError::Handshake)); } } @@ -729,10 +729,9 @@ impl Engine { F: FnOnce(&mut Buf), { let maybe_suite = if epoch >= 1 { - Some( - self.cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?, - ) + Some(self.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?) } else { None }; @@ -802,10 +801,11 @@ impl Engine { }; let Some(iv) = iv else { - return Err(Error::CryptoError(format!( - "{} write IV not available", - if self.is_client { "Client" } else { "Server" } - ))); + return Err(Error::CryptoError( + crate::CryptoError::WriteIvNotAvailable { + is_client: self.is_client, + }, + )); }; let explicit_nonce_len = self.explicit_nonce_len; @@ -818,10 +818,12 @@ impl Engine { Nonce::new(iv, &explicit_nonce) } _ => { - return Err(Error::CryptoError(format!( - "Unsupported DTLS 1.2 record_iv_len={} for {:?}", - explicit_nonce_len, suite - ))); + return Err(Error::CryptoError( + crate::CryptoError::UnsupportedDtls12RecordIvLen { + len: explicit_nonce_len, + suite, + }, + )); } }; @@ -922,8 +924,9 @@ impl Engine { // Per-record protection overhead on the wire (for AEAD suites this is // explicit_nonce + tag). Used to size fragments to fit the MTU. let protection_overhead = if epoch >= 1 { - self.cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + self.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; self.min_protected_fragment_len() } else { 0 @@ -1039,11 +1042,11 @@ impl Engine { if self.is_client { self.crypto_context .encrypt_client_to_server(plaintext, aad, nonce) - .map_err(|e| Error::CryptoError(format!("Client encryption failed: {}", e))) + .map_err(Error::CryptoError) } else { self.crypto_context .encrypt_server_to_client(plaintext, aad, nonce) - .map_err(|e| Error::CryptoError(format!("Server encryption failed: {}", e))) + .map_err(Error::CryptoError) } } @@ -1057,11 +1060,11 @@ impl Engine { if self.is_client { self.crypto_context .decrypt_server_to_client(ciphertext, aad, nonce) - .map_err(|e| Error::CryptoError(format!("Client decryption failed: {}", e))) + .map_err(Error::CryptoError) } else { self.crypto_context .decrypt_client_to_server(ciphertext, aad, nonce) - .map_err(|e| Error::CryptoError(format!("Server decryption failed: {}", e))) + .map_err(Error::CryptoError) } } @@ -1190,7 +1193,9 @@ impl Engine { pub fn generate_verify_data(&mut self, is_client: bool) -> Result<[u8; 12], Error> { let Some(suite) = self.cipher_suite() else { - return Err(Error::InvalidState("No cipher suite selected".to_string())); + return Err(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + )); }; let algorithm = suite.hash_algorithm(); let mut handshake_hash = self.buffers_free.pop(); @@ -1208,10 +1213,12 @@ impl Engine { &mut out, &mut scratch, ) - .map_err(|e| Error::CryptoError(format!("Failed to generate verify data: {}", e)))?; + .map_err(Error::CryptoError)?; if verify_data_vec.len() != 12 { - return Err(Error::CryptoError("Invalid verify data length".to_string())); + return Err(Error::CryptoError( + crate::CryptoError::InvalidVerifyDataLength, + )); } let mut verify_data = [0u8; 12]; @@ -1273,10 +1280,10 @@ impl RecordHandler for Engine { self.push_buffer(record.into_buffer()); if let Some(description) = fatal_description { - return Err(Error::SecurityError(format!( - "Received fatal alert: level=2, description={}", - description - ))); + return Err(Error::SecurityError(crate::SecurityError::FatalAlert { + level: 2, + description, + })); } return Ok(None); @@ -1301,10 +1308,10 @@ impl RecordHandler for Engine { } if level == 2 { - return Err(Error::SecurityError(format!( - "Received fatal alert: level={}, description={}", - level, description - ))); + return Err(Error::SecurityError(crate::SecurityError::FatalAlert { + level, + description, + })); } } diff --git a/src/dtls12/server.rs b/src/dtls12/server.rs index 15035091..54d11457 100644 --- a/src/dtls12/server.rs +++ b/src/dtls12/server.rs @@ -331,20 +331,20 @@ impl State { // Enforce DTLS1.2 if ch.client_version != ProtocolVersion::DTLS1_2 { - return Err((Error::SecurityError(format!( - "Unsupported DTLS version from client: {:?}", - ch.client_version - ))) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::UnsupportedClientVersion( + ch.client_version, + )) + .into(), + ); } // Enforce Null compression only (client must offer it) let has_null = ch.compression_methods.contains(&CompressionMethod::Null); if !has_null { - return Err((Error::SecurityError( - "Client did not offer Null compression".to_string(), - )) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::UnsupportedClientCompression).into(), + ); } trace!( @@ -403,9 +403,10 @@ impl State { } let Some(cs) = selected else { - return Err( - (Error::SecurityError("No mutually acceptable cipher suite".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::NoMutuallyAcceptableCipherSuite, + )) + .into()); }; server.engine.set_cipher_suite(cs); @@ -454,9 +455,9 @@ impl State { // EMS is mandatory if !client_offers_ems { - return Err((Error::SecurityError( - "Extended Master Secret not negotiated".to_string(), - )) + return Err(Error::SecurityError( + crate::SecurityError::ExtendedMasterSecretNotNegotiated, + ) .into()); } @@ -515,10 +516,9 @@ impl State { ) })?; - let cs = server - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let cs = server.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; // PSK suites skip Certificate if cs.is_psk() { @@ -541,18 +541,17 @@ impl State { fn send_server_key_exchange(self, server: &mut Server) -> Result { trace!("Sending ServerKeyExchange"); - let cs = server - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let cs = server.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; if cs.is_psk() { return self.send_server_key_exchange_psk(server); } - let client_random = server - .client_random - .ok_or_else(|| Error::InvalidState("No client random".to_string()))?; + let client_random = server.client_random.ok_or(Error::InvalidState( + crate::InvalidStateError::NoClientRandom, + ))?; // unwrap: is ok because we set the random in handle_timeout let server_random = server.random.unwrap(); @@ -572,13 +571,9 @@ impl State { ) .ok_or_else(|| { if server.client_supported_groups.is_some() { - Error::SecurityError( - "No common DTLS 1.2 key exchange group between client supported_groups \ - and server configuration" - .into(), - ) + Error::SecurityError(crate::SecurityError::NoCommonKeyExchangeGroup) } else { - Error::CryptoError("No DTLS 1.2 key exchange groups configured".into()) + Error::CryptoError(crate::CryptoError::NoDtls12KeyExchangeGroupsConfigured) } })?; @@ -677,10 +672,9 @@ impl State { .engine .create_handshake(MessageType::ServerHelloDone, |_, _| Ok(()))?; - let cs = server - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let cs = server.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; // PSK: no client certificates if cs.is_psk() { @@ -756,19 +750,18 @@ impl State { unreachable!() }; - let suite = server - .engine - .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite selected".to_string()))?; + let suite = server.engine.cipher_suite().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + ))?; if suite.is_psk() { // Extract PSK identity range before dropping handshake let identity_range = match &ckx.exchange_keys { ExchangeKeys::Psk(keys) => keys.identity_range.clone(), _ => { - return Err((Error::UnexpectedMessage( - "ECDHE ClientKeyExchange in PSK path".to_string(), - )) + return Err(Error::UnexpectedMessage( + crate::UnexpectedMessageError::EcdheClientKeyExchangeInPskPath, + ) .into()); } }; @@ -786,7 +779,7 @@ impl State { .engine .config() .psk_resolver() - .ok_or_else(|| Error::PskError("No PSK resolver configured".to_string()))? + .ok_or(Error::PskError(crate::PskError::NoPskResolverConfigured))? .resolve(identity); let (psk, psk_valid) = match resolved { @@ -803,15 +796,15 @@ impl State { crypto.set_psk(psk); crypto .compute_psk_pre_master_secret() - .map_err(|e| Error::CryptoError(format!("Failed to compute PSK PMS: {}", e)))?; + .map_err(Error::CryptoError)?; } else { // Extract client's public key range before dropping handshake let public_key_range = match &ckx.exchange_keys { ExchangeKeys::Ecdh(keys) => keys.public_key_range.clone(), ExchangeKeys::Psk(_) => { - return Err((Error::UnexpectedMessage( - "PSK ClientKeyExchange in ECDHE path".to_string(), - )) + return Err(Error::UnexpectedMessage( + crate::UnexpectedMessageError::PskClientKeyExchangeInEcdhePath, + ) .into()); } }; @@ -827,9 +820,7 @@ impl State { .engine .crypto_context_mut() .compute_shared_secret(client_pub, &mut buf) - .map_err(|e| { - Error::CryptoError(format!("Failed to compute shared secret: {}", e)) - })?; + .map_err(Error::CryptoError)?; server.engine.push_buffer(buf); } @@ -852,11 +843,12 @@ impl State { b }; - let session_hash = server.captured_session_hash.as_ref().ok_or_else(|| { - Error::InvalidState( - "Extended Master Secret negotiated but session hash not captured".to_string(), - ) - })?; + let session_hash = server + .captured_session_hash + .as_ref() + .ok_or(Error::InvalidState(crate::InvalidStateError::Other( + "Extended Master Secret negotiated but session hash not captured", + )))?; let mut out = server.engine.pop_buffer(); let mut scratch = server.engine.pop_buffer(); @@ -864,9 +856,7 @@ impl State { .engine .crypto_context_mut() .derive_extended_master_secret(session_hash, suite_hash, &mut out, &mut scratch) - .map_err(|e| { - Error::CryptoError(format!("Failed to derive extended master secret: {}", e)) - })?; + .map_err(Error::CryptoError)?; server .engine @@ -878,7 +868,7 @@ impl State { &mut out, &mut scratch, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive keys: {}", e)))?; + .map_err(Error::CryptoError)?; server.engine.push_buffer(out); server.engine.push_buffer(scratch); @@ -929,9 +919,9 @@ impl State { let signature_bytes = &server.defragment_buffer[signature_range]; if server.client_certificates.is_empty() { - return Err((Error::CertificateError( - "CertificateVerify received but no client certificate".to_string(), - )) + return Err(Error::CertificateError( + crate::CertificateError::NoClientCertificateForVerification, + ) .into()); } @@ -950,9 +940,7 @@ impl State { signature_bytes, &server.client_certificates[0], ) - .map_err(|e| { - Error::CryptoError(format!("Failed to verify client CertificateVerify: {}", e)) - })?; + .map_err(Error::CryptoError)?; debug!("Client CertificateVerify verified successfully"); @@ -1012,9 +1000,10 @@ impl State { // Use constant-time comparison to prevent timing attacks let is_eq: bool = verify_data.ct_eq(expected.as_slice()).into(); if !is_eq { - return Err( - (Error::SecurityError("Client Finished verification failed".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ClientFinishedVerificationFailed, + )) + .into()); } // Invariant: full PSK handshakes always set psk_valid in @@ -1039,9 +1028,10 @@ impl State { // skip ClientKeyExchange and therefore never set psk_valid — those // paths reuse a cached master_secret and don't consult the resolver. if server.psk_valid == Some(false) { - return Err( - (Error::SecurityError("Client Finished verification failed".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ClientFinishedVerificationFailed, + )) + .into()); } trace!("Client Finished verified successfully"); @@ -1164,9 +1154,12 @@ fn compute_cookie( client_random.serialize(&mut buf); let tag = hmac_provider .hmac_sha256(secret, &buf) - .map_err(|e| Error::CryptoError(format!("Failed to compute HMAC: {}", e)))?; - let cookie = Cookie::try_new(&tag) - .map_err(|_| Error::CryptoError("Failed to build cookie from HMAC output".to_string()))?; + .map_err(Error::CryptoError)?; + let cookie = Cookie::try_new(&tag).map_err(|_| { + Error::CryptoError(crate::CryptoError::OperationFailed( + crate::CryptoOperation::ComputeCookie, + )) + })?; Ok(cookie) } @@ -1204,7 +1197,7 @@ fn handshake_create_server_hello( let cs = engine .cipher_suite() - .ok_or_else(|| Error::InvalidState("No cipher suite".to_string()))?; + .ok_or(Error::InvalidState(crate::InvalidStateError::NoCipherSuite))?; let srtp_pid = negotiated_srtp_profile.map(|p| match p { SrtpProfile::AEAD_AES_256_GCM => SrtpProfileId::SRTP_AEAD_AES_256_GCM, @@ -1235,7 +1228,9 @@ fn handshake_create_server_key_exchange( algorithm: SignatureAndHashAlgorithm, ) -> Result<(), Error> { let Some(cipher_suite) = engine.cipher_suite() else { - return Err(Error::InvalidState("No cipher suite selected".to_string())); + return Err(Error::InvalidState( + crate::InvalidStateError::NoCipherSuiteSelected, + )); }; let key_exchange_algorithm = cipher_suite.as_key_exchange_algorithm(); @@ -1251,7 +1246,7 @@ fn handshake_create_server_key_exchange( let pubkey = engine .crypto_context_mut() .init_ecdh_server(named_group, &mut kx_buf) - .map_err(|e| Error::CryptoError(format!("Failed to init ECDHE: {}", e)))?; + .map_err(Error::CryptoError)?; trace!( "SKE ECDHE: group={:?}, pubkey_len={}", @@ -1277,9 +1272,7 @@ fn handshake_create_server_key_exchange( engine .crypto_context .sign_data(&signed_data, hash_alg, &mut signature) - .map_err(|e| { - Error::CryptoError(format!("Failed to sign server key exchange: {}", e)) - })?; + .map_err(Error::CryptoError)?; // unwrap: safe because init_ecdh_server() above sets key_exchange = Some(...). // If that failed, we returned Err earlier and never reach this point. @@ -1304,7 +1297,7 @@ fn handshake_create_server_key_exchange( Ok(()) } _ => Err(Error::SecurityError( - "Unsupported key exchange algorithm".to_string(), + crate::SecurityError::UnsupportedKeyExchangeAlgorithm, )), } } diff --git a/src/dtls13/client.rs b/src/dtls13/client.rs index a23d7a3c..40310d23 100644 --- a/src/dtls13/client.rs +++ b/src/dtls13/client.rs @@ -388,17 +388,22 @@ impl State { .kx_groups() .next() .map(|g| g.name()) - .ok_or_else(|| Error::CryptoError("No supported key exchange groups".to_string()))? + .ok_or(Error::CryptoError( + crate::CryptoError::NoSupportedKeyExchangeGroups, + ))? }; - let kx_group = client.engine.find_kx_group(group).ok_or_else(|| { - Error::CryptoError(format!("Key exchange group not found: {:?}", group)) - })?; + let kx_group = client + .engine + .find_kx_group(group) + .ok_or(Error::CryptoError( + crate::CryptoError::KeyExchangeGroupNotFound(group), + ))?; let kx_buf = client.engine.pop_buffer(); let key_exchange = kx_group .start_exchange(kx_buf) - .map_err(|e| Error::CryptoError(format!("Failed to start key exchange: {}", e)))?; + .map_err(Error::CryptoError)?; // Build the key_share extension data into extension_data buffer client.extension_data.clear(); @@ -496,9 +501,10 @@ impl State { .engine .is_cipher_suite_allowed(server_hello.cipher_suite) { - return Err( - (Error::SecurityError("HRR selected disallowed cipher suite".into())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::HrrSelectedDisallowedCipherSuite, + )) + .into()); } client.engine.set_cipher_suite(server_hello.cipher_suite); @@ -515,7 +521,9 @@ impl State { } } if !hrr_version_ok { - return Err((Error::SecurityError("HRR did not select DTLS 1.3".into())).into()); + return Err( + (Error::SecurityError(crate::SecurityError::HrrDidNotSelectDtls13)).into(), + ); } // Replace transcript with message_hash per RFC 8446 Section 4.4.1. @@ -536,17 +544,18 @@ impl State { // Validate legacy_version (must be DTLS 1.2) if server_hello.legacy_version != ProtocolVersion::DTLS1_2 { - return Err((Error::SecurityError( - "ServerHello legacy_version must be DTLS 1.2".into(), - )) + return Err(Error::SecurityError( + crate::SecurityError::ServerHelloLegacyVersionNotDtls12, + ) .into()); } // Validate legacy_compression_method (must be null) if server_hello.legacy_compression_method != CompressionMethod::Null { - return Err( - (Error::SecurityError("ServerHello compression must be null".into())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ServerHelloCompressionMustBeNull, + )) + .into()); } debug!( @@ -557,16 +566,16 @@ impl State { // Validate cipher suite let cs = server_hello.cipher_suite; if matches!(cs, Dtls13CipherSuite::Unknown(_)) { - return Err( - (Error::SecurityError("Server selected unknown cipher suite".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ServerSelectedUnknownCipherSuite, + )) + .into()); } if !client.engine.is_cipher_suite_allowed(cs) { - return Err((Error::SecurityError(format!( - "Server selected disallowed cipher suite: {:?}", - cs - ))) + return Err(Error::SecurityError( + crate::SecurityError::ServerSelectedDisallowedDtls13CipherSuite(cs), + ) .into()); } @@ -609,39 +618,38 @@ impl State { } if !supported_version_ok { - return Err((Error::SecurityError( - "Server did not negotiate DTLS 1.3 via supported_versions".to_string(), - )) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::ServerDidNotNegotiateDtls13).into(), + ); } let Some((server_group, ke_range)) = server_key_share else { - return Err((Error::SecurityError( - "Server did not provide key_share extension".to_string(), - )) - .into()); + return Err(Error::SecurityError(crate::SecurityError::ServerMissingKeyShare).into()); }; // Complete ECDHE key exchange let key_exchange = client .active_key_exchange .take() - .ok_or_else(|| Error::InvalidState("No active key exchange".to_string()))?; + .ok_or(Error::InvalidState( + crate::InvalidStateError::NoActiveKeyExchange, + ))?; if key_exchange.group() != server_group { - return Err((Error::SecurityError(format!( - "Server key share group mismatch: {:?} != {:?}", - server_group, - key_exchange.group() - ))) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::ServerKeyShareGroupMismatch { + selected: server_group, + actual: key_exchange.group(), + }) + .into(), + ); } let peer_pub_key = &client.extension_data[ke_range]; let mut shared_secret = client.engine.pop_buffer(); key_exchange .complete(peer_pub_key, &mut shared_secret) - .map_err(|e| Error::CryptoError(format!("ECDHE completion failed: {}", e)))?; + .map_err(Error::CryptoError)?; // Derive handshake secrets let (c_hs_traffic, s_hs_traffic, handshake_secret) = @@ -758,15 +766,17 @@ impl State { }; if !certificate.context_range.is_empty() { - return Err( - (Error::SecurityError("Server certificate context must be empty".into())).into(), - ); + return Err(Error::CertificateError( + crate::CertificateError::ServerCertificateContextMustBeEmpty, + ) + .into()); } if certificate.certificate_list.is_empty() { - return Err( - (Error::CertificateError("No server certificate received".to_string())).into(), - ); + return Err((Error::CertificateError( + crate::CertificateError::NoServerCertificateReceived, + )) + .into()); } debug!( @@ -826,11 +836,10 @@ impl State { // RFC 8446 §4.4.3: The receiver MUST verify that the signature algorithm // is one that was offered in the signature_algorithms extension. if !SignatureScheme::supported().contains(&scheme) { - return Err((Error::SecurityError(format!( - "Server used un-offered signature scheme: {:?}", - scheme - ))) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::SignatureSchemeNotOffered(scheme)) + .into(), + ); } // Build the signed content per RFC 8446 Section 4.4.3: @@ -844,14 +853,17 @@ impl State { let mut signature_copy = ArrayVec::::new(); signature_copy .try_extend_from_slice(signature) - .map_err(|_| Error::SecurityError("Signature too large".into()))?; + .map_err(|_| Error::SecurityError(crate::SecurityError::SignatureTooLarge))?; drop(maybe); // Verify the signature - let cert_der = client.server_certificates.first().ok_or_else(|| { - Error::CertificateError("No server certificate for verification".to_string()) - })?; + let cert_der = client + .server_certificates + .first() + .ok_or(Error::CertificateError( + crate::CertificateError::NoServerCertificateForVerification, + ))?; #[cfg(feature = "_crypto-common")] verify_scheme_curve(scheme, cert_der)?; @@ -875,10 +887,13 @@ impl State { fn await_finished(self, client: &mut Client) -> Result { // Compute expected verify_data BEFORE consuming Finished // (verify_data uses transcript hash up to but not including Finished) - let server_hs_secret = client - .server_hs_traffic_secret - .as_ref() - .ok_or_else(|| Error::InvalidState("No server handshake traffic secret".to_string()))?; + let server_hs_secret = + client + .server_hs_traffic_secret + .as_ref() + .ok_or(Error::InvalidState( + crate::InvalidStateError::NoServerHandshakeTrafficSecret, + ))?; let expected_verify_data = client.engine.compute_verify_data(server_hs_secret)?; let maybe = client @@ -904,9 +919,10 @@ impl State { // Constant-time comparison let is_eq: bool = verify_data.ct_eq(&*expected_verify_data).into(); if !is_eq { - return Err( - (Error::SecurityError("Server Finished verification failed".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ServerFinishedVerificationFailed, + )) + .into()); } trace!("Server Finished verified successfully"); @@ -916,9 +932,9 @@ impl State { client.engine.advance_peer_handshake_seq(); // Derive application secrets from handshake secret + transcript through server Finished - let handshake_secret = client.handshake_secret.as_ref().ok_or_else(|| { - Error::InvalidState("No handshake secret for application key derivation".to_string()) - })?; + let handshake_secret = client.handshake_secret.as_ref().ok_or(Error::InvalidState( + crate::InvalidStateError::NoHandshakeSecretForApplicationKeyDerivation, + ))?; let (c_ap_traffic, s_ap_traffic) = client.engine.derive_application_secrets(handshake_secret)?; @@ -1010,9 +1026,13 @@ impl State { client.engine.flight_begin(5); } - let client_hs_secret = client.client_hs_traffic_secret.as_ref().ok_or_else(|| { - Error::InvalidState("No client handshake traffic secret for Finished".to_string()) - })?; + let client_hs_secret = + client + .client_hs_traffic_secret + .as_ref() + .ok_or(Error::InvalidState( + crate::InvalidStateError::NoClientHandshakeTrafficSecretForFinished, + ))?; let mut client_hs_secret_copy = Buf::new(); client_hs_secret_copy.extend_from_slice(client_hs_secret); let client_hs_secret = client_hs_secret_copy; @@ -1347,7 +1367,7 @@ pub(crate) fn handshake_create_certificate_verify( engine .signing_key() .sign(&signed_content, hash_alg, &mut signature) - .map_err(|e| Error::CryptoError(format!("Failed to sign CertificateVerify: {}", e)))?; + .map_err(Error::CryptoError)?; // Determine the SignatureScheme from hash_alg + sig_alg let scheme = match (sig_alg, hash_alg) { @@ -1361,10 +1381,12 @@ pub(crate) fn handshake_create_certificate_verify( SignatureScheme::RSA_PSS_RSAE_SHA256 } _ => { - return Err(Error::CryptoError(format!( - "Unsupported signature algorithm: {:?}/{:?}", - sig_alg, hash_alg - ))); + return Err(Error::CryptoError( + crate::CryptoError::UnsupportedSignaturePair { + signature: sig_alg, + hash: hash_alg, + }, + )); } }; @@ -1399,7 +1421,7 @@ pub(crate) fn signature_scheme_to_components( Ok((HashAlgorithm::SHA512, SignatureAlgorithm::ECDSA)) } SignatureScheme::ED25519 => Err(Error::SecurityError( - "ED25519 not yet supported by crypto provider".into(), + crate::SecurityError::UnsupportedSignatureScheme(scheme), )), SignatureScheme::RSA_PSS_RSAE_SHA256 => { Ok((HashAlgorithm::SHA256, SignatureAlgorithm::RSA)) @@ -1416,12 +1438,11 @@ pub(crate) fn signature_scheme_to_components( SignatureScheme::RSA_PKCS1_SHA256 | SignatureScheme::RSA_PKCS1_SHA384 | SignatureScheme::RSA_PKCS1_SHA512 => Err(Error::SecurityError( - "RSA PKCS1v1.5 not allowed in TLS 1.3".into(), + crate::SecurityError::UnsupportedSignatureScheme(scheme), + )), + _ => Err(Error::SecurityError( + crate::SecurityError::UnsupportedSignatureScheme(scheme), )), - _ => Err(Error::SecurityError(format!( - "Unsupported signature scheme: {:?}", - scheme - ))), } } @@ -1430,12 +1451,16 @@ pub(crate) fn signature_scheme_to_components( #[cfg(feature = "_crypto-common")] pub(crate) fn verify_scheme_curve(scheme: SignatureScheme, cert_der: &[u8]) -> Result<(), Error> { if let Some(expected_group) = scheme.named_group() { - let cert_group = crate::crypto::cert_named_group(cert_der).map_err(Error::SecurityError)?; + let cert_group = + crate::crypto::cert_named_group(cert_der).map_err(Error::CertificateError)?; if expected_group != cert_group { - return Err(Error::SecurityError(format!( - "SignatureScheme {:?} requires {:?} but certificate uses {:?}", - scheme, expected_group, cert_group - ))); + return Err(Error::SecurityError( + crate::SecurityError::SignatureSchemeCertificateCurveMismatch { + scheme, + expected: expected_group, + actual: cert_group, + }, + )); } } Ok(()) diff --git a/src/dtls13/engine.rs b/src/dtls13/engine.rs index 9729d804..5cac6f9a 100644 --- a/src/dtls13/engine.rs +++ b/src/dtls13/engine.rs @@ -495,7 +495,7 @@ impl Engine { if let Timeout::Armed(connect_timeout) = self.connect_timeout { if now >= connect_timeout { - return Err(Error::Timeout("connect")); + return Err(Error::Timeout(crate::TimeoutError::Connect)); } } @@ -514,7 +514,7 @@ impl Engine { self.flight_timeout = Timeout::Armed(timeout); self.flight_resend("flight timeout")?; } else { - return Err(Error::Timeout("handshake")); + return Err(Error::Timeout(crate::TimeoutError::Handshake)); } } @@ -923,7 +923,7 @@ impl Engine { if self.sequence_epoch_0.sequence_number >= MAX_SEQUENCE_NUMBER { return Err(Error::CryptoError( - "Epoch 0 sequence number exhausted".to_string(), + crate::CryptoError::Epoch0SequenceNumberExhausted, )); } self.sequence_epoch_0.sequence_number += 1; @@ -999,10 +999,9 @@ impl Engine { }; let Some(keys) = keys else { - return Err(Error::CryptoError(format!( - "Send keys not available for epoch {}", - epoch - ))); + return Err(Error::CryptoError( + crate::CryptoError::SendKeysNotAvailable { epoch }, + )); }; // Construct the nonce: iv XOR padded_seq @@ -1033,7 +1032,7 @@ impl Engine { // Encrypt in place (appends tag) keys.cipher .encrypt(&mut fragment, aad, nonce) - .map_err(|e| Error::CryptoError(format!("Encryption failed: {}", e)))?; + .map_err(Error::CryptoError)?; // Record number encryption (RFC 9147 Section 4.2.3): // mask = AES-ECB(sn_key, ciphertext_sample) @@ -1080,21 +1079,21 @@ impl Engine { if epoch == 2 { if self.hs_send_seq >= MAX_SEQUENCE_NUMBER { return Err(Error::CryptoError( - "Handshake epoch sequence number exhausted".to_string(), + crate::CryptoError::SendSequenceNumberExhausted { epoch }, )); } self.hs_send_seq += 1; } else if self.prev_app_send_keys.is_some() && epoch == self.prev_app_send_epoch { if self.prev_app_send_seq >= MAX_SEQUENCE_NUMBER { return Err(Error::CryptoError( - "Previous epoch sequence number exhausted".to_string(), + crate::CryptoError::SendSequenceNumberExhausted { epoch }, )); } self.prev_app_send_seq += 1; } else { if self.app_send_seq >= MAX_SEQUENCE_NUMBER { return Err(Error::CryptoError( - "Application epoch sequence number exhausted".to_string(), + crate::CryptoError::SendSequenceNumberExhausted { epoch }, )); } self.app_send_seq += 1; @@ -1593,7 +1592,7 @@ impl Engine { let zeros = &zeros[..hash_len]; let mut early_secret = self.buffers_free.pop(); prf_hkdf::hkdf_extract(self.hmac(), hash, zeros, zeros, &mut early_secret) - .map_err(|e| Error::CryptoError(format!("Failed to derive early secret: {}", e)))?; + .map_err(Error::CryptoError)?; Ok(early_secret) } @@ -1623,12 +1622,12 @@ impl Engine { &mut derived, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive 'derived' secret: {}", e)))?; + .map_err(Error::CryptoError)?; // handshake_secret = HKDF-Extract(derived, shared_secret) let mut handshake_secret = Buf::new(); prf_hkdf::hkdf_extract(hmac, hash, &derived, shared_secret, &mut handshake_secret) - .map_err(|e| Error::CryptoError(format!("Failed to derive handshake secret: {}", e)))?; + .map_err(Error::CryptoError)?; // Get transcript hash up to and including ServerHello let mut transcript_hash = Buf::new(); @@ -1645,7 +1644,7 @@ impl Engine { &mut c_hs_traffic, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive c_hs_traffic: {}", e)))?; + .map_err(Error::CryptoError)?; // server_handshake_traffic_secret let mut s_hs_traffic = Buf::new(); @@ -1658,7 +1657,7 @@ impl Engine { &mut s_hs_traffic, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive s_hs_traffic: {}", e)))?; + .map_err(Error::CryptoError)?; Ok((c_hs_traffic, s_hs_traffic, handshake_secret)) } @@ -1707,14 +1706,14 @@ impl Engine { &mut derived, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive 'derived' for master: {}", e)))?; + .map_err(Error::CryptoError)?; // master_secret = HKDF-Extract(derived, 0) let zeros = [0u8; 48]; let zeros = &zeros[..hash_len]; let mut master_secret = Buf::new(); prf_hkdf::hkdf_extract(hmac, hash, &derived, zeros, &mut master_secret) - .map_err(|e| Error::CryptoError(format!("Failed to derive master secret: {}", e)))?; + .map_err(Error::CryptoError)?; // Get transcript hash up to and including server Finished let mut transcript_hash = Buf::new(); @@ -1731,9 +1730,7 @@ impl Engine { &mut exp_master, hash_len, ) - .map_err(|e| { - Error::CryptoError(format!("Failed to derive exporter master secret: {}", e)) - })?; + .map_err(Error::CryptoError)?; // client_application_traffic_secret_0 let mut c_ap_traffic = Buf::new(); @@ -1746,7 +1743,7 @@ impl Engine { &mut c_ap_traffic, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive c_ap_traffic: {}", e)))?; + .map_err(Error::CryptoError)?; // server_application_traffic_secret_0 let mut s_ap_traffic = Buf::new(); @@ -1759,7 +1756,7 @@ impl Engine { &mut s_ap_traffic, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive s_ap_traffic: {}", e)))?; + .map_err(Error::CryptoError)?; // Store exporter master secret (deferred to avoid borrow conflict with hmac) self.exporter_master_secret = Some(exp_master); @@ -1813,16 +1810,16 @@ impl Engine { &mut next, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive next traffic secret: {}", e)))?; + .map_err(Error::CryptoError)?; Ok(next) } /// Rotate send keys: move current app send keys → prev, derive new ones. fn update_send_keys(&mut self) -> Result<(), Error> { - let current_keys = self.app_send_keys.take().ok_or_else(|| { - Error::InvalidState("No current app send keys for KeyUpdate".to_string()) - })?; + let current_keys = self.app_send_keys.take().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCurrentAppSendKeysForKeyUpdate, + ))?; let next_secret = self.derive_next_traffic_secret(¤t_keys.traffic_secret)?; let new_keys = self.derive_epoch_keys(&next_secret)?; @@ -1851,9 +1848,9 @@ impl Engine { /// Returns the new epoch number. pub fn update_recv_keys(&mut self) -> Result { // Find the latest recv epoch entry - let latest = self.app_recv_keys.last().ok_or_else(|| { - Error::InvalidState("No current app recv keys for KeyUpdate".to_string()) - })?; + let latest = self.app_recv_keys.last().ok_or(Error::InvalidState( + crate::InvalidStateError::NoCurrentAppRecvKeysForKeyUpdate, + ))?; let next_secret = self.derive_next_traffic_secret(&latest.keys.traffic_secret)?; let new_epoch = latest.epoch + 1; @@ -1946,7 +1943,7 @@ impl Engine { &mut key, suite.key_len(), ) - .map_err(|e| Error::CryptoError(format!("Failed to derive key: {}", e)))?; + .map_err(Error::CryptoError)?; // iv = HKDF-Expand-Label(secret, "iv", "", iv_length) let mut iv_buf = Buf::new(); @@ -1959,7 +1956,7 @@ impl Engine { &mut iv_buf, suite.iv_len(), ) - .map_err(|e| Error::CryptoError(format!("Failed to derive iv: {}", e)))?; + .map_err(Error::CryptoError)?; // sn_key = HKDF-Expand-Label(secret, "sn", "", key_length) let mut sn_key = Buf::new(); @@ -1972,11 +1969,9 @@ impl Engine { &mut sn_key, suite.key_len(), ) - .map_err(|e| Error::CryptoError(format!("Failed to derive sn_key: {}", e)))?; + .map_err(Error::CryptoError)?; - let cipher = suite - .create_cipher(&key) - .map_err(|e| Error::CryptoError(format!("Failed to create cipher: {}", e)))?; + let cipher = suite.create_cipher(&key).map_err(Error::CryptoError)?; let mut iv = [0u8; 12]; iv.copy_from_slice(&iv_buf); @@ -2017,7 +2012,7 @@ impl Engine { &mut finished_key, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive finished key: {}", e)))?; + .map_err(Error::CryptoError)?; let mut transcript_hash = Buf::new(); self.transcript_hash(&mut transcript_hash); @@ -2031,7 +2026,7 @@ impl Engine { &transcript_hash, &mut verify_data, ) - .map_err(|e| Error::CryptoError(format!("Failed to compute verify data: {}", e)))?; + .map_err(Error::CryptoError)?; Ok(verify_data) } @@ -2158,7 +2153,7 @@ impl Engine { .crypto_provider() .signature_verification .verify_signature(cert_der, data, signature, hash_alg, sig_alg) - .map_err(|e| Error::CryptoError(format!("Signature verification failed: {}", e))) + .map_err(Error::CryptoError) } // ========================================================================= @@ -2173,9 +2168,12 @@ impl Engine { let hash_len = hash.output_len(); let hmac = self.hmac(); - let exp_master = self.exporter_master_secret.as_ref().ok_or_else(|| { - Error::CryptoError("Exporter master secret not yet derived".to_string()) - })?; + let exp_master = self + .exporter_master_secret + .as_ref() + .ok_or(Error::CryptoError( + crate::CryptoError::ExporterMasterSecretNotDerived, + ))?; let total_len = profile.keying_material_len(); @@ -2192,7 +2190,7 @@ impl Engine { &mut derived, hash_len, ) - .map_err(|e| Error::CryptoError(format!("Failed to derive SRTP exporter secret: {}", e)))?; + .map_err(Error::CryptoError)?; // 2. result = HKDF-Expand-Label(derived_secret, "exporter", Hash(context), length) let context_hash = self.transcript_hash_of(b""); @@ -2206,9 +2204,7 @@ impl Engine { &mut keying_material_buf, total_len, ) - .map_err(|e| { - Error::CryptoError(format!("Failed to extract SRTP keying material: {}", e)) - })?; + .map_err(Error::CryptoError)?; let mut keying_material = ArrayVec::new(); for &b in keying_material_buf.iter().take(total_len) { @@ -2331,10 +2327,12 @@ impl RecordHandler for Engine { Ok(None) } Some(90) => Ok(None), - Some(description) => Err(Error::SecurityError(format!( - "Received fatal alert: description={}", - description - ))), + Some(description) => { + Err(Error::SecurityError(crate::SecurityError::FatalAlert { + level: 2, + description, + })) + } None => Ok(None), } } @@ -2453,10 +2451,9 @@ impl RecordHandler for Engine { }; let Some(keys) = keys else { - return Err(Error::CryptoError(format!( - "No recv keys for epoch {}", - seq.epoch - ))); + return Err(Error::CryptoError( + crate::CryptoError::RecvKeysNotAvailable { epoch: seq.epoch }, + )); }; // Construct nonce: iv XOR padded_seq @@ -2467,7 +2464,7 @@ impl RecordHandler for Engine { keys.cipher .decrypt(ciphertext, aad, nonce) - .map_err(|e| Error::CryptoError(format!("Decryption failed: {}", e)))?; + .map_err(Error::CryptoError)?; Ok(()) } diff --git a/src/dtls13/server.rs b/src/dtls13/server.rs index 3986797c..4711c8d4 100644 --- a/src/dtls13/server.rs +++ b/src/dtls13/server.rs @@ -419,9 +419,9 @@ impl State { // Validate legacy_version if client_hello.legacy_version != ProtocolVersion::DTLS1_2 { - return Err((Error::SecurityError( - "ClientHello legacy_version must be DTLS 1.2".to_string(), - )) + return Err(Error::SecurityError( + crate::SecurityError::ClientHelloLegacyVersionNotDtls12, + ) .into()); } @@ -430,9 +430,9 @@ impl State { .legacy_compression_methods .contains(&CompressionMethod::Null); if !has_null_compression { - return Err((Error::SecurityError( - "ClientHello must offer null compression".to_string(), - )) + return Err(Error::SecurityError( + crate::SecurityError::ClientHelloMustOfferNullCompression, + ) .into()); } @@ -495,7 +495,7 @@ impl State { let (_, cookie) = parse_cookie_extension(ext_data).map_err(InternalError::from)?; client_cookie_data = Some(ArrayVec::try_from(cookie).map_err(|_| { - Error::SecurityError("Invalid cookie in ClientHello".to_string()) + Error::SecurityError(crate::SecurityError::InvalidCookieInClientHello) })?); } _ => {} @@ -506,9 +506,9 @@ impl State { if server.auto_mode { return Err((Error::Dtls12Fallback).into()); } - return Err((Error::SecurityError( - "ClientHello must include DTLS 1.3 in supported_versions".to_string(), - )) + return Err(Error::SecurityError( + crate::SecurityError::ClientHelloMissingDtls13SupportedVersions, + ) .into()); } @@ -518,7 +518,9 @@ impl State { .iter() .find(|cs| server.engine.is_cipher_suite_allowed(**cs)) .copied() - .ok_or_else(|| Error::SecurityError("No common cipher suite found".to_string()))?; + .ok_or(Error::SecurityError( + crate::SecurityError::NoCommonCipherSuite, + ))?; // Save the client random and session_id early so HRR can use them let client_random = client_hello.random; @@ -568,9 +570,9 @@ impl State { // Validate the cookie let is_valid: bool = cookie_bytes.as_slice().ct_eq(&expected_cookie).into(); if !is_valid { - return Err((Error::SecurityError( - "Invalid cookie in ClientHello".to_string(), - )) + return Err(Error::SecurityError( + crate::SecurityError::InvalidCookieInClientHello, + ) .into()); } debug!("Cookie validated successfully"); @@ -618,9 +620,10 @@ impl State { ); let is_valid: bool = cookie_bytes.as_slice().ct_eq(&expected_cookie).into(); if !is_valid { - return Err( - (Error::SecurityError("Invalid cookie in ClientHello".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::InvalidCookieInClientHello, + )) + .into()); } } @@ -643,9 +646,9 @@ impl State { return if let Some(group) = common_group { // Need HRR for key exchange if server.hello_retry { - return Err((Error::SecurityError( - "Cannot send second HelloRetryRequest".to_string(), - )) + return Err(Error::SecurityError( + crate::SecurityError::CannotSendSecondHelloRetryRequest, + ) .into()); } @@ -679,22 +682,22 @@ impl State { Ok(Self::AwaitClientHello) } else { - Err(Error::SecurityError("No common key exchange group".to_string()).into()) + Err(Error::SecurityError(crate::SecurityError::NoCommonKeyExchangeGroup).into()) }; }; // Start ECDHE key exchange - let kx_group = server.engine.find_kx_group(selected_group).ok_or_else(|| { - Error::CryptoError(format!( - "Key exchange group not found: {:?}", - selected_group - )) - })?; + let kx_group = server + .engine + .find_kx_group(selected_group) + .ok_or(Error::CryptoError( + crate::CryptoError::KeyExchangeGroupNotFound(selected_group), + ))?; let kx_buf = server.engine.pop_buffer(); let key_exchange = kx_group .start_exchange(kx_buf) - .map_err(|e| Error::CryptoError(format!("Failed to start key exchange: {}", e)))?; + .map_err(Error::CryptoError)?; // Store server's public key in extension_data server.extension_data.clear(); @@ -708,7 +711,7 @@ impl State { let mut shared_secret = server.engine.pop_buffer(); key_exchange .complete(peer_pub_key, &mut shared_secret) - .map_err(|e| Error::CryptoError(format!("ECDHE completion failed: {}", e)))?; + .map_err(Error::CryptoError)?; server.shared_secret = Some(shared_secret); @@ -808,9 +811,9 @@ impl State { })?; // Derive handshake secrets - let shared_secret = server.shared_secret.take().ok_or_else(|| { - Error::InvalidState("No shared secret for handshake key derivation".to_string()) - })?; + let shared_secret = server.shared_secret.take().ok_or(Error::InvalidState( + crate::InvalidStateError::NoSharedSecretForHandshakeKeyDerivation, + ))?; let (c_hs_traffic, s_hs_traffic, handshake_secret) = server.engine.derive_handshake_secrets(&shared_secret)?; @@ -898,9 +901,13 @@ impl State { fn send_finished(self, server: &mut Server) -> Result { trace!("Sending server Finished message"); - let server_hs_secret = server.server_hs_traffic_secret.as_ref().ok_or_else(|| { - Error::InvalidState("No server handshake traffic secret for Finished".to_string()) - })?; + let server_hs_secret = + server + .server_hs_traffic_secret + .as_ref() + .ok_or(Error::InvalidState( + crate::InvalidStateError::NoServerHandshakeTrafficSecretForFinished, + ))?; let mut server_hs_secret_copy = Buf::new(); server_hs_secret_copy.extend_from_slice(server_hs_secret); let server_hs_secret = server_hs_secret_copy; @@ -914,9 +921,9 @@ impl State { })?; // Derive application secrets from handshake secret + transcript through server Finished - let handshake_secret = server.handshake_secret.as_ref().ok_or_else(|| { - Error::InvalidState("No handshake secret for application key derivation".to_string()) - })?; + let handshake_secret = server.handshake_secret.as_ref().ok_or(Error::InvalidState( + crate::InvalidStateError::NoHandshakeSecretForApplicationKeyDerivation, + ))?; let (c_ap_traffic, s_ap_traffic) = server.engine.derive_application_secrets(handshake_secret)?; @@ -1023,11 +1030,10 @@ impl State { // RFC 8446 §4.4.3: The signature algorithm MUST be one offered in // the signature_algorithms field of the CertificateRequest message. if !SignatureScheme::supported().contains(&scheme) { - return Err((Error::SecurityError(format!( - "Client used un-offered signature scheme: {:?}", - scheme - ))) - .into()); + return Err( + Error::SecurityError(crate::SecurityError::SignatureSchemeNotOffered(scheme)) + .into(), + ); } // Build the signed content per RFC 8446 Section 4.4.3: @@ -1041,14 +1047,17 @@ impl State { let mut signature_copy = ArrayVec::::new(); signature_copy .try_extend_from_slice(signature) - .map_err(|_| Error::SecurityError("Signature too large".into()))?; + .map_err(|_| Error::SecurityError(crate::SecurityError::SignatureTooLarge))?; drop(maybe); // Verify the signature - let cert_der = server.client_certificates.first().ok_or_else(|| { - Error::CertificateError("No client certificate for verification".to_string()) - })?; + let cert_der = server + .client_certificates + .first() + .ok_or(Error::CertificateError( + crate::CertificateError::NoClientCertificateForVerification, + ))?; #[cfg(feature = "_crypto-common")] verify_scheme_curve(scheme, cert_der)?; @@ -1072,10 +1081,13 @@ impl State { fn await_finished(self, server: &mut Server) -> Result { // Compute expected verify_data BEFORE consuming Finished // (verify_data uses transcript hash up to but not including Finished) - let client_hs_secret = server - .client_hs_traffic_secret - .as_ref() - .ok_or_else(|| Error::InvalidState("No client handshake traffic secret".to_string()))?; + let client_hs_secret = + server + .client_hs_traffic_secret + .as_ref() + .ok_or(Error::InvalidState( + crate::InvalidStateError::NoClientHandshakeTrafficSecret, + ))?; let expected_verify_data = server.engine.compute_verify_data(client_hs_secret)?; let maybe = server @@ -1101,9 +1113,10 @@ impl State { // Constant-time comparison let is_eq: bool = verify_data.ct_eq(&*expected_verify_data).into(); if !is_eq { - return Err( - (Error::SecurityError("Client Finished verification failed".to_string())).into(), - ); + return Err((Error::SecurityError( + crate::SecurityError::ClientFinishedVerificationFailed, + )) + .into()); } trace!("Client Finished verified successfully"); diff --git a/src/error.rs b/src/error.rs index c4f4889b..50bfd30b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,32 +1,43 @@ //! Public error type returned by the high-level DTLS API. -#[derive(Debug)] +use std::fmt; + +use crate::dtls12::message::Dtls12CipherSuite; +use crate::types::CompressionMethod; +use crate::types::Dtls13CipherSuite; +use crate::types::HashAlgorithm; +use crate::types::NamedGroup; +use crate::types::ProtocolVersion; +use crate::types::SignatureAlgorithm; +use crate::types::SignatureScheme; + +#[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] /// Errors returned by DTLS processing functions. pub enum Error { - /// Unexpected DTLS message - UnexpectedMessage(String), - /// Local state was missing data required for the requested operation - InvalidState(String), - /// Cryptographic operation failed - CryptoError(String), - /// Certificate validation failed - CertificateError(String), - /// Security policy violation - SecurityError(String), - /// PSK (Pre-Shared Key) error - PskError(String), - /// Incoming queue exceeded capacity + /// Unexpected DTLS message. + UnexpectedMessage(UnexpectedMessageError), + /// Local state was missing data required for the requested operation. + InvalidState(InvalidStateError), + /// Cryptographic operation failed. + CryptoError(CryptoError), + /// Certificate validation failed. + CertificateError(CertificateError), + /// Security policy violation. + SecurityError(SecurityError), + /// PSK (Pre-Shared Key) error. + PskError(PskError), + /// Incoming queue exceeded capacity. ReceiveQueueFull, - /// Outgoing queue exceeded capacity + /// Outgoing queue exceeded capacity. TransmitQueueFull, - /// Missing fields when parsing ServerHello + /// Missing fields when parsing ServerHello. IncompleteServerHello, - /// Something timed out - Timeout(&'static str), - /// Configuration error (e.g., invalid crypto provider) - ConfigError(String), - /// Peer attempted renegotiation (not supported) + /// Something timed out. + Timeout(TimeoutError), + /// Configuration error (e.g., invalid crypto provider). + ConfigError(ConfigError), + /// Peer attempted renegotiation (not supported). RenegotiationAttempt, /// Application data cannot be sent because the handshake is not yet complete. /// @@ -45,11 +56,368 @@ pub enum Error { /// packets. /// /// This value should never be seen outside dimpl. It's an internal - /// value to communicate from dtls13/server.rs to lib.rs + /// value to communicate from dtls13/server.rs to lib.rs. #[doc(hidden)] Dtls12Fallback, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum UnexpectedMessageError { + UnrecognizedAutoServerResponse, + ServerKeyExchangeWithoutSignature, + PskServerKeyExchangeInEcdhePath, + EcdheServerKeyExchangeInPskPath, + PskClientKeyExchangeInEcdhePath, + EcdheClientKeyExchangeInPskPath, + CertificateRequestContextTruncated, + CertificateRequestExtensionsTruncated, + Other(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum InvalidStateError { + NoCipherSuiteSelected, + NoCipherSuite, + NoClientRandom, + NoServerRandom, + NoSharedSecretForHandshakeKeyDerivation, + NoServerHandshakeTrafficSecret, + NoServerHandshakeTrafficSecretForFinished, + NoClientHandshakeTrafficSecret, + NoClientHandshakeTrafficSecretForFinished, + NoHandshakeSecretForApplicationKeyDerivation, + NoActiveKeyExchange, + NoCurrentAppSendKeysForKeyUpdate, + NoCurrentAppRecvKeysForKeyUpdate, + ExporterMasterSecretNotDerived, + Other(&'static str), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum CryptoError { + NoSupportedKeyExchangeGroups, + NoDtls12KeyExchangeGroupsConfigured, + Epoch0SequenceNumberExhausted, + SendSequenceNumberExhausted { + epoch: u16, + }, + SendKeysNotAvailable { + epoch: u16, + }, + RecvKeysNotAvailable { + epoch: u16, + }, + KeyExchangeGroupNotFound(NamedGroup), + KeyExchangeNotInitialized, + UnsupportedKeyExchangeGroup(NamedGroup), + UnsupportedEcdheNamedGroup(NamedGroup), + UnsupportedCipherSuite(Dtls12CipherSuite), + UnsupportedHmacHash(HashAlgorithm), + UnsupportedSignatureAlgorithm(SignatureAlgorithm), + SignatureAlgorithmNotOfferedByClient, + SignatureAlgorithmMismatch { + expected: SignatureAlgorithm, + actual: SignatureAlgorithm, + }, + UnsupportedSignaturePair { + signature: SignatureAlgorithm, + hash: HashAlgorithm, + }, + UnsupportedSignatureVerification { + signature: SignatureAlgorithm, + hash: HashAlgorithm, + group: NamedGroup, + }, + UnsupportedPublicKeyAlgorithm, + UnsupportedEcCurve(String), + MissingEcCurveParameter, + InvalidEcCurveParameter, + InvalidSubjectPublicKey, + InvalidSignatureFormat, + InvalidPublicKey(NamedGroup), + InvalidPrivateKey, + SigningKeyHashMismatch { + key_hash: HashAlgorithm, + requested: HashAlgorithm, + }, + SigningKeyUnsupportedHash { + group: NamedGroup, + hash: HashAlgorithm, + }, + InvalidAesGcmKeySize { + actual: usize, + }, + InvalidChacha20Poly1305KeySize { + actual: usize, + }, + InvalidAes128Ccm8KeySize { + actual: usize, + }, + InvalidNonce, + InvalidNonceLength { + expected: usize, + actual: usize, + }, + CiphertextTooShort { + minimum: usize, + actual: usize, + }, + HkdfOutputTooLong, + HkdfLabelTooLong, + HkdfContextTooLong, + HkdfOutputLengthTooLarge, + InvalidVerifyDataLength, + VerifyDataTooLong, + MasterSecretTooLong, + KeyingMaterialTooLong, + PreMasterSecretNotAvailable, + MasterSecretNotAvailable, + ClientRandomNotAvailable, + ServerRandomNotAvailable, + ClientCipherNotInitialized, + ServerCipherNotInitialized, + WriteIvNotAvailable { + is_client: bool, + }, + UnsupportedDtls12RecordIvLen { + len: usize, + suite: Dtls12CipherSuite, + }, + NoPrivateKeyConfigured, + PskNotSet, + ExporterMasterSecretNotDerived, + OperationFailed(CryptoOperation), + ProviderFailure { + operation: CryptoOperation, + reason: String, + }, + Other(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum CryptoOperation { + CreateCipher, + Encrypt, + Decrypt, + Sign, + VerifySignature, + LoadPrivateKey, + StartKeyExchange, + CompleteKeyExchange, + GenerateEphemeralKey, + ComputePublicKey, + FillRandom, + ComputeHmac, + Prf, + HkdfExtract, + HkdfExpand, + HkdfExpandLabel, + DeriveEarlySecret, + DeriveDerivedSecret, + DeriveHandshakeSecret, + DeriveTrafficSecret, + DeriveMasterSecret, + DeriveExporterMasterSecret, + DeriveNextTrafficSecret, + DeriveKey, + DeriveIv, + DeriveSequenceNumberKey, + DeriveFinishedKey, + ComputeVerifyData, + VerifyData, + ComputePskPreMasterSecret, + ComputeCookie, + ExtractSrtpKeyingMaterial, + EncodeKey, + DecodeKey, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum CertificateError { + NoServerCertificateReceived, + NoClientCertificateForVerification, + NoServerCertificateForVerification, + ServerCertificateContextMustBeEmpty, + ClientCertificateContextMustBeEmpty, + UnsupportedHashAlgorithm(HashAlgorithm), + ParseFailed, + MissingEcCurveParameter, + InvalidEcCurveParameter, + UnsupportedEcCurve(String), + InvalidSubjectPublicKey, + PrivateKey(CryptoError), + Verification(CryptoError), + Other(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum SecurityError { + UnsupportedHelloVerifyRequestVersion(ProtocolVersion), + UnsupportedServerVersion(ProtocolVersion), + UnsupportedClientVersion(ProtocolVersion), + UnsupportedServerCompression(CompressionMethod), + UnsupportedClientCompression, + UnsupportedKeyExchangeAlgorithm, + ServerSelectedUnknownCipherSuite, + ServerSelectedIncompatibleCipherSuite(Dtls12CipherSuite), + ServerSelectedDisallowedCipherSuite(Dtls12CipherSuite), + ServerSelectedDisallowedDtls13CipherSuite(Dtls13CipherSuite), + ExtendedMasterSecretNotNegotiated, + NoMutuallyAcceptableCipherSuite, + ClientHelloLegacyVersionNotDtls12, + ServerHelloLegacyVersionNotDtls12, + ClientHelloMissingDtls13SupportedVersions, + ClientHelloMustOfferNullCompression, + InvalidCookieInClientHello, + CannotSendSecondHelloRetryRequest, + NoCommonCipherSuite, + NoCommonKeyExchangeGroup, + HrrSelectedDisallowedCipherSuite, + HrrDidNotSelectDtls13, + ServerHelloCompressionMustBeNull, + ServerDidNotNegotiateDtls13, + ServerMissingKeyShare, + ServerKeyShareGroupMismatch { + selected: NamedGroup, + actual: NamedGroup, + }, + SignatureTooLarge, + SignatureSchemeNotOffered(SignatureScheme), + SignatureAlgorithmMismatch { + expected: SignatureAlgorithm, + actual: SignatureAlgorithm, + }, + UnsupportedSignatureScheme(SignatureScheme), + SignatureSchemeCertificateCurveMismatch { + scheme: SignatureScheme, + expected: NamedGroup, + actual: NamedGroup, + }, + ServerFinishedVerificationFailed, + ClientFinishedVerificationFailed, + FatalAlert { + level: u8, + description: u8, + }, + Other(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum PskError { + NoPskResolverConfigured, + NoPskIdentityConfigured, + ResolverReturnedNoKey, + Other(&'static str), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum TimeoutError { + HybridClientHello, + Connect, + Handshake, + Other(&'static str), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum ConfigError { + NoCryptoProvider, + MtuTooSmall { mtu: usize, minimum: usize }, + AeadEncryptionLimitTooSmall, + NoCipherSuitesAfterFiltering, + PskConfiguredWithoutPskCipherSuite, + NoDtls12KeyExchangeGroupsAfterFiltering, + NoDtls13KeyExchangeGroupsAfterFiltering, + CryptoProvider(CryptoProviderValidationError), + Other(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum CryptoProviderValidationError { + NoCipherSuites, + EcdhCipherSuitesWithoutKeyExchangeGroups, + NoDtls13CipherSuites, + MissingHashTestVector(HashAlgorithm), + HashProviderIncorrect(HashAlgorithm), + PrfFailed { + hash: HashAlgorithm, + source: CryptoError, + }, + PrfWrongLength { + hash: HashAlgorithm, + expected: usize, + actual: usize, + }, + MissingPrfTestVector(HashAlgorithm), + PrfIncorrect(HashAlgorithm), + NoSignatureValidationVector { + hash: HashAlgorithm, + signature: SignatureAlgorithm, + }, + SignatureVerificationFailed { + hash: HashAlgorithm, + signature: SignatureAlgorithm, + source: CryptoError, + }, + HkdfFailed { + suite: Dtls13CipherSuite, + source: CryptoError, + }, + HkdfEmptyOutput(Dtls13CipherSuite), + NoAeadTestVector(Dtls13CipherSuite), + AeadCreateFailed { + suite: Dtls13CipherSuite, + source: CryptoError, + }, + AeadEncryptFailed { + suite: Dtls13CipherSuite, + source: CryptoError, + }, + AeadEncryptWrongOutput(Dtls13CipherSuite), + AeadDecryptFailed { + suite: Dtls13CipherSuite, + source: CryptoError, + }, + AeadDecryptWrongOutput(Dtls13CipherSuite), + NoRecordNumberEncryptionTestVector(Dtls13CipherSuite), + RecordNumberEncryptionWrongMask(Dtls13CipherSuite), + KeyExchangeStartFailed { + group: NamedGroup, + source: CryptoError, + }, + KeyExchangeCompleteFailed { + group: NamedGroup, + source: CryptoError, + }, + KeyExchangeMismatchedSharedSecret(NamedGroup), + HmacFailed(CryptoError), + HmacWrongLength { + expected: usize, + actual: usize, + }, + HmacIncorrect, + Other(String), +} + #[derive(Debug)] pub(crate) enum InternalError { Transient(TransientError), @@ -100,10 +468,100 @@ impl<'a> From>> for InternalError { } } +impl From for Error { + fn from(value: CryptoError) -> Self { + Self::CryptoError(value) + } +} + +impl From for Error { + fn from(value: CertificateError) -> Self { + Self::CertificateError(value) + } +} + +impl From for Error { + fn from(value: SecurityError) -> Self { + Self::SecurityError(value) + } +} + +impl From for Error { + fn from(value: PskError) -> Self { + Self::PskError(value) + } +} + +impl From for Error { + fn from(value: ConfigError) -> Self { + Self::ConfigError(value) + } +} + +impl From for UnexpectedMessageError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl From<&'static str> for UnexpectedMessageError { + fn from(value: &'static str) -> Self { + Self::Other(value.to_string()) + } +} + +impl From for CryptoError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl From<&'static str> for CryptoError { + fn from(value: &'static str) -> Self { + Self::Other(value.to_string()) + } +} + +impl From for CertificateError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl From<&'static str> for CertificateError { + fn from(value: &'static str) -> Self { + Self::Other(value.to_string()) + } +} + +impl From for SecurityError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl From<&'static str> for SecurityError { + fn from(value: &'static str) -> Self { + Self::Other(value.to_string()) + } +} + +impl From for ConfigError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl From<&'static str> for ConfigError { + fn from(value: &'static str) -> Self { + Self::Other(value.to_string()) + } +} + impl std::error::Error for Error {} -impl std::fmt::Display for InternalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for InternalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { InternalError::Transient(err) => err.fmt(f), InternalError::Fatal(err) => err.fmt(f), @@ -111,8 +569,8 @@ impl std::fmt::Display for InternalError { } } -impl std::fmt::Display for TransientError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for TransientError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TransientError::ParseIncomplete => write!(f, "parse incomplete"), TransientError::Parse(kind) => write!(f, "parse error: {:?}", kind), @@ -121,29 +579,594 @@ impl std::fmt::Display for TransientError { } } -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Error::UnexpectedMessage(msg) => write!(f, "unexpected message: {}", msg), - Error::InvalidState(msg) => write!(f, "invalid state: {}", msg), - Error::CryptoError(msg) => write!(f, "crypto error: {}", msg), - Error::CertificateError(msg) => write!(f, "certificate error: {}", msg), - Error::SecurityError(msg) => write!(f, "security error: {}", msg), - Error::PskError(msg) => write!(f, "psk error: {}", msg), + Error::UnexpectedMessage(err) => write!(f, "unexpected message: {err}"), + Error::InvalidState(err) => write!(f, "invalid state: {err}"), + Error::CryptoError(err) => write!(f, "crypto error: {err}"), + Error::CertificateError(err) => write!(f, "certificate error: {err}"), + Error::SecurityError(err) => write!(f, "security error: {err}"), + Error::PskError(err) => write!(f, "psk error: {err}"), Error::ReceiveQueueFull => write!(f, "receive queue full"), Error::TransmitQueueFull => write!(f, "transmit queue full"), Error::IncompleteServerHello => write!(f, "incomplete ServerHello"), - Error::Timeout(what) => write!(f, "timeout: {}", what), - Error::ConfigError(msg) => write!(f, "config error: {}", msg), + Error::Timeout(err) => write!(f, "timeout: {err}"), + Error::ConfigError(err) => write!(f, "config error: {err}"), Error::RenegotiationAttempt => write!(f, "peer attempted renegotiation"), Error::HandshakePending => { write!(f, "handshake pending: cannot send application data yet") } Error::TooManyClientHelloFragments => write!(f, "too many client hello fragments"), Error::ConnectionClosed => write!(f, "connection closed"), - Error::Dtls12Fallback => { - write!(f, "dtls 1.2 fallback (internal)") + Error::Dtls12Fallback => write!(f, "dtls 1.2 fallback (internal)"), + } + } +} + +impl fmt::Display for UnexpectedMessageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnrecognizedAutoServerResponse => write!(f, "unrecognized response from server"), + Self::ServerKeyExchangeWithoutSignature => { + write!(f, "ServerKeyExchange without signature") + } + Self::PskServerKeyExchangeInEcdhePath => { + write!(f, "PSK ServerKeyExchange in ECDHE path") + } + Self::EcdheServerKeyExchangeInPskPath => { + write!(f, "ECDHE ServerKeyExchange in PSK path") + } + Self::PskClientKeyExchangeInEcdhePath => { + write!(f, "PSK ClientKeyExchange in ECDHE path") + } + Self::EcdheClientKeyExchangeInPskPath => { + write!(f, "ECDHE ClientKeyExchange in PSK path") + } + Self::CertificateRequestContextTruncated => { + write!(f, "CertificateRequest context truncated") + } + Self::CertificateRequestExtensionsTruncated => { + write!(f, "CertificateRequest extensions truncated") + } + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for InvalidStateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoCipherSuiteSelected => write!(f, "no cipher suite selected"), + Self::NoCipherSuite => write!(f, "no cipher suite"), + Self::NoClientRandom => write!(f, "no client random"), + Self::NoServerRandom => write!(f, "no server random"), + Self::NoSharedSecretForHandshakeKeyDerivation => { + write!(f, "no shared secret for handshake key derivation") + } + Self::NoServerHandshakeTrafficSecret => { + write!(f, "no server handshake traffic secret") + } + Self::NoServerHandshakeTrafficSecretForFinished => { + write!(f, "no server handshake traffic secret for Finished") + } + Self::NoClientHandshakeTrafficSecret => { + write!(f, "no client handshake traffic secret") + } + Self::NoClientHandshakeTrafficSecretForFinished => { + write!(f, "no client handshake traffic secret for Finished") + } + Self::NoHandshakeSecretForApplicationKeyDerivation => { + write!(f, "no handshake secret for application key derivation") + } + Self::NoActiveKeyExchange => write!(f, "no active key exchange"), + Self::NoCurrentAppSendKeysForKeyUpdate => { + write!(f, "no current app send keys for KeyUpdate") + } + Self::NoCurrentAppRecvKeysForKeyUpdate => { + write!(f, "no current app recv keys for KeyUpdate") + } + Self::ExporterMasterSecretNotDerived => { + write!(f, "exporter master secret not yet derived") + } + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for CryptoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoSupportedKeyExchangeGroups => write!(f, "no supported key exchange groups"), + Self::NoDtls12KeyExchangeGroupsConfigured => { + write!(f, "no DTLS 1.2 key exchange groups configured") + } + Self::Epoch0SequenceNumberExhausted => { + write!(f, "epoch 0 sequence number exhausted") + } + Self::SendSequenceNumberExhausted { epoch } => { + write!(f, "send sequence number exhausted for epoch {epoch}") + } + Self::SendKeysNotAvailable { epoch } => { + write!(f, "send keys not available for epoch {epoch}") + } + Self::RecvKeysNotAvailable { epoch } => { + write!(f, "recv keys not available for epoch {epoch}") + } + Self::KeyExchangeGroupNotFound(group) => { + write!(f, "key exchange group not found: {group:?}") + } + Self::KeyExchangeNotInitialized => write!(f, "key exchange not initialized"), + Self::UnsupportedKeyExchangeGroup(group) => { + write!(f, "unsupported key exchange group: {group:?}") + } + Self::UnsupportedEcdheNamedGroup(group) => { + write!(f, "unsupported ECDHE named group: {group:?}") + } + Self::UnsupportedCipherSuite(suite) => { + write!(f, "unsupported cipher suite: {suite:?}") + } + Self::UnsupportedHmacHash(hash) => { + write!(f, "unsupported HMAC hash algorithm: {hash:?}") + } + Self::UnsupportedSignatureAlgorithm(sig) => { + write!(f, "unsupported signature algorithm: {sig:?}") + } + Self::SignatureAlgorithmNotOfferedByClient => { + write!(f, "signature algorithm not offered by client") + } + Self::SignatureAlgorithmMismatch { expected, actual } => { + write!( + f, + "signature algorithm mismatch: {actual:?} != {expected:?}" + ) + } + Self::UnsupportedSignaturePair { signature, hash } => { + write!(f, "unsupported signature algorithm: {signature:?}/{hash:?}") + } + Self::UnsupportedSignatureVerification { + signature, + hash, + group, + } => write!( + f, + "unsupported signature verification: {signature:?} + {hash:?} + {group:?}" + ), + Self::UnsupportedPublicKeyAlgorithm => write!(f, "unsupported public key algorithm"), + Self::UnsupportedEcCurve(curve) => write!(f, "unsupported EC curve: {curve}"), + Self::MissingEcCurveParameter => { + write!(f, "missing EC curve parameter in certificate") + } + Self::InvalidEcCurveParameter => { + write!(f, "invalid EC curve parameter in certificate") + } + Self::InvalidSubjectPublicKey => { + write!(f, "invalid EC subject_public_key bitstring") + } + Self::InvalidSignatureFormat => write!(f, "invalid signature format"), + Self::InvalidPublicKey(group) => write!(f, "invalid {group:?} public key"), + Self::InvalidPrivateKey => { + write!(f, "failed to parse private key in any supported format") + } + Self::SigningKeyHashMismatch { + key_hash, + requested, + } => write!( + f, + "signing key is locked to {key_hash:?} but {requested:?} was requested" + ), + Self::SigningKeyUnsupportedHash { group, hash } => { + write!(f, "{group:?} key does not support hash algorithm {hash:?}") + } + Self::InvalidAesGcmKeySize { actual } => { + write!(f, "invalid key size for AES-GCM: {actual}") + } + Self::InvalidChacha20Poly1305KeySize { actual } => { + write!(f, "invalid key size for CHACHA20-POLY1305: {actual}") + } + Self::InvalidAes128Ccm8KeySize { actual } => { + write!(f, "invalid key size for AES-128-CCM-8: {actual}") + } + Self::InvalidNonce => write!(f, "invalid nonce"), + Self::InvalidNonceLength { expected, actual } => { + write!(f, "invalid nonce length: expected {expected}, got {actual}") + } + Self::CiphertextTooShort { minimum, actual } => { + write!(f, "ciphertext too short: got {actual}, minimum {minimum}") + } + Self::HkdfOutputTooLong => write!(f, "HKDF output too long"), + Self::HkdfLabelTooLong => write!(f, "label too long for HKDF-Expand-Label"), + Self::HkdfContextTooLong => write!(f, "context too long for HKDF-Expand-Label"), + Self::HkdfOutputLengthTooLarge => { + write!(f, "output length too large for HKDF-Expand-Label") + } + Self::InvalidVerifyDataLength => write!(f, "invalid verify data length"), + Self::VerifyDataTooLong => write!(f, "verify data too long"), + Self::MasterSecretTooLong => write!(f, "master secret too long"), + Self::KeyingMaterialTooLong => write!(f, "keying material too long"), + Self::PreMasterSecretNotAvailable => write!(f, "pre-master secret not available"), + Self::MasterSecretNotAvailable => write!(f, "master secret not available"), + Self::ClientRandomNotAvailable => write!(f, "client random not available"), + Self::ServerRandomNotAvailable => write!(f, "server random not available"), + Self::ClientCipherNotInitialized => write!(f, "client cipher not initialized"), + Self::ServerCipherNotInitialized => write!(f, "server cipher not initialized"), + Self::WriteIvNotAvailable { is_client } => { + let side = if *is_client { "client" } else { "server" }; + write!(f, "{side} write IV not available") + } + Self::UnsupportedDtls12RecordIvLen { len, suite } => { + write!(f, "unsupported DTLS 1.2 record_iv_len={len} for {suite:?}") + } + Self::NoPrivateKeyConfigured => write!(f, "no private key configured"), + Self::PskNotSet => write!(f, "PSK not set"), + Self::ExporterMasterSecretNotDerived => { + write!(f, "exporter master secret not yet derived") + } + Self::OperationFailed(op) => write!(f, "{op} failed"), + Self::ProviderFailure { operation, reason } => { + write!(f, "{operation} failed: {reason}") + } + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for CryptoOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let text = match self { + Self::CreateCipher => "cipher creation", + Self::Encrypt => "encryption", + Self::Decrypt => "decryption", + Self::Sign => "signing", + Self::VerifySignature => "signature verification", + Self::LoadPrivateKey => "private key loading", + Self::StartKeyExchange => "key exchange start", + Self::CompleteKeyExchange => "key exchange completion", + Self::GenerateEphemeralKey => "ephemeral key generation", + Self::ComputePublicKey => "public key computation", + Self::FillRandom => "random generation", + Self::ComputeHmac => "HMAC computation", + Self::Prf => "PRF", + Self::HkdfExtract => "HKDF extract", + Self::HkdfExpand => "HKDF expand", + Self::HkdfExpandLabel => "HKDF expand label", + Self::DeriveEarlySecret => "early secret derivation", + Self::DeriveDerivedSecret => "derived secret derivation", + Self::DeriveHandshakeSecret => "handshake secret derivation", + Self::DeriveTrafficSecret => "traffic secret derivation", + Self::DeriveMasterSecret => "master secret derivation", + Self::DeriveExporterMasterSecret => "exporter master secret derivation", + Self::DeriveNextTrafficSecret => "next traffic secret derivation", + Self::DeriveKey => "key derivation", + Self::DeriveIv => "IV derivation", + Self::DeriveSequenceNumberKey => "sequence number key derivation", + Self::DeriveFinishedKey => "Finished key derivation", + Self::ComputeVerifyData => "verify data computation", + Self::VerifyData => "verify data verification", + Self::ComputePskPreMasterSecret => "PSK pre-master secret computation", + Self::ComputeCookie => "cookie computation", + Self::ExtractSrtpKeyingMaterial => "SRTP keying material extraction", + Self::EncodeKey => "key encoding", + Self::DecodeKey => "key decoding", + }; + write!(f, "{text}") + } +} + +impl fmt::Display for CertificateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoServerCertificateReceived => write!(f, "no server certificate received"), + Self::NoClientCertificateForVerification => { + write!(f, "no client certificate for verification") + } + Self::NoServerCertificateForVerification => { + write!(f, "no server certificate for verification") + } + Self::ServerCertificateContextMustBeEmpty => { + write!(f, "server certificate context must be empty") + } + Self::ClientCertificateContextMustBeEmpty => { + write!(f, "client certificate context must be empty") + } + Self::UnsupportedHashAlgorithm(hash) => { + write!(f, "unsupported hash algorithm: {hash:?}") + } + Self::ParseFailed => write!(f, "failed to parse certificate"), + Self::MissingEcCurveParameter => { + write!(f, "missing EC curve parameter in certificate") + } + Self::InvalidEcCurveParameter => { + write!(f, "invalid EC curve parameter in certificate") + } + Self::UnsupportedEcCurve(curve) => write!(f, "unsupported EC curve: {curve}"), + Self::InvalidSubjectPublicKey => { + write!(f, "invalid EC subject_public_key bitstring") + } + Self::PrivateKey(err) => write!(f, "private key error: {err}"), + Self::Verification(err) => write!(f, "verification failed: {err}"), + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for SecurityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedHelloVerifyRequestVersion(version) => { + write!( + f, + "unsupported DTLS version in HelloVerifyRequest: {version:?}" + ) + } + Self::UnsupportedServerVersion(version) => { + write!(f, "unsupported DTLS version from server: {version:?}") + } + Self::UnsupportedClientVersion(version) => { + write!(f, "unsupported DTLS version from client: {version:?}") + } + Self::UnsupportedServerCompression(compression) => { + write!( + f, + "unsupported compression method from server: {compression:?}" + ) + } + Self::UnsupportedClientCompression => { + write!(f, "client did not offer null compression") + } + Self::UnsupportedKeyExchangeAlgorithm => { + write!(f, "unsupported key exchange algorithm") + } + Self::ServerSelectedUnknownCipherSuite => { + write!(f, "server selected unknown cipher suite") + } + Self::ServerSelectedIncompatibleCipherSuite(suite) => { + write!(f, "server selected incompatible cipher suite: {suite:?}") + } + Self::ServerSelectedDisallowedCipherSuite(suite) => { + write!(f, "server selected disallowed cipher suite: {suite:?}") + } + Self::ServerSelectedDisallowedDtls13CipherSuite(suite) => { + write!(f, "server selected disallowed cipher suite: {suite:?}") + } + Self::ExtendedMasterSecretNotNegotiated => { + write!(f, "extended master secret not negotiated") + } + Self::NoMutuallyAcceptableCipherSuite => { + write!(f, "no mutually acceptable cipher suite") + } + Self::ClientHelloLegacyVersionNotDtls12 => { + write!(f, "ClientHello legacy_version must be DTLS 1.2") + } + Self::ServerHelloLegacyVersionNotDtls12 => { + write!(f, "ServerHello legacy_version must be DTLS 1.2") + } + Self::ClientHelloMissingDtls13SupportedVersions => { + write!(f, "ClientHello missing DTLS 1.3 supported_versions") + } + Self::ClientHelloMustOfferNullCompression => { + write!(f, "ClientHello must offer null compression") + } + Self::InvalidCookieInClientHello => write!(f, "invalid cookie in ClientHello"), + Self::CannotSendSecondHelloRetryRequest => { + write!(f, "cannot send second HelloRetryRequest") + } + Self::NoCommonCipherSuite => write!(f, "no common cipher suite found"), + Self::NoCommonKeyExchangeGroup => write!(f, "no common key exchange group"), + Self::HrrSelectedDisallowedCipherSuite => { + write!(f, "HRR selected disallowed cipher suite") + } + Self::HrrDidNotSelectDtls13 => write!(f, "HRR did not select DTLS 1.3"), + Self::ServerHelloCompressionMustBeNull => { + write!(f, "ServerHello compression must be null") + } + Self::ServerDidNotNegotiateDtls13 => { + write!(f, "server did not negotiate DTLS 1.3") + } + Self::ServerMissingKeyShare => write!(f, "server missing key_share"), + Self::ServerKeyShareGroupMismatch { selected, actual } => { + write!( + f, + "server key_share group mismatch: selected {selected:?}, actual {actual:?}" + ) + } + Self::SignatureTooLarge => write!(f, "signature too large"), + Self::SignatureSchemeNotOffered(scheme) => { + write!(f, "signature scheme {scheme:?} was not offered") + } + Self::SignatureAlgorithmMismatch { expected, actual } => { + write!( + f, + "signature algorithm mismatch: expected {expected:?}, got {actual:?}" + ) + } + Self::UnsupportedSignatureScheme(scheme) => { + write!(f, "unsupported signature scheme: {scheme:?}") + } + Self::SignatureSchemeCertificateCurveMismatch { + scheme, + expected, + actual, + } => write!( + f, + "signature scheme {scheme:?} requires {expected:?} but certificate uses {actual:?}" + ), + Self::ServerFinishedVerificationFailed => { + write!(f, "server Finished verification failed") + } + Self::ClientFinishedVerificationFailed => { + write!(f, "client Finished verification failed") + } + Self::FatalAlert { level, description } => { + write!( + f, + "received fatal alert: level={level}, description={description}" + ) + } + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for PskError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoPskResolverConfigured => write!(f, "no PSK resolver configured"), + Self::NoPskIdentityConfigured => write!(f, "no PSK identity configured"), + Self::ResolverReturnedNoKey => write!(f, "PSK resolver returned no key"), + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for TimeoutError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::HybridClientHello => write!(f, "hybrid ClientHello"), + Self::Connect => write!(f, "connect"), + Self::Handshake => write!(f, "handshake"), + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoCryptoProvider => write!( + f, + concat!( + "no crypto provider available; set one explicitly, install a default, ", + "or enable a crypto feature" + ) + ), + Self::MtuTooSmall { mtu, minimum } => { + write!(f, "MTU {mtu} is too small (minimum {minimum})") + } + Self::AeadEncryptionLimitTooSmall => { + write!(f, "aead_encryption_limit must be at least 1") + } + Self::NoCipherSuitesAfterFiltering => write!( + f, + concat!( + "no cipher suites remain after filtering; at least one DTLS 1.2 or ", + "DTLS 1.3 cipher suite must be available" + ) + ), + Self::PskConfiguredWithoutPskCipherSuite => write!( + f, + "PSK is configured but no PSK cipher suite remains after filtering DTLS 1.2 suites" + ), + Self::NoDtls12KeyExchangeGroupsAfterFiltering => write!( + f, + concat!( + "DTLS 1.2 cipher suites are enabled but no compatible key exchange ", + "groups remain after filtering" + ) + ), + Self::NoDtls13KeyExchangeGroupsAfterFiltering => write!( + f, + "DTLS 1.3 cipher suites are enabled but no key exchange groups remain after filtering" + ), + Self::CryptoProvider(err) => write!(f, "crypto provider validation failed: {err}"), + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl fmt::Display for CryptoProviderValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoCipherSuites => { + write!(f, "CryptoProvider has no cipher suites supported by dimpl") + } + Self::EcdhCipherSuitesWithoutKeyExchangeGroups => write!( + f, + "CryptoProvider has ECDH cipher suites but no supported key exchange groups" + ), + Self::NoDtls13CipherSuites => { + write!(f, "CryptoProvider has no DTLS 1.3 cipher suites") + } + Self::MissingHashTestVector(hash) => { + write!(f, "no expected hash data for hash algorithm: {hash:?}") + } + Self::HashProviderIncorrect(hash) => { + write!(f, "hash provider {hash:?} produced incorrect result") + } + Self::PrfFailed { hash, source } => write!(f, "PRF failed for {hash:?}: {source}"), + Self::PrfWrongLength { + hash, + expected, + actual, + } => write!( + f, + "PRF {hash:?} returned wrong length: expected {expected}, got {actual}" + ), + Self::MissingPrfTestVector(hash) => { + write!(f, "no expected PRF data for hash algorithm: {hash:?}") + } + Self::PrfIncorrect(hash) => write!(f, "PRF {hash:?} produced incorrect result"), + Self::NoSignatureValidationVector { hash, signature } => { + write!(f, "no validation test vectors for {hash:?} + {signature:?}") + } + Self::SignatureVerificationFailed { + hash, + signature, + source, + } => write!( + f, + "signature verification failed for {hash:?} + {signature:?}: {source}" + ), + Self::HkdfFailed { suite, source } => { + write!(f, "HKDF failed for DTLS 1.3 suite {suite:?}: {source}") + } + Self::HkdfEmptyOutput(suite) => { + write!(f, "HKDF returned empty output for {suite:?}") + } + Self::NoAeadTestVector(suite) => { + write!(f, "no AEAD test vector for DTLS 1.3 suite {suite:?}") + } + Self::AeadCreateFailed { suite, source } => { + write!(f, "failed to create cipher for {suite:?}: {source}") + } + Self::AeadEncryptFailed { suite, source } => { + write!(f, "AEAD encrypt failed for {suite:?}: {source}") + } + Self::AeadEncryptWrongOutput(suite) => { + write!(f, "AEAD encrypt produced wrong output for {suite:?}") + } + Self::AeadDecryptFailed { suite, source } => { + write!(f, "AEAD decrypt failed for {suite:?}: {source}") + } + Self::AeadDecryptWrongOutput(suite) => { + write!(f, "AEAD decrypt produced wrong output for {suite:?}") + } + Self::NoRecordNumberEncryptionTestVector(suite) => { + write!(f, "no encrypt_sn test vector for DTLS 1.3 suite {suite:?}") + } + Self::RecordNumberEncryptionWrongMask(suite) => { + write!(f, "encrypt_sn produced wrong mask for {suite:?}") + } + Self::KeyExchangeStartFailed { group, source } => { + write!(f, "key exchange start failed for {group:?}: {source}") + } + Self::KeyExchangeCompleteFailed { group, source } => { + write!(f, "key exchange complete failed for {group:?}: {source}") + } + Self::KeyExchangeMismatchedSharedSecret(group) => { + write!(f, "key exchange produced different secrets for {group:?}") + } + Self::HmacFailed(source) => write!(f, "HMAC provider failed: {source}"), + Self::HmacWrongLength { expected, actual } => { + write!( + f, + "HMAC provider returned wrong length: expected {expected} bytes, got {actual}" + ) + } + Self::HmacIncorrect => { + write!(f, "HMAC provider produced incorrect result for HMAC-SHA256") } + Self::Other(msg) => write!(f, "{msg}"), } } } diff --git a/src/lib.rs b/src/lib.rs index 1013661f..e7a2caad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -227,8 +227,11 @@ mod window; mod util; mod error; -pub use error::Error; pub(crate) use error::InternalError; +pub use error::{ + CertificateError, ConfigError, CryptoError, CryptoOperation, CryptoProviderValidationError, + Error, InvalidStateError, PskError, SecurityError, TimeoutError, UnexpectedMessageError, +}; mod config; pub use config::{Config, ConfigBuilder, Psk, PskResolver}; @@ -633,7 +636,7 @@ impl Dtls { // while inner is None would leave us unable to poll/timeout. if matches!(version, auto::DetectedVersion::Unknown) { return Err(Error::UnexpectedMessage( - "Unrecognized response from server".to_string(), + crate::UnexpectedMessageError::UnrecognizedAutoServerResponse, )); } diff --git a/tests/auto/handshake.rs b/tests/auto/handshake.rs index 4f32c26f..ea421e00 100644 --- a/tests/auto/handshake.rs +++ b/tests/auto/handshake.rs @@ -591,10 +591,8 @@ fn auto_client_rejects_unknown_version_response() { "Unknown version response should be an error" ); match result.unwrap_err() { - Error::UnexpectedMessage(msg) => assert!( - msg.contains("Unrecognized"), - "error should mention unrecognized: {msg}" - ), + Error::UnexpectedMessage(dimpl::UnexpectedMessageError::UnrecognizedAutoServerResponse) => { + } other => panic!("expected UnexpectedMessage, got: {other:?}"), } } From ca1fa5280157f8d5b582fe5ed945ed5fb4f5915b Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Fri, 29 May 2026 12:58:44 +0300 Subject: [PATCH 03/10] error: document structured error payloads --- src/error.rs | 293 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 282 insertions(+), 11 deletions(-) diff --git a/src/error.rs b/src/error.rs index 50bfd30b..de1272cb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -61,360 +61,631 @@ pub enum Error { Dtls12Fallback, } +/// Fine-grained reason for an [`Error::UnexpectedMessage`]. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum UnexpectedMessageError { + /// Auto-detection received a server response that was neither DTLS 1.2 nor DTLS 1.3. UnrecognizedAutoServerResponse, + /// A DTLS 1.2 `ServerKeyExchange` omitted its required signature. ServerKeyExchangeWithoutSignature, + /// A PSK `ServerKeyExchange` was received while processing an ECDHE suite. PskServerKeyExchangeInEcdhePath, + /// An ECDHE `ServerKeyExchange` was received while processing a PSK suite. EcdheServerKeyExchangeInPskPath, + /// A PSK `ClientKeyExchange` was received while processing an ECDHE suite. PskClientKeyExchangeInEcdhePath, + /// An ECDHE `ClientKeyExchange` was received while processing a PSK suite. EcdheClientKeyExchangeInPskPath, + /// A DTLS 1.3 `CertificateRequest` context was shorter than declared. CertificateRequestContextTruncated, + /// A DTLS 1.3 `CertificateRequest` extension block was shorter than declared. CertificateRequestExtensionsTruncated, + /// A free-form unexpected-message reason not represented by a dedicated variant. Other(String), } +/// Fine-grained reason for an [`Error::InvalidState`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum InvalidStateError { + /// No cipher suite has been selected for the connection. NoCipherSuiteSelected, + /// A cipher suite was required but not available. NoCipherSuite, + /// The client random was required but not available. NoClientRandom, + /// The server random was required but not available. NoServerRandom, + /// The handshake key schedule was used before a shared secret existed. NoSharedSecretForHandshakeKeyDerivation, + /// The server handshake traffic secret was required but not available. NoServerHandshakeTrafficSecret, + /// The server handshake traffic secret was required to verify or create `Finished`. NoServerHandshakeTrafficSecretForFinished, + /// The client handshake traffic secret was required but not available. NoClientHandshakeTrafficSecret, + /// The client handshake traffic secret was required to verify or create `Finished`. NoClientHandshakeTrafficSecretForFinished, + /// Application traffic keys were requested before the handshake secret existed. NoHandshakeSecretForApplicationKeyDerivation, + /// A key exchange was required but no exchange was active. NoActiveKeyExchange, + /// A DTLS 1.3 key update was requested before current send keys existed. NoCurrentAppSendKeysForKeyUpdate, + /// A DTLS 1.3 peer key update was processed before current receive keys existed. NoCurrentAppRecvKeysForKeyUpdate, + /// Exported keying material was requested before the exporter secret was derived. ExporterMasterSecretNotDerived, + /// A static invalid-state reason not represented by a dedicated variant. Other(&'static str), } +/// Fine-grained reason for an [`Error::CryptoError`]. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum CryptoError { + /// No supported key exchange group is available. NoSupportedKeyExchangeGroups, + /// DTLS 1.2 needs a key exchange group but none is configured. NoDtls12KeyExchangeGroupsConfigured, + /// The epoch 0 sequence number space has been exhausted. Epoch0SequenceNumberExhausted, + /// The send sequence number space for an epoch has been exhausted. SendSequenceNumberExhausted { + /// The epoch whose send sequence number was exhausted. epoch: u16, }, + /// Send keys are not available for an epoch. SendKeysNotAvailable { + /// The epoch whose send keys are unavailable. epoch: u16, }, + /// Receive keys are not available for an epoch. RecvKeysNotAvailable { + /// The epoch whose receive keys are unavailable. epoch: u16, }, + /// No provider key exchange group matches the negotiated group. KeyExchangeGroupNotFound(NamedGroup), + /// A key exchange operation was requested before initialization. KeyExchangeNotInitialized, + /// The requested key exchange group is unsupported. UnsupportedKeyExchangeGroup(NamedGroup), + /// The requested ECDHE group is unsupported. UnsupportedEcdheNamedGroup(NamedGroup), + /// The requested DTLS 1.2 cipher suite is unsupported. UnsupportedCipherSuite(Dtls12CipherSuite), + /// The requested HMAC hash algorithm is unsupported. UnsupportedHmacHash(HashAlgorithm), + /// The requested signature algorithm is unsupported. UnsupportedSignatureAlgorithm(SignatureAlgorithm), + /// No locally supported signature algorithm was offered by the peer. SignatureAlgorithmNotOfferedByClient, + /// The signature algorithm did not match the expected algorithm. SignatureAlgorithmMismatch { + /// The signature algorithm expected for this operation. expected: SignatureAlgorithm, + /// The signature algorithm actually present. actual: SignatureAlgorithm, }, + /// The signature and hash algorithm pair is unsupported. UnsupportedSignaturePair { + /// The requested signature algorithm. signature: SignatureAlgorithm, + /// The requested hash algorithm. hash: HashAlgorithm, }, + /// The signature, hash, and key group combination is unsupported for verification. UnsupportedSignatureVerification { + /// The requested signature algorithm. signature: SignatureAlgorithm, + /// The requested hash algorithm. hash: HashAlgorithm, + /// The public key group used for verification. group: NamedGroup, }, + /// The public key algorithm is unsupported. UnsupportedPublicKeyAlgorithm, + /// The certificate or key references an unsupported EC curve. UnsupportedEcCurve(String), + /// A certificate omitted its required EC curve parameter. MissingEcCurveParameter, + /// A certificate had an invalid EC curve parameter. InvalidEcCurveParameter, + /// A certificate had an invalid subject public key. InvalidSubjectPublicKey, + /// A signature was not encoded in the expected format. InvalidSignatureFormat, + /// A public key could not be parsed for the given group. InvalidPublicKey(NamedGroup), + /// A private key could not be parsed in any supported format. InvalidPrivateKey, + /// A signing key was used with a hash other than the one it is bound to. SigningKeyHashMismatch { + /// The hash algorithm bound to the signing key. key_hash: HashAlgorithm, + /// The hash algorithm requested by the caller. requested: HashAlgorithm, }, + /// A signing key group does not support the requested hash algorithm. SigningKeyUnsupportedHash { + /// The key group used for signing. group: NamedGroup, + /// The requested hash algorithm. hash: HashAlgorithm, }, + /// An AES-GCM key had the wrong length. InvalidAesGcmKeySize { + /// The actual key length in bytes. actual: usize, }, + /// A ChaCha20-Poly1305 key had the wrong length. InvalidChacha20Poly1305KeySize { + /// The actual key length in bytes. actual: usize, }, + /// An AES-128-CCM-8 key had the wrong length. InvalidAes128Ccm8KeySize { + /// The actual key length in bytes. actual: usize, }, + /// A nonce was invalid for the selected cipher. InvalidNonce, + /// A nonce had the wrong length. InvalidNonceLength { + /// The expected nonce length in bytes. expected: usize, + /// The actual nonce length in bytes. actual: usize, }, + /// A ciphertext was shorter than the selected AEAD permits. CiphertextTooShort { + /// The minimum accepted ciphertext length in bytes. minimum: usize, + /// The actual ciphertext length in bytes. actual: usize, }, + /// The requested HKDF output length is too large. HkdfOutputTooLong, + /// The HKDF label is too long to encode. HkdfLabelTooLong, + /// The HKDF context is too long to encode. HkdfContextTooLong, + /// The HKDF-Expand-Label output length is too large to encode. HkdfOutputLengthTooLarge, + /// The `Finished` verify-data length was invalid. InvalidVerifyDataLength, + /// The `Finished` verify-data is too long to encode. VerifyDataTooLong, + /// The TLS 1.2 master secret is too long to encode. MasterSecretTooLong, + /// The requested exported keying material is too long. KeyingMaterialTooLong, + /// The TLS 1.2 pre-master secret is not available. PreMasterSecretNotAvailable, + /// The TLS 1.2 master secret is not available. MasterSecretNotAvailable, + /// The client random is not available. ClientRandomNotAvailable, + /// The server random is not available. ServerRandomNotAvailable, + /// The client cipher has not been initialized. ClientCipherNotInitialized, + /// The server cipher has not been initialized. ServerCipherNotInitialized, + /// A write IV is not available for the requested side. WriteIvNotAvailable { + /// Whether the missing write IV is for the client side. is_client: bool, }, + /// The DTLS 1.2 record IV length is unsupported for the selected suite. UnsupportedDtls12RecordIvLen { + /// The unsupported record IV length. len: usize, + /// The selected DTLS 1.2 cipher suite. suite: Dtls12CipherSuite, }, + /// No private key is configured. NoPrivateKeyConfigured, + /// A PSK operation was requested before a PSK was set. PskNotSet, + /// The exporter master secret is not available. ExporterMasterSecretNotDerived, + /// A provider operation failed without a more specific reason. OperationFailed(CryptoOperation), + /// A provider operation failed with a backend-specific reason. ProviderFailure { + /// The provider operation that failed. operation: CryptoOperation, + /// The backend-specific reason. reason: String, }, + /// A free-form crypto reason not represented by a dedicated variant. Other(String), } +/// A cryptographic operation that can fail. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum CryptoOperation { + /// Create an AEAD cipher instance. CreateCipher, + /// Encrypt plaintext. Encrypt, + /// Decrypt ciphertext. Decrypt, + /// Sign a transcript or payload. Sign, + /// Verify a signature. VerifySignature, + /// Load a private key. LoadPrivateKey, + /// Start a key exchange. StartKeyExchange, + /// Complete a key exchange. CompleteKeyExchange, + /// Generate an ephemeral key. GenerateEphemeralKey, + /// Compute a public key. ComputePublicKey, + /// Fill a buffer with random bytes. FillRandom, + /// Compute an HMAC. ComputeHmac, + /// Run the TLS 1.2 PRF. Prf, + /// Run HKDF-Extract. HkdfExtract, + /// Run HKDF-Expand. HkdfExpand, + /// Run TLS HKDF-Expand-Label. HkdfExpandLabel, + /// Derive a DTLS 1.3 early secret. DeriveEarlySecret, + /// Derive a DTLS 1.3 derived secret. DeriveDerivedSecret, + /// Derive a DTLS 1.3 handshake secret. DeriveHandshakeSecret, + /// Derive a DTLS 1.3 traffic secret. DeriveTrafficSecret, + /// Derive a DTLS 1.3 master secret. DeriveMasterSecret, + /// Derive a DTLS 1.3 exporter master secret. DeriveExporterMasterSecret, + /// Derive a DTLS 1.3 next-generation traffic secret. DeriveNextTrafficSecret, + /// Derive an AEAD key. DeriveKey, + /// Derive an AEAD IV. DeriveIv, + /// Derive a record sequence-number encryption key. DeriveSequenceNumberKey, + /// Derive a Finished-message key. DeriveFinishedKey, + /// Compute Finished verify data. ComputeVerifyData, + /// Verify Finished verify data. VerifyData, + /// Compute a PSK pre-master secret. ComputePskPreMasterSecret, + /// Compute a DTLS cookie. ComputeCookie, + /// Extract SRTP keying material. ExtractSrtpKeyingMaterial, + /// Encode a key. EncodeKey, + /// Decode a key. DecodeKey, } +/// Fine-grained reason for an [`Error::CertificateError`]. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum CertificateError { + /// The peer did not send a server certificate. NoServerCertificateReceived, + /// Client certificate verification was requested with no client certificate. NoClientCertificateForVerification, + /// Server certificate verification was requested with no server certificate. NoServerCertificateForVerification, + /// A DTLS 1.3 server certificate carried a non-empty context. ServerCertificateContextMustBeEmpty, + /// A DTLS 1.3 client certificate carried a non-empty context. ClientCertificateContextMustBeEmpty, + /// The certificate operation needs an unsupported hash algorithm. UnsupportedHashAlgorithm(HashAlgorithm), + /// Certificate parsing failed. ParseFailed, + /// A certificate omitted its EC curve parameter. MissingEcCurveParameter, + /// A certificate had an invalid EC curve parameter. InvalidEcCurveParameter, + /// A certificate references an unsupported EC curve. UnsupportedEcCurve(String), + /// A certificate had an invalid subject public key. InvalidSubjectPublicKey, + /// Private-key handling failed during certificate processing. PrivateKey(CryptoError), + /// Certificate verification failed. Verification(CryptoError), + /// A free-form certificate reason not represented by a dedicated variant. Other(String), } +/// Fine-grained reason for an [`Error::SecurityError`]. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum SecurityError { + /// `HelloVerifyRequest` used an unsupported protocol version. UnsupportedHelloVerifyRequestVersion(ProtocolVersion), + /// A server selected an unsupported protocol version. UnsupportedServerVersion(ProtocolVersion), + /// A client offered an unsupported protocol version. UnsupportedClientVersion(ProtocolVersion), + /// A server selected an unsupported DTLS 1.2 compression method. UnsupportedServerCompression(CompressionMethod), + /// A client did not offer null compression. UnsupportedClientCompression, + /// The selected key exchange algorithm is unsupported. UnsupportedKeyExchangeAlgorithm, + /// The server selected a cipher suite that was not offered or recognized. ServerSelectedUnknownCipherSuite, + /// The server selected a DTLS 1.2 cipher suite incompatible with local mode. ServerSelectedIncompatibleCipherSuite(Dtls12CipherSuite), + /// The server selected a DTLS 1.2 cipher suite disallowed by configuration. ServerSelectedDisallowedCipherSuite(Dtls12CipherSuite), + /// The server selected a DTLS 1.3 cipher suite disallowed by configuration. ServerSelectedDisallowedDtls13CipherSuite(Dtls13CipherSuite), + /// DTLS 1.2 extended master secret was required but not negotiated. ExtendedMasterSecretNotNegotiated, + /// No mutually acceptable cipher suite was found. NoMutuallyAcceptableCipherSuite, + /// A DTLS 1.3 ClientHello did not use the required DTLS 1.2 legacy version. ClientHelloLegacyVersionNotDtls12, + /// A DTLS 1.3 ServerHello did not use the required DTLS 1.2 legacy version. ServerHelloLegacyVersionNotDtls12, + /// A DTLS 1.3 ClientHello did not contain a recognized DTLS 1.3 version. ClientHelloMissingDtls13SupportedVersions, + /// A ClientHello did not offer null compression. ClientHelloMustOfferNullCompression, + /// The ClientHello cookie did not match the expected cookie. InvalidCookieInClientHello, + /// The server attempted to send a second HelloRetryRequest. CannotSendSecondHelloRetryRequest, + /// No common DTLS 1.3 cipher suite was found. NoCommonCipherSuite, + /// No common DTLS 1.3 key exchange group was found. NoCommonKeyExchangeGroup, + /// A HelloRetryRequest selected a disallowed cipher suite. HrrSelectedDisallowedCipherSuite, + /// A HelloRetryRequest did not select DTLS 1.3. HrrDidNotSelectDtls13, + /// A ServerHello selected a non-null compression method. ServerHelloCompressionMustBeNull, + /// A server did not negotiate DTLS 1.3. ServerDidNotNegotiateDtls13, + /// A DTLS 1.3 server did not send a key share. ServerMissingKeyShare, + /// The server key share group did not match the negotiated group. ServerKeyShareGroupMismatch { + /// The negotiated key exchange group. selected: NamedGroup, + /// The key exchange group in the server key share. actual: NamedGroup, }, + /// A signature was too large to encode. SignatureTooLarge, + /// A signature scheme was used even though the peer did not offer it. SignatureSchemeNotOffered(SignatureScheme), + /// A signature algorithm did not match the expected algorithm. SignatureAlgorithmMismatch { + /// The expected signature algorithm. expected: SignatureAlgorithm, + /// The actual signature algorithm. actual: SignatureAlgorithm, }, + /// The signature scheme is unsupported. UnsupportedSignatureScheme(SignatureScheme), + /// The signature scheme and certificate curve are incompatible. SignatureSchemeCertificateCurveMismatch { + /// The signature scheme used for the operation. scheme: SignatureScheme, + /// The certificate curve required by the signature scheme. expected: NamedGroup, + /// The actual certificate curve. actual: NamedGroup, }, + /// Server Finished verification failed. ServerFinishedVerificationFailed, + /// Client Finished verification failed. ClientFinishedVerificationFailed, + /// A fatal DTLS alert was received. FatalAlert { + /// The DTLS alert level. level: u8, + /// The DTLS alert description. description: u8, }, + /// A free-form security reason not represented by a dedicated variant. Other(String), } +/// Fine-grained reason for an [`Error::PskError`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum PskError { + /// No PSK resolver is configured. NoPskResolverConfigured, + /// No PSK identity is configured. NoPskIdentityConfigured, + /// The configured PSK resolver did not return a key. ResolverReturnedNoKey, + /// A static PSK reason not represented by a dedicated variant. Other(&'static str), } +/// Fine-grained reason for an [`Error::Timeout`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum TimeoutError { + /// Timeout while waiting for an auto client hybrid ClientHello to resolve. HybridClientHello, + /// Timeout while connecting. Connect, + /// Timeout while handshaking. Handshake, + /// A static timeout reason not represented by a dedicated variant. Other(&'static str), } +/// Fine-grained reason for an [`Error::ConfigError`]. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum ConfigError { + /// No crypto provider is available. NoCryptoProvider, - MtuTooSmall { mtu: usize, minimum: usize }, + /// The configured MTU is smaller than dimpl permits. + MtuTooSmall { + /// The configured MTU. + mtu: usize, + /// The minimum accepted MTU. + minimum: usize, + }, + /// The configured AEAD encryption limit is too small. AeadEncryptionLimitTooSmall, + /// Cipher-suite filtering removed every available suite. NoCipherSuitesAfterFiltering, + /// A PSK resolver is configured but no PSK cipher suite remains enabled. PskConfiguredWithoutPskCipherSuite, + /// DTLS 1.2 suites are enabled but no compatible key exchange group remains enabled. NoDtls12KeyExchangeGroupsAfterFiltering, + /// DTLS 1.3 suites are enabled but no key exchange group remains enabled. NoDtls13KeyExchangeGroupsAfterFiltering, + /// Crypto provider validation failed. CryptoProvider(CryptoProviderValidationError), + /// A free-form configuration reason not represented by a dedicated variant. Other(String), } +/// Fine-grained reason for crypto provider validation failure. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] -#[allow(missing_docs)] pub enum CryptoProviderValidationError { + /// The provider has no cipher suites supported by dimpl. NoCipherSuites, + /// The provider has ECDH cipher suites but no key exchange groups. EcdhCipherSuitesWithoutKeyExchangeGroups, + /// The provider has no DTLS 1.3 cipher suites. NoDtls13CipherSuites, + /// No hash test vector exists for the hash algorithm. MissingHashTestVector(HashAlgorithm), + /// The provider hash implementation returned an unexpected value. HashProviderIncorrect(HashAlgorithm), + /// The provider PRF operation failed. PrfFailed { + /// The hash algorithm used by the PRF. hash: HashAlgorithm, + /// The underlying crypto failure. source: CryptoError, }, + /// The provider PRF returned the wrong output length. PrfWrongLength { + /// The hash algorithm used by the PRF. hash: HashAlgorithm, + /// The expected output length in bytes. expected: usize, + /// The actual output length in bytes. actual: usize, }, + /// No PRF test vector exists for the hash algorithm. MissingPrfTestVector(HashAlgorithm), + /// The provider PRF returned incorrect output. PrfIncorrect(HashAlgorithm), + /// No signature-validation vector exists for the algorithm pair. NoSignatureValidationVector { + /// The hash algorithm used by the vector. hash: HashAlgorithm, + /// The signature algorithm used by the vector. signature: SignatureAlgorithm, }, + /// Provider signature verification failed. SignatureVerificationFailed { + /// The hash algorithm used for verification. hash: HashAlgorithm, + /// The signature algorithm used for verification. signature: SignatureAlgorithm, + /// The underlying crypto failure. source: CryptoError, }, + /// Provider HKDF validation failed. HkdfFailed { + /// The DTLS 1.3 cipher suite under validation. suite: Dtls13CipherSuite, + /// The underlying crypto failure. source: CryptoError, }, + /// Provider HKDF returned empty output. HkdfEmptyOutput(Dtls13CipherSuite), + /// No AEAD test vector exists for the suite. NoAeadTestVector(Dtls13CipherSuite), + /// Creating the AEAD cipher failed. AeadCreateFailed { + /// The DTLS 1.3 cipher suite under validation. suite: Dtls13CipherSuite, + /// The underlying crypto failure. source: CryptoError, }, + /// AEAD encryption failed. AeadEncryptFailed { + /// The DTLS 1.3 cipher suite under validation. suite: Dtls13CipherSuite, + /// The underlying crypto failure. source: CryptoError, }, + /// AEAD encryption returned the wrong output. AeadEncryptWrongOutput(Dtls13CipherSuite), + /// AEAD decryption failed. AeadDecryptFailed { + /// The DTLS 1.3 cipher suite under validation. suite: Dtls13CipherSuite, + /// The underlying crypto failure. source: CryptoError, }, + /// AEAD decryption returned the wrong output. AeadDecryptWrongOutput(Dtls13CipherSuite), + /// No record-number-encryption vector exists for the suite. NoRecordNumberEncryptionTestVector(Dtls13CipherSuite), + /// Record-number encryption returned the wrong mask. RecordNumberEncryptionWrongMask(Dtls13CipherSuite), + /// Starting key exchange failed. KeyExchangeStartFailed { + /// The key exchange group under validation. group: NamedGroup, + /// The underlying crypto failure. source: CryptoError, }, + /// Completing key exchange failed. KeyExchangeCompleteFailed { + /// The key exchange group under validation. group: NamedGroup, + /// The underlying crypto failure. source: CryptoError, }, + /// Two sides of a validation key exchange produced different shared secrets. KeyExchangeMismatchedSharedSecret(NamedGroup), + /// HMAC validation failed. HmacFailed(CryptoError), + /// HMAC validation returned the wrong output length. HmacWrongLength { + /// The expected HMAC length in bytes. expected: usize, + /// The actual HMAC length in bytes. actual: usize, }, + /// HMAC validation returned incorrect output. HmacIncorrect, + /// A free-form provider-validation reason not represented by a dedicated variant. Other(String), } From e8976f385cd52be535ce25cf4b2f5a3761cdd52d Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Fri, 29 May 2026 13:02:46 +0300 Subject: [PATCH 04/10] error: remove stringly fallback payloads --- src/crypto/aws_lc_rs/kx_group.rs | 5 +- src/crypto/aws_lc_rs/sign.rs | 7 +- src/crypto/provider.rs | 2 +- src/crypto/rust_crypto/sign.rs | 7 +- src/dtls12/client.rs | 6 +- src/dtls12/server.rs | 6 +- src/dtls13/client.rs | 4 +- src/error.rs | 118 +++++-------------------------- 8 files changed, 30 insertions(+), 125 deletions(-) diff --git a/src/crypto/aws_lc_rs/kx_group.rs b/src/crypto/aws_lc_rs/kx_group.rs index 21e38ebf..547db1b3 100644 --- a/src/crypto/aws_lc_rs/kx_group.rs +++ b/src/crypto/aws_lc_rs/kx_group.rs @@ -81,10 +81,7 @@ impl ActiveKeyExchange for EcdhKeyExchange { Ok(()) }, ) - .map_err(|e| CryptoError::ProviderFailure { - operation: CryptoOperation::CompleteKeyExchange, - reason: e.to_string(), - }) + .map_err(|_| CryptoError::OperationFailed(CryptoOperation::CompleteKeyExchange)) } fn group(&self) -> NamedGroup { diff --git a/src/crypto/aws_lc_rs/sign.rs b/src/crypto/aws_lc_rs/sign.rs index 256c0b5b..2afb986c 100644 --- a/src/crypto/aws_lc_rs/sign.rs +++ b/src/crypto/aws_lc_rs/sign.rs @@ -197,10 +197,7 @@ impl SignatureVerifier for AwsLcSignatureVerifier { } let cert = - X509Certificate::from_der(cert_der).map_err(|e| CryptoError::ProviderFailure { - operation: CryptoOperation::VerifySignature, - reason: e.to_string(), - })?; + X509Certificate::from_der(cert_der).map_err(|_| CryptoError::CertificateParseFailed)?; let spki = &cert.tbs_certificate.subject_public_key_info; const OID_EC_PUBLIC_KEY: ObjectIdentifier = @@ -226,7 +223,7 @@ impl SignatureVerifier for AwsLcSignatureVerifier { let group = match curve_oid { OID_P256 => NamedGroup::Secp256r1, OID_P384 => NamedGroup::Secp384r1, - _ => return Err(CryptoError::UnsupportedEcCurve(curve_oid.to_string())), + _ => return Err(CryptoError::UnsupportedEcCurve), }; check_verify_scheme(sig_alg, hash_alg, group)?; diff --git a/src/crypto/provider.rs b/src/crypto/provider.rs index cdff2992..a4f0af08 100644 --- a/src/crypto/provider.rs +++ b/src/crypto/provider.rs @@ -382,7 +382,7 @@ pub fn cert_named_group(cert_der: &[u8]) -> Result match curve_oid { OID_P256 => Ok(NamedGroup::Secp256r1), OID_P384 => Ok(NamedGroup::Secp384r1), - _ => Err(CertificateError::UnsupportedEcCurve(curve_oid.to_string())), + _ => Err(CertificateError::UnsupportedEcCurve), } } diff --git a/src/crypto/rust_crypto/sign.rs b/src/crypto/rust_crypto/sign.rs index bd6bc848..d5a693e6 100644 --- a/src/crypto/rust_crypto/sign.rs +++ b/src/crypto/rust_crypto/sign.rs @@ -207,10 +207,7 @@ impl SignatureVerifier for RustCryptoSignatureVerifier { } let cert = - X509Certificate::from_der(cert_der).map_err(|e| CryptoError::ProviderFailure { - operation: CryptoOperation::VerifySignature, - reason: e.to_string(), - })?; + X509Certificate::from_der(cert_der).map_err(|_| CryptoError::CertificateParseFailed)?; let spki = &cert.tbs_certificate.subject_public_key_info; const OID_EC_PUBLIC_KEY: ObjectIdentifier = @@ -236,7 +233,7 @@ impl SignatureVerifier for RustCryptoSignatureVerifier { let group = match curve_oid { OID_P256 => NamedGroup::Secp256r1, OID_P384 => NamedGroup::Secp384r1, - _ => return Err(CryptoError::UnsupportedEcCurve(curve_oid.to_string())), + _ => return Err(CryptoError::UnsupportedEcCurve), }; check_verify_scheme(sig_alg, hash_alg, group)?; diff --git a/src/dtls12/client.rs b/src/dtls12/client.rs index 6bdd19c7..7f962b6d 100644 --- a/src/dtls12/client.rs +++ b/src/dtls12/client.rs @@ -992,9 +992,9 @@ impl State { let session_hash = client .captured_session_hash .as_ref() - .ok_or(Error::InvalidState(crate::InvalidStateError::Other( - "Extended Master Secret negotiated but session hash not captured", - )))?; + .ok_or(Error::InvalidState( + crate::InvalidStateError::ExtendedMasterSecretSessionHashMissing, + ))?; trace!( "Using captured session hash for Extended Master Secret (length: {})", session_hash.len() diff --git a/src/dtls12/server.rs b/src/dtls12/server.rs index 54d11457..48e22f1a 100644 --- a/src/dtls12/server.rs +++ b/src/dtls12/server.rs @@ -846,9 +846,9 @@ impl State { let session_hash = server .captured_session_hash .as_ref() - .ok_or(Error::InvalidState(crate::InvalidStateError::Other( - "Extended Master Secret negotiated but session hash not captured", - )))?; + .ok_or(Error::InvalidState( + crate::InvalidStateError::ExtendedMasterSecretSessionHashMissing, + ))?; let mut out = server.engine.pop_buffer(); let mut scratch = server.engine.pop_buffer(); diff --git a/src/dtls13/client.rs b/src/dtls13/client.rs index 40310d23..b08a4e8c 100644 --- a/src/dtls13/client.rs +++ b/src/dtls13/client.rs @@ -1482,7 +1482,7 @@ fn parse_certificate_request(cr_data: &[u8], base_offset: usize) -> Result 0 { if cr_data.len() < 1 + context_len { return Err(Error::UnexpectedMessage( - "CertificateRequest context truncated".into(), + crate::UnexpectedMessageError::CertificateRequestContextTruncated, )); } let mut ctx = Buf::new(); @@ -1501,7 +1501,7 @@ fn parse_certificate_request(cr_data: &[u8], base_offset: usize) -> Result for Error { } } -impl From for UnexpectedMessageError { - fn from(value: String) -> Self { - Self::Other(value) - } -} - -impl From<&'static str> for UnexpectedMessageError { - fn from(value: &'static str) -> Self { - Self::Other(value.to_string()) - } -} - -impl From for CryptoError { - fn from(value: String) -> Self { - Self::Other(value) - } -} - -impl From<&'static str> for CryptoError { - fn from(value: &'static str) -> Self { - Self::Other(value.to_string()) - } -} - -impl From for CertificateError { - fn from(value: String) -> Self { - Self::Other(value) - } -} - -impl From<&'static str> for CertificateError { - fn from(value: &'static str) -> Self { - Self::Other(value.to_string()) - } -} - -impl From for SecurityError { - fn from(value: String) -> Self { - Self::Other(value) - } -} - -impl From<&'static str> for SecurityError { - fn from(value: &'static str) -> Self { - Self::Other(value.to_string()) - } -} - -impl From for ConfigError { - fn from(value: String) -> Self { - Self::Other(value) - } -} - -impl From<&'static str> for ConfigError { - fn from(value: &'static str) -> Self { - Self::Other(value.to_string()) - } -} - impl std::error::Error for Error {} impl fmt::Display for InternalError { @@ -900,7 +819,6 @@ impl fmt::Display for UnexpectedMessageError { Self::CertificateRequestExtensionsTruncated => { write!(f, "CertificateRequest extensions truncated") } - Self::Other(msg) => write!(f, "{msg}"), } } } @@ -940,7 +858,12 @@ impl fmt::Display for InvalidStateError { Self::ExporterMasterSecretNotDerived => { write!(f, "exporter master secret not yet derived") } - Self::Other(msg) => write!(f, "{msg}"), + Self::ExtendedMasterSecretSessionHashMissing => { + write!( + f, + "extended master secret negotiated but session hash not captured" + ) + } } } } @@ -1004,7 +927,8 @@ impl fmt::Display for CryptoError { "unsupported signature verification: {signature:?} + {hash:?} + {group:?}" ), Self::UnsupportedPublicKeyAlgorithm => write!(f, "unsupported public key algorithm"), - Self::UnsupportedEcCurve(curve) => write!(f, "unsupported EC curve: {curve}"), + Self::UnsupportedEcCurve => write!(f, "unsupported EC curve"), + Self::CertificateParseFailed => write!(f, "failed to parse certificate"), Self::MissingEcCurveParameter => { write!(f, "missing EC curve parameter in certificate") } @@ -1074,10 +998,6 @@ impl fmt::Display for CryptoError { write!(f, "exporter master secret not yet derived") } Self::OperationFailed(op) => write!(f, "{op} failed"), - Self::ProviderFailure { operation, reason } => { - write!(f, "{operation} failed: {reason}") - } - Self::Other(msg) => write!(f, "{msg}"), } } } @@ -1150,13 +1070,12 @@ impl fmt::Display for CertificateError { Self::InvalidEcCurveParameter => { write!(f, "invalid EC curve parameter in certificate") } - Self::UnsupportedEcCurve(curve) => write!(f, "unsupported EC curve: {curve}"), + Self::UnsupportedEcCurve => write!(f, "unsupported EC curve"), Self::InvalidSubjectPublicKey => { write!(f, "invalid EC subject_public_key bitstring") } Self::PrivateKey(err) => write!(f, "private key error: {err}"), Self::Verification(err) => write!(f, "verification failed: {err}"), - Self::Other(msg) => write!(f, "{msg}"), } } } @@ -1274,7 +1193,6 @@ impl fmt::Display for SecurityError { "received fatal alert: level={level}, description={description}" ) } - Self::Other(msg) => write!(f, "{msg}"), } } } @@ -1285,7 +1203,6 @@ impl fmt::Display for PskError { Self::NoPskResolverConfigured => write!(f, "no PSK resolver configured"), Self::NoPskIdentityConfigured => write!(f, "no PSK identity configured"), Self::ResolverReturnedNoKey => write!(f, "PSK resolver returned no key"), - Self::Other(msg) => write!(f, "{msg}"), } } } @@ -1296,7 +1213,6 @@ impl fmt::Display for TimeoutError { Self::HybridClientHello => write!(f, "hybrid ClientHello"), Self::Connect => write!(f, "connect"), Self::Handshake => write!(f, "handshake"), - Self::Other(msg) => write!(f, "{msg}"), } } } @@ -1340,7 +1256,6 @@ impl fmt::Display for ConfigError { "DTLS 1.3 cipher suites are enabled but no key exchange groups remain after filtering" ), Self::CryptoProvider(err) => write!(f, "crypto provider validation failed: {err}"), - Self::Other(msg) => write!(f, "{msg}"), } } } @@ -1437,7 +1352,6 @@ impl fmt::Display for CryptoProviderValidationError { Self::HmacIncorrect => { write!(f, "HMAC provider produced incorrect result for HMAC-SHA256") } - Self::Other(msg) => write!(f, "{msg}"), } } } From 7220af48aa097f9fcf4a4304b99e9965f3763735 Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Fri, 29 May 2026 13:23:12 +0300 Subject: [PATCH 05/10] error: tighten structured error payloads --- src/config.rs | 2 +- src/crypto/aws_lc_rs/cipher_suite.rs | 15 +++- src/crypto/aws_lc_rs/kx_group.rs | 20 ++++- src/crypto/aws_lc_rs/sign.rs | 46 +++++++++- src/crypto/ccm_cipher.rs | 21 ++--- src/crypto/rust_crypto/cipher_suite.rs | 43 ++------- src/crypto/rust_crypto/kx_group.rs | 24 +++++- src/crypto/rust_crypto/sign.rs | 60 +++++++++++-- src/crypto/validation/mod.rs | 22 ----- src/dtls12/engine.rs | 2 - src/dtls13/client.rs | 4 +- src/dtls13/engine.rs | 1 - src/error.rs | 115 +++++++++---------------- 13 files changed, 207 insertions(+), 168 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9ae2b4c1..b84dc122 100644 --- a/src/config.rs +++ b/src/config.rs @@ -508,7 +508,7 @@ impl ConfigBuilder { // Validate MTU: must be large enough for DTLS record + handshake headers if self.mtu < 64 { return Err(Error::ConfigError(ConfigError::MtuTooSmall { - mtu: self.mtu, + mtu: self.mtu as u16, minimum: 64, })); } diff --git a/src/crypto/aws_lc_rs/cipher_suite.rs b/src/crypto/aws_lc_rs/cipher_suite.rs index c775cfd8..0d20c3ca 100644 --- a/src/crypto/aws_lc_rs/cipher_suite.rs +++ b/src/crypto/aws_lc_rs/cipher_suite.rs @@ -9,6 +9,7 @@ use super::super::{Cipher, SupportedDtls12CipherSuite, SupportedDtls13CipherSuit use crate::buffer::{Buf, TmpBuf}; use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::Dtls12CipherSuite; +use crate::error::bounded_error_len; use crate::types::{Dtls13CipherSuite, HashAlgorithm}; use crate::{CryptoError, CryptoOperation}; @@ -28,7 +29,11 @@ impl AesGcm { let algorithm = match key.len() { 16 => &AES_128_GCM, 32 => &AES_256_GCM, - _ => return Err(CryptoError::InvalidAesGcmKeySize { actual: key.len() }), + _ => { + return Err(CryptoError::InvalidAesGcmKeySize { + actual: bounded_error_len(key.len()), + }); + } }; let unbound_key = UnboundKey::new(algorithm, key) @@ -63,7 +68,7 @@ impl Cipher for AesGcm { if ciphertext.len() < 16 { return Err(CryptoError::CiphertextTooShort { minimum: 16, - actual: ciphertext.len(), + actual: ciphertext.len() as u8, }); } @@ -101,7 +106,9 @@ impl ChaCha20Poly1305Cipher { // The UnboundKey::new call also validates length, but doesnt give us // a reasonable error message. This makes it equivalent to RustCrypto if key.len() != 32 { - return Err(CryptoError::InvalidChacha20Poly1305KeySize { actual: key.len() }); + return Err(CryptoError::InvalidChacha20Poly1305KeySize { + actual: bounded_error_len(key.len()), + }); } let unbound_key = UnboundKey::new(&CHACHA20_POLY1305, key) .map_err(|_| CryptoError::OperationFailed(CryptoOperation::CreateCipher))?; @@ -135,7 +142,7 @@ impl Cipher for ChaCha20Poly1305Cipher { if ciphertext.len() < 16 { return Err(CryptoError::CiphertextTooShort { minimum: 16, - actual: ciphertext.len(), + actual: ciphertext.len() as u8, }); } diff --git a/src/crypto/aws_lc_rs/kx_group.rs b/src/crypto/aws_lc_rs/kx_group.rs index 547db1b3..1a79cce9 100644 --- a/src/crypto/aws_lc_rs/kx_group.rs +++ b/src/crypto/aws_lc_rs/kx_group.rs @@ -81,7 +81,7 @@ impl ActiveKeyExchange for EcdhKeyExchange { Ok(()) }, ) - .map_err(|_| CryptoError::OperationFailed(CryptoOperation::CompleteKeyExchange)) + .map_err(|_| CryptoError::InvalidPublicKey(self.group)) } fn group(&self) -> NamedGroup { @@ -139,3 +139,21 @@ static KX_GROUP_P384: P384 = P384; /// All supported key exchange groups. pub(super) static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[&KX_GROUP_X25519, &KX_GROUP_P256, &KX_GROUP_P384]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn x25519_non_contributory_peer_key_returns_invalid_public_key() { + let exchange = X25519Kx + .start_exchange(Buf::new()) + .expect("start key exchange"); + let mut out = Buf::new(); + + assert_eq!( + exchange.complete(&[0; 32], &mut out), + Err(CryptoError::InvalidPublicKey(NamedGroup::X25519)) + ); + } +} diff --git a/src/crypto/aws_lc_rs/sign.rs b/src/crypto/aws_lc_rs/sign.rs index 2afb986c..614202d5 100644 --- a/src/crypto/aws_lc_rs/sign.rs +++ b/src/crypto/aws_lc_rs/sign.rs @@ -240,7 +240,11 @@ impl SignatureVerifier for AwsLcSignatureVerifier { let public_key = UnparsedPublicKey::new(algorithm, pubkey_bytes); public_key .verify(data, signature) - .map_err(|_| CryptoError::OperationFailed(CryptoOperation::VerifySignature)) + .map_err(|_| CryptoError::SignatureVerificationFailed { + signature: sig_alg, + hash: hash_alg, + group, + }) } } @@ -249,3 +253,43 @@ pub(super) static KEY_PROVIDER: AwsLcKeyProvider = AwsLcKeyProvider; /// Static instance of the signature verifier. pub(super) static SIGNATURE_VERIFIER: AwsLcSignatureVerifier = AwsLcSignatureVerifier; + +#[cfg(all(test, feature = "rcgen"))] +mod tests { + use super::*; + use crate::certificate::generate_self_signed_certificate; + + #[test] + fn invalid_signature_returns_structured_verification_error() { + let cert = generate_self_signed_certificate().expect("generate cert"); + let mut key = KEY_PROVIDER + .load_private_key(&cert.private_key) + .expect("load private key"); + let data = b"signed data"; + let mut signature = Buf::new(); + key.sign(data, HashAlgorithm::SHA256, &mut signature) + .expect("sign data"); + + let last = signature.len() - 1; + signature[last] ^= 0x01; + + let err = SIGNATURE_VERIFIER + .verify_signature( + &cert.certificate, + data, + &signature, + HashAlgorithm::SHA256, + SignatureAlgorithm::ECDSA, + ) + .expect_err("corrupt signature should fail"); + + assert_eq!( + err, + CryptoError::SignatureVerificationFailed { + signature: SignatureAlgorithm::ECDSA, + hash: HashAlgorithm::SHA256, + group: NamedGroup::Secp256r1, + } + ); + } +} diff --git a/src/crypto/ccm_cipher.rs b/src/crypto/ccm_cipher.rs index be2c8600..463ecd5d 100644 --- a/src/crypto/ccm_cipher.rs +++ b/src/crypto/ccm_cipher.rs @@ -9,6 +9,7 @@ use ccm::consts::{U8, U12}; use super::{Aad, Cipher, Nonce}; use crate::buffer::{Buf, TmpBuf}; +use crate::error::bounded_error_len; use crate::{CryptoError, CryptoOperation}; /// AES-128-CCM with 8-byte tag, 12-byte nonce. @@ -28,7 +29,9 @@ impl std::fmt::Debug for AesCcm8Cipher { impl AesCcm8Cipher { pub fn new(key: &[u8]) -> Result { if key.len() != 16 { - return Err(CryptoError::InvalidAes128Ccm8KeySize { actual: key.len() }); + return Err(CryptoError::InvalidAes128Ccm8KeySize { + actual: bounded_error_len(key.len()), + }); } let cipher = Aes128Ccm8::new_from_slice(key) .map_err(|_| CryptoError::OperationFailed(CryptoOperation::CreateCipher))?; @@ -40,13 +43,6 @@ impl AesCcm8Cipher { impl Cipher for AesCcm8Cipher { fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { - if nonce.len() != 12 { - return Err(CryptoError::InvalidNonceLength { - expected: 12, - actual: nonce.len(), - }); - } - let ccm_nonce = ccm::aead::generic_array::GenericArray::from_slice(&nonce[..12]); let tag = self .cipher @@ -68,14 +64,7 @@ impl Cipher for AesCcm8Cipher { if ciphertext.len() < 8 { return Err(CryptoError::CiphertextTooShort { minimum: 8, - actual: ciphertext.len(), - }); - } - - if nonce.len() != 12 { - return Err(CryptoError::InvalidNonceLength { - expected: 12, - actual: nonce.len(), + actual: ciphertext.len() as u8, }); } diff --git a/src/crypto/rust_crypto/cipher_suite.rs b/src/crypto/rust_crypto/cipher_suite.rs index acd9b51b..7c48282d 100644 --- a/src/crypto/rust_crypto/cipher_suite.rs +++ b/src/crypto/rust_crypto/cipher_suite.rs @@ -10,6 +10,7 @@ use super::super::{Cipher, SupportedDtls12CipherSuite, SupportedDtls13CipherSuit use crate::buffer::{Buf, TmpBuf}; use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::Dtls12CipherSuite; +use crate::error::bounded_error_len; use crate::types::{Dtls13CipherSuite, HashAlgorithm}; use crate::{CryptoError, CryptoOperation}; @@ -39,21 +40,15 @@ impl AesGcm { let key = Key::::from_slice(key); Ok(AesGcm::Aes256(Box::new(Aes256Gcm::new(key)))) } - _ => Err(CryptoError::InvalidAesGcmKeySize { actual: key.len() }), + _ => Err(CryptoError::InvalidAesGcmKeySize { + actual: bounded_error_len(key.len()), + }), } } } impl Cipher for AesGcm { fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { - // AES-GCM nonce is 12 bytes - if nonce.len() != 12 { - return Err(CryptoError::InvalidNonceLength { - expected: 12, - actual: nonce.len(), - }); - } - // Create nonce from the provided nonce bytes let nonce_array: [u8; 12] = nonce[..12] .try_into() @@ -90,15 +85,7 @@ impl Cipher for AesGcm { if ciphertext.len() < 16 { return Err(CryptoError::CiphertextTooShort { minimum: 16, - actual: ciphertext.len(), - }); - } - - // AES-GCM nonce is 12 bytes - if nonce.len() != 12 { - return Err(CryptoError::InvalidNonceLength { - expected: 12, - actual: nonce.len(), + actual: ciphertext.len() as u8, }); } @@ -149,7 +136,9 @@ impl ChaCha20Poly1305Cipher { fn new(key: &[u8]) -> Result { use chacha20poly1305::KeyInit; if key.len() != 32 { - return Err(CryptoError::InvalidChacha20Poly1305KeySize { actual: key.len() }); + return Err(CryptoError::InvalidChacha20Poly1305KeySize { + actual: bounded_error_len(key.len()), + }); } let key = chacha20poly1305::Key::from_slice(key); Ok(ChaCha20Poly1305Cipher { @@ -160,13 +149,6 @@ impl ChaCha20Poly1305Cipher { impl Cipher for ChaCha20Poly1305Cipher { fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { - if nonce.len() != 12 { - return Err(CryptoError::InvalidNonceLength { - expected: 12, - actual: nonce.len(), - }); - } - let nonce_array: [u8; 12] = nonce[..12] .try_into() .map_err(|_| CryptoError::InvalidNonce)?; @@ -189,14 +171,7 @@ impl Cipher for ChaCha20Poly1305Cipher { if ciphertext.len() < 16 { return Err(CryptoError::CiphertextTooShort { minimum: 16, - actual: ciphertext.len(), - }); - } - - if nonce.len() != 12 { - return Err(CryptoError::InvalidNonceLength { - expected: 12, - actual: nonce.len(), + actual: ciphertext.len() as u8, }); } diff --git a/src/crypto/rust_crypto/kx_group.rs b/src/crypto/rust_crypto/kx_group.rs index 03ef91d7..6593b0dc 100644 --- a/src/crypto/rust_crypto/kx_group.rs +++ b/src/crypto/rust_crypto/kx_group.rs @@ -4,9 +4,9 @@ use p256::{PublicKey as P256PublicKey, ecdh::EphemeralSecret}; use p384::{PublicKey as P384PublicKey, ecdh::EphemeralSecret as P384EphemeralSecret}; use super::super::{ActiveKeyExchange, SupportedKxGroup}; +use crate::CryptoError; use crate::buffer::Buf; use crate::types::NamedGroup; -use crate::{CryptoError, CryptoOperation}; /// ECDHE key exchange implementation. enum EcdhKeyExchange { @@ -105,9 +105,7 @@ impl ActiveKeyExchange for EcdhKeyExchange { let shared_secret = secret.diffie_hellman(&peer_key); // RFC 7748 §6.1: check the shared secret is not zero (low-order point) if !shared_secret.was_contributory() { - return Err(CryptoError::OperationFailed( - CryptoOperation::CompleteKeyExchange, - )); + return Err(CryptoError::InvalidPublicKey(NamedGroup::X25519)); } out.clear(); out.extend_from_slice(shared_secret.as_bytes()); @@ -191,3 +189,21 @@ static KX_GROUP_P384: P384 = P384; /// All supported key exchange groups. pub(super) static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[&KX_GROUP_X25519, &KX_GROUP_P256, &KX_GROUP_P384]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn x25519_non_contributory_peer_key_returns_invalid_public_key() { + let exchange = X25519Kx + .start_exchange(Buf::new()) + .expect("start key exchange"); + let mut out = Buf::new(); + + assert_eq!( + exchange.complete(&[0; 32], &mut out), + Err(CryptoError::InvalidPublicKey(NamedGroup::X25519)) + ); + } +} diff --git a/src/crypto/rust_crypto/sign.rs b/src/crypto/rust_crypto/sign.rs index d5a693e6..1bf80084 100644 --- a/src/crypto/rust_crypto/sign.rs +++ b/src/crypto/rust_crypto/sign.rs @@ -255,18 +255,26 @@ impl SignatureVerifier for RustCryptoSignatureVerifier { .map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::Secp256r1))?; let sig = Signature::::from_der(signature) .map_err(|_| CryptoError::InvalidSignatureFormat)?; - verifying_key - .verify_prehash(&hash, &sig) - .map_err(|_| CryptoError::OperationFailed(CryptoOperation::VerifySignature)) + verifying_key.verify_prehash(&hash, &sig).map_err(|_| { + CryptoError::SignatureVerificationFailed { + signature: sig_alg, + hash: hash_alg, + group, + } + }) } NamedGroup::Secp384r1 => { let verifying_key = VerifyingKey::::from_sec1_bytes(pubkey_bytes) .map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::Secp384r1))?; let sig = Signature::::from_der(signature) .map_err(|_| CryptoError::InvalidSignatureFormat)?; - verifying_key - .verify_prehash(&hash, &sig) - .map_err(|_| CryptoError::OperationFailed(CryptoOperation::VerifySignature)) + verifying_key.verify_prehash(&hash, &sig).map_err(|_| { + CryptoError::SignatureVerificationFailed { + signature: sig_alg, + hash: hash_alg, + group, + } + }) } // unreachable: OID match above only produces Secp256r1/Secp384r1 _ => unreachable!(), @@ -279,3 +287,43 @@ pub(super) static KEY_PROVIDER: RustCryptoKeyProvider = RustCryptoKeyProvider; /// Static instance of the signature verifier. pub(super) static SIGNATURE_VERIFIER: RustCryptoSignatureVerifier = RustCryptoSignatureVerifier; + +#[cfg(all(test, feature = "rcgen"))] +mod tests { + use super::*; + use crate::certificate::generate_self_signed_certificate; + + #[test] + fn invalid_signature_returns_structured_verification_error() { + let cert = generate_self_signed_certificate().expect("generate cert"); + let mut key = KEY_PROVIDER + .load_private_key(&cert.private_key) + .expect("load private key"); + let data = b"signed data"; + let mut signature = Buf::new(); + key.sign(data, HashAlgorithm::SHA256, &mut signature) + .expect("sign data"); + + let last = signature.len() - 1; + signature[last] ^= 0x01; + + let err = SIGNATURE_VERIFIER + .verify_signature( + &cert.certificate, + data, + &signature, + HashAlgorithm::SHA256, + SignatureAlgorithm::ECDSA, + ) + .expect_err("corrupt signature should fail"); + + assert_eq!( + err, + CryptoError::SignatureVerificationFailed { + signature: SignatureAlgorithm::ECDSA, + hash: HashAlgorithm::SHA256, + group: NamedGroup::Secp256r1, + } + ); + } +} diff --git a/src/crypto/validation/mod.rs b/src/crypto/validation/mod.rs index 465e4941..3b19acce 100644 --- a/src/crypto/validation/mod.rs +++ b/src/crypto/validation/mod.rs @@ -177,16 +177,6 @@ impl CryptoProvider { }) })?; - if result.len() != output_len { - return Err(provider_error( - CryptoProviderValidationError::PrfWrongLength { - hash: hash_alg, - expected: output_len, - actual: result.len(), - }, - )); - } - let maybe_expected = PRF_TEST_VECTORS .iter() .find(|(h, _)| *h == hash_alg) @@ -429,18 +419,6 @@ impl CryptoProvider { .hmac_sha256(key, data) .map_err(|e| provider_error(CryptoProviderValidationError::HmacFailed(e)))?; - // Verify the result matches expected HMAC-SHA256 output - // Expected: HMAC-SHA256("key", "The quick brown fox jumps over the lazy dog") - // This is a standard test vector for HMAC-SHA256 - if result.len() != 32 { - return Err(provider_error( - CryptoProviderValidationError::HmacWrongLength { - expected: 32, - actual: result.len(), - }, - )); - } - // Verify against known HMAC-SHA256 test vector if result.as_slice() != HMAC_SHA256_TEST_VECTOR { return Err(provider_error(CryptoProviderValidationError::HmacIncorrect)); diff --git a/src/dtls12/engine.rs b/src/dtls12/engine.rs index 69a31331..1777e147 100644 --- a/src/dtls12/engine.rs +++ b/src/dtls12/engine.rs @@ -1281,7 +1281,6 @@ impl RecordHandler for Engine { if let Some(description) = fatal_description { return Err(Error::SecurityError(crate::SecurityError::FatalAlert { - level: 2, description, })); } @@ -1309,7 +1308,6 @@ impl RecordHandler for Engine { if level == 2 { return Err(Error::SecurityError(crate::SecurityError::FatalAlert { - level, description, })); } diff --git a/src/dtls13/client.rs b/src/dtls13/client.rs index b08a4e8c..90133a5c 100644 --- a/src/dtls13/client.rs +++ b/src/dtls13/client.rs @@ -638,8 +638,8 @@ impl State { if key_exchange.group() != server_group { return Err( Error::SecurityError(crate::SecurityError::ServerKeyShareGroupMismatch { - selected: server_group, - actual: key_exchange.group(), + expected: key_exchange.group(), + actual: server_group, }) .into(), ); diff --git a/src/dtls13/engine.rs b/src/dtls13/engine.rs index 5cac6f9a..d67ccc34 100644 --- a/src/dtls13/engine.rs +++ b/src/dtls13/engine.rs @@ -2329,7 +2329,6 @@ impl RecordHandler for Engine { Some(90) => Ok(None), Some(description) => { Err(Error::SecurityError(crate::SecurityError::FatalAlert { - level: 2, description, })) } diff --git a/src/error.rs b/src/error.rs index b000a3b2..de743530 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,6 +11,10 @@ use crate::types::ProtocolVersion; use crate::types::SignatureAlgorithm; use crate::types::SignatureScheme; +pub(crate) fn bounded_error_len(len: usize) -> u16 { + len.min(u16::MAX as usize) as u16 +} + #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] /// Errors returned by DTLS processing functions. @@ -183,6 +187,15 @@ pub enum CryptoError { /// The public key group used for verification. group: NamedGroup, }, + /// Signature verification failed for a supported signature/hash/group combination. + SignatureVerificationFailed { + /// The requested signature algorithm. + signature: SignatureAlgorithm, + /// The requested hash algorithm. + hash: HashAlgorithm, + /// The public key group used for verification. + group: NamedGroup, + }, /// The public key algorithm is unsupported. UnsupportedPublicKeyAlgorithm, /// A certificate or key references an unsupported EC curve. @@ -218,33 +231,32 @@ pub enum CryptoError { /// An AES-GCM key had the wrong length. InvalidAesGcmKeySize { /// The actual key length in bytes. - actual: usize, + /// + /// Values greater than `u16::MAX` are reported as `u16::MAX`. + actual: u16, }, /// A ChaCha20-Poly1305 key had the wrong length. InvalidChacha20Poly1305KeySize { /// The actual key length in bytes. - actual: usize, + /// + /// Values greater than `u16::MAX` are reported as `u16::MAX`. + actual: u16, }, /// An AES-128-CCM-8 key had the wrong length. InvalidAes128Ccm8KeySize { /// The actual key length in bytes. - actual: usize, + /// + /// Values greater than `u16::MAX` are reported as `u16::MAX`. + actual: u16, }, /// A nonce was invalid for the selected cipher. InvalidNonce, - /// A nonce had the wrong length. - InvalidNonceLength { - /// The expected nonce length in bytes. - expected: usize, - /// The actual nonce length in bytes. - actual: usize, - }, /// A ciphertext was shorter than the selected AEAD permits. CiphertextTooShort { /// The minimum accepted ciphertext length in bytes. - minimum: usize, + minimum: u8, /// The actual ciphertext length in bytes. - actual: usize, + actual: u8, }, /// The requested HKDF output length is too large. HkdfOutputTooLong, @@ -396,10 +408,6 @@ pub enum CertificateError { UnsupportedEcCurve, /// A certificate had an invalid subject public key. InvalidSubjectPublicKey, - /// Private-key handling failed during certificate processing. - PrivateKey(CryptoError), - /// Certificate verification failed. - Verification(CryptoError), } /// Fine-grained reason for an [`Error::SecurityError`]. @@ -456,10 +464,10 @@ pub enum SecurityError { ServerDidNotNegotiateDtls13, /// A DTLS 1.3 server did not send a key share. ServerMissingKeyShare, - /// The server key share group did not match the negotiated group. + /// The server key share group did not match the expected group. ServerKeyShareGroupMismatch { - /// The negotiated key exchange group. - selected: NamedGroup, + /// The expected key exchange group. + expected: NamedGroup, /// The key exchange group in the server key share. actual: NamedGroup, }, @@ -491,8 +499,6 @@ pub enum SecurityError { ClientFinishedVerificationFailed, /// A fatal DTLS alert was received. FatalAlert { - /// The DTLS alert level. - level: u8, /// The DTLS alert description. description: u8, }, @@ -526,14 +532,12 @@ pub enum TimeoutError { #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum ConfigError { - /// No crypto provider is available. - NoCryptoProvider, /// The configured MTU is smaller than dimpl permits. MtuTooSmall { /// The configured MTU. - mtu: usize, + mtu: u16, /// The minimum accepted MTU. - minimum: usize, + minimum: u16, }, /// The configured AEAD encryption limit is too small. AeadEncryptionLimitTooSmall, @@ -570,15 +574,6 @@ pub enum CryptoProviderValidationError { /// The underlying crypto failure. source: CryptoError, }, - /// The provider PRF returned the wrong output length. - PrfWrongLength { - /// The hash algorithm used by the PRF. - hash: HashAlgorithm, - /// The expected output length in bytes. - expected: usize, - /// The actual output length in bytes. - actual: usize, - }, /// No PRF test vector exists for the hash algorithm. MissingPrfTestVector(HashAlgorithm), /// The provider PRF returned incorrect output. @@ -657,13 +652,6 @@ pub enum CryptoProviderValidationError { KeyExchangeMismatchedSharedSecret(NamedGroup), /// HMAC validation failed. HmacFailed(CryptoError), - /// HMAC validation returned the wrong output length. - HmacWrongLength { - /// The expected HMAC length in bytes. - expected: usize, - /// The actual HMAC length in bytes. - actual: usize, - }, /// HMAC validation returned incorrect output. HmacIncorrect, } @@ -926,6 +914,14 @@ impl fmt::Display for CryptoError { f, "unsupported signature verification: {signature:?} + {hash:?} + {group:?}" ), + Self::SignatureVerificationFailed { + signature, + hash, + group, + } => write!( + f, + "signature verification failed: {signature:?} + {hash:?} + {group:?}" + ), Self::UnsupportedPublicKeyAlgorithm => write!(f, "unsupported public key algorithm"), Self::UnsupportedEcCurve => write!(f, "unsupported EC curve"), Self::CertificateParseFailed => write!(f, "failed to parse certificate"), @@ -963,9 +959,6 @@ impl fmt::Display for CryptoError { write!(f, "invalid key size for AES-128-CCM-8: {actual}") } Self::InvalidNonce => write!(f, "invalid nonce"), - Self::InvalidNonceLength { expected, actual } => { - write!(f, "invalid nonce length: expected {expected}, got {actual}") - } Self::CiphertextTooShort { minimum, actual } => { write!(f, "ciphertext too short: got {actual}, minimum {minimum}") } @@ -1074,8 +1067,6 @@ impl fmt::Display for CertificateError { Self::InvalidSubjectPublicKey => { write!(f, "invalid EC subject_public_key bitstring") } - Self::PrivateKey(err) => write!(f, "private key error: {err}"), - Self::Verification(err) => write!(f, "verification failed: {err}"), } } } @@ -1154,10 +1145,10 @@ impl fmt::Display for SecurityError { write!(f, "server did not negotiate DTLS 1.3") } Self::ServerMissingKeyShare => write!(f, "server missing key_share"), - Self::ServerKeyShareGroupMismatch { selected, actual } => { + Self::ServerKeyShareGroupMismatch { expected, actual } => { write!( f, - "server key_share group mismatch: selected {selected:?}, actual {actual:?}" + "server key_share group mismatch: expected {expected:?}, actual {actual:?}" ) } Self::SignatureTooLarge => write!(f, "signature too large"), @@ -1187,11 +1178,8 @@ impl fmt::Display for SecurityError { Self::ClientFinishedVerificationFailed => { write!(f, "client Finished verification failed") } - Self::FatalAlert { level, description } => { - write!( - f, - "received fatal alert: level={level}, description={description}" - ) + Self::FatalAlert { description } => { + write!(f, "received fatal alert: description={description}") } } } @@ -1220,13 +1208,6 @@ impl fmt::Display for TimeoutError { impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::NoCryptoProvider => write!( - f, - concat!( - "no crypto provider available; set one explicitly, install a default, ", - "or enable a crypto feature" - ) - ), Self::MtuTooSmall { mtu, minimum } => { write!(f, "MTU {mtu} is too small (minimum {minimum})") } @@ -1280,14 +1261,6 @@ impl fmt::Display for CryptoProviderValidationError { write!(f, "hash provider {hash:?} produced incorrect result") } Self::PrfFailed { hash, source } => write!(f, "PRF failed for {hash:?}: {source}"), - Self::PrfWrongLength { - hash, - expected, - actual, - } => write!( - f, - "PRF {hash:?} returned wrong length: expected {expected}, got {actual}" - ), Self::MissingPrfTestVector(hash) => { write!(f, "no expected PRF data for hash algorithm: {hash:?}") } @@ -1343,12 +1316,6 @@ impl fmt::Display for CryptoProviderValidationError { write!(f, "key exchange produced different secrets for {group:?}") } Self::HmacFailed(source) => write!(f, "HMAC provider failed: {source}"), - Self::HmacWrongLength { expected, actual } => { - write!( - f, - "HMAC provider returned wrong length: expected {expected} bytes, got {actual}" - ) - } Self::HmacIncorrect => { write!(f, "HMAC provider produced incorrect result for HMAC-SHA256") } From 76f4af70a852a34a52f7a12f28cd442cbbf87a9f Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Fri, 29 May 2026 13:32:09 +0300 Subject: [PATCH 06/10] error: lock structured diagnostics in tests --- src/dtls12/engine.rs | 3 +- src/dtls13/client.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 4 ++- tests/dtls12/edge.rs | 9 +++--- tests/dtls13/edge.rs | 8 ++--- 5 files changed, 89 insertions(+), 11 deletions(-) diff --git a/src/dtls12/engine.rs b/src/dtls12/engine.rs index 1777e147..3bd86e99 100644 --- a/src/dtls12/engine.rs +++ b/src/dtls12/engine.rs @@ -10,6 +10,7 @@ use crate::dtls12::context::{AuthMode, CryptoContext}; use crate::dtls12::incoming::{Incoming, Record, RecordHandler}; use crate::dtls12::message::{Body, HashAlgorithm, Header, MessageType, ProtocolVersion, Sequence}; use crate::dtls12::message::{ContentType, DTLSRecord, Dtls12CipherSuite, Handshake}; +use crate::error::bounded_error_len; use crate::timer::ExponentialBackoff; use crate::window::ReplayWindow; use crate::{Config, Error, InternalError, Output, SeededRng}; @@ -820,7 +821,7 @@ impl Engine { _ => { return Err(Error::CryptoError( crate::CryptoError::UnsupportedDtls12RecordIvLen { - len: explicit_nonce_len, + len: bounded_error_len(explicit_nonce_len), suite, }, )); diff --git a/src/dtls13/client.rs b/src/dtls13/client.rs index 90133a5c..092be3bb 100644 --- a/src/dtls13/client.rs +++ b/src/dtls13/client.rs @@ -1573,6 +1573,26 @@ mod tests { use super::*; use crate::Config; use crate::certificate::generate_self_signed_certificate; + use crate::{CryptoError, SecurityError}; + + #[derive(Debug)] + struct TestKeyExchange { + group: NamedGroup, + } + + impl ActiveKeyExchange for TestKeyExchange { + fn pub_key(&self) -> &[u8] { + &[] + } + + fn complete(self: Box, _peer_pub: &[u8], _out: &mut Buf) -> Result<(), CryptoError> { + unreachable!("mismatched server key share should fail before completing ECDHE") + } + + fn group(&self) -> NamedGroup { + self.group + } + } fn client() -> Client { let cert = generate_self_signed_certificate().expect("generate cert"); @@ -1597,6 +1617,31 @@ mod tests { packet } + fn server_hello_with_key_share(group: NamedGroup) -> Vec { + let mut key_share = Vec::new(); + key_share.extend_from_slice(&group.as_u16().to_be_bytes()); + key_share.extend_from_slice(&1u16.to_be_bytes()); + key_share.push(0); + + let mut extensions = Vec::new(); + extensions.extend_from_slice(&ExtensionType::SupportedVersions.as_u16().to_be_bytes()); + extensions.extend_from_slice(&2u16.to_be_bytes()); + extensions.extend_from_slice(&ProtocolVersion::DTLS1_3.as_u16().to_be_bytes()); + extensions.extend_from_slice(&ExtensionType::KeyShare.as_u16().to_be_bytes()); + extensions.extend_from_slice(&(key_share.len() as u16).to_be_bytes()); + extensions.extend_from_slice(&key_share); + + let mut body = Vec::new(); + body.extend_from_slice(&ProtocolVersion::DTLS1_2.as_u16().to_be_bytes()); + body.extend_from_slice(&[7; 32]); + body.push(0); // legacy_session_id + body.extend_from_slice(&Dtls13CipherSuite::AES_128_GCM_SHA256.as_u16().to_be_bytes()); + body.push(CompressionMethod::Null.as_u8()); + body.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); + body.extend_from_slice(&extensions); + body + } + #[test] fn empty_server_certificate_is_certificate_error() { let mut client = client(); @@ -1621,4 +1666,35 @@ mod tests { crate::InternalError::Fatal(Error::CertificateError(_)) )); } + + #[test] + fn server_key_share_group_mismatch_reports_expected_and_actual_groups() { + let mut client = client(); + client.active_key_exchange = Some(Box::new(TestKeyExchange { + group: NamedGroup::X25519, + })); + + client + .engine + .parse_packet(&epoch0_handshake_packet( + MessageType::ServerHello, + 0, + &server_hello_with_key_share(NamedGroup::Secp256r1), + )) + .expect("queue mismatched ServerHello"); + + let err = State::AwaitServerHello + .await_server_hello(&mut client) + .expect_err("mismatched server key share group should fail"); + + assert!(matches!( + err, + crate::InternalError::Fatal(Error::SecurityError( + SecurityError::ServerKeyShareGroupMismatch { + expected: NamedGroup::X25519, + actual: NamedGroup::Secp256r1, + } + )) + )); + } } diff --git a/src/error.rs b/src/error.rs index de743530..25549faf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -294,7 +294,9 @@ pub enum CryptoError { /// The DTLS 1.2 record IV length is unsupported for the selected suite. UnsupportedDtls12RecordIvLen { /// The unsupported record IV length. - len: usize, + /// + /// Values greater than `u16::MAX` are reported as `u16::MAX`. + len: u16, /// The selected DTLS 1.2 cipher suite. suite: Dtls12CipherSuite, }, diff --git a/tests/dtls12/edge.rs b/tests/dtls12/edge.rs index 97bcecf3..7624c612 100644 --- a/tests/dtls12/edge.rs +++ b/tests/dtls12/edge.rs @@ -1024,11 +1024,10 @@ fn dtls12_fatal_alert_during_handshake() { "Fatal alert during handshake should return an error" ); let err = result.unwrap_err(); - assert!( - matches!(err, dimpl::Error::SecurityError(_)), - "Error should be SecurityError, got: {:?}", - err - ); + assert!(matches!( + err, + dimpl::Error::SecurityError(dimpl::SecurityError::FatalAlert { description: 40 }) + )); } #[test] diff --git a/tests/dtls13/edge.rs b/tests/dtls13/edge.rs index c716964d..33e94423 100644 --- a/tests/dtls13/edge.rs +++ b/tests/dtls13/edge.rs @@ -398,10 +398,10 @@ fn dtls13_unknown_warning_level_alert_is_still_fatal() { let err = server .handle_packet(&packet) .expect_err("non-whitelisted alert must be fatal regardless of level"); - assert!( - matches!(err, dimpl::Error::SecurityError(_)), - "expected SecurityError, got {err:?}" - ); + assert!(matches!( + err, + dimpl::Error::SecurityError(dimpl::SecurityError::FatalAlert { description: 40 }) + )); } fn queue_ack_with_peer_key_update(sender: &mut Dtls, receiver: &mut Dtls, now: &mut Instant) { From b11941392bdc41dcbcc7db69b8a1f264f8c7ed27 Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Fri, 29 May 2026 20:01:13 +0300 Subject: [PATCH 07/10] changelog: record structured error API change --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c86620e0..aa3d3dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased + * Make public errors structured and fatal-only (breaking) #134 + # 0.6.2 * Split local DTLS invalid-state errors from peer-input errors #126 From 2b9a1837e82c7f6ede0779315f3e5e778c73f410 Mon Sep 17 00:00:00 2001 From: Martin Algesten Date: Fri, 29 May 2026 21:13:52 +0200 Subject: [PATCH 08/10] error: document fatal-only error contract --- README.md | 17 +++++++++++++++++ src/lib.rs | 22 ++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 830c636d..90518ed1 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,23 @@ references into your provided buffer: - `ApplicationData(&[u8])`: plaintext received from peer - `CloseNotify`: peer sent a `close_notify` alert (graceful shutdown) +## Error handling + +Every `Error` returned by the public API +([`handle_packet`][handle_packet], [`handle_timeout`][handle_timeout], +`send_application_data`, and `close`) is **fatal**: the connection is no +longer usable and must be thrown away. The engine has no recoverable +error states, so the correct response is always to drop the `Dtls` +instance — and start a fresh handshake if you still need a connection. + +Transient, non‑fatal conditions inherent to running over an unreliable +transport — malformed datagrams, replayed or out‑of‑window records, and +other parser noise — are handled internally and never surface as an +`Error`. Such packets are discarded (logged at `debug!`) while the +connection keeps running. You therefore never need to distinguish +"retry" from "give up": a returned `Error` always means give up on this +connection. + ## Example (Sans‑IO loop) ```rust diff --git a/src/lib.rs b/src/lib.rs index e7a2caad..d17a0cc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,24 @@ //! - `PeerCert(&[u8])`: peer leaf certificate (DER) — validate in your app //! - `KeyingMaterial(KeyingMaterial, SrtpProfile)`: DTLS‑SRTP export //! - `ApplicationData(&[u8])`: plaintext received from peer -//! - `CloseNotify`: peer sent a graceful shutdown alert +//! - `CloseNotify`: peer sent a `close_notify` alert (graceful shutdown) +//! +//! # Error handling +//! +//! Every `Error` returned by the public API +//! ([`handle_packet`][handle_packet], [`handle_timeout`][handle_timeout], +//! `send_application_data`, and `close`) is **fatal**: the connection is no +//! longer usable and must be thrown away. The engine has no recoverable +//! error states, so the correct response is always to drop the `Dtls` +//! instance — and start a fresh handshake if you still need a connection. +//! +//! Transient, non‑fatal conditions inherent to running over an unreliable +//! transport — malformed datagrams, replayed or out‑of‑window records, and +//! other parser noise — are handled internally and never surface as an +//! `Error`. Such packets are discarded (logged at `debug!`) while the +//! connection keeps running. You therefore never need to distinguish +//! "retry" from "give up": a returned `Error` always means give up on this +//! connection. //! //! # Example (Sans‑IO loop) //! @@ -111,6 +128,7 @@ //! } //! Output::CloseNotify => { //! // Peer initiated graceful shutdown +//! break; //! } //! _ => {} //! } @@ -138,7 +156,7 @@ //! # } //! ``` //! -//! ## Example (PSK client) +//! # Example (PSK client) //! //! ```rust,no_run //! use std::sync::Arc; From d30b054659442a8a1f8da740f8fd5c89e780bf50 Mon Sep 17 00:00:00 2001 From: Martin Algesten Date: Fri, 29 May 2026 21:13:53 +0200 Subject: [PATCH 09/10] error: log discarded transient packets at debug --- src/error.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 25549faf..5856c569 100644 --- a/src/error.rs +++ b/src/error.rs @@ -686,7 +686,10 @@ impl InternalError { pub(crate) fn into_public_error(self) -> Option { match self { - Self::Transient(_) => None, + Self::Transient(err) => { + debug!("Discarding packet: {err}"); + None + } Self::Fatal(err) => Some(err), } } From a2ee774582a470098a3ec52f266d26c71c5833c3 Mon Sep 17 00:00:00 2001 From: Martin Algesten Date: Fri, 29 May 2026 21:30:40 +0200 Subject: [PATCH 10/10] docs: exit event loop on CloseNotify in example --- README.md | 4 ++-- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 90518ed1..88c16c63 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ fn example_event_loop(mut dtls: Dtls) -> Result<(), dimpl::Error> { // Deliver plaintext to application } Output::CloseNotify => { - // Peer initiated graceful shutdown - break; + // Peer initiated graceful shutdown — leave the event loop + return Ok(()); } _ => {} } diff --git a/src/lib.rs b/src/lib.rs index d17a0cc1..e1680c0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,8 +127,8 @@ //! // Deliver plaintext to application //! } //! Output::CloseNotify => { -//! // Peer initiated graceful shutdown -//! break; +//! // Peer initiated graceful shutdown — leave the event loop +//! return Ok(()); //! } //! _ => {} //! }