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 diff --git a/README.md b/README.md index 830c636d..88c16c63 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 @@ -110,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/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..b84dc122 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 as u16, + 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..0d20c3ca 100644 --- a/src/crypto/aws_lc_rs/cipher_suite.rs +++ b/src/crypto/aws_lc_rs/cipher_suite.rs @@ -9,7 +9,9 @@ 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}; /// AES-GCM cipher implementation using aws-lc-rs. struct AesGcm { @@ -23,15 +25,19 @@ 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: bounded_error_len(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 +46,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() as u8, + }); } 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 +102,16 @@ 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: bounded_error_len(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 +120,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() as u8, + }); } 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 +188,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 +218,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 +248,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 +278,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 +328,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 +362,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 +396,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 +451,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..1a79cce9 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,7 @@ impl ActiveKeyExchange for EcdhKeyExchange { Ok(()) }, ) - .map_err(|e| e.to_string()) + .map_err(|_| CryptoError::InvalidPublicKey(self.group)) } fn group(&self) -> NamedGroup { @@ -97,7 +98,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 +112,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 +126,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)?)) } } @@ -138,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/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..614202d5 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,39 @@ 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(|_| CryptoError::CertificateParseFailed)?; 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), }; check_verify_scheme(sig_alg, hash_alg, group)?; @@ -235,12 +238,13 @@ 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::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 d5837ace..463ecd5d 100644 --- a/src/crypto/ccm_cipher.rs +++ b/src/crypto/ccm_cipher.rs @@ -9,6 +9,8 @@ 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. type Aes128Ccm8 = ccm::Ccm; @@ -25,12 +27,14 @@ 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: bounded_error_len(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 +42,12 @@ impl AesCcm8Cipher { } impl Cipher for AesCcm8Cipher { - fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { - if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); - } - + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { 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 +55,17 @@ 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())); - } - - if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::CiphertextTooShort { + minimum: 8, + actual: ciphertext.len() as u8, + }); } let ccm_nonce = ccm::aead::generic_array::GenericArray::from_slice(&nonce[..12]); @@ -83,7 +81,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..a4f0af08 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), } } /// 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..7c48282d 100644 --- a/src/crypto/rust_crypto/cipher_suite.rs +++ b/src/crypto/rust_crypto/cipher_suite.rs @@ -10,7 +10,9 @@ 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}; /// AES-GCM cipher implementation using RustCrypto. enum AesGcm { @@ -28,7 +30,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 +40,19 @@ 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: bounded_error_len(key.len()), + }), } } } impl Cipher for AesGcm { - fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { - // AES-GCM nonce is 12 bytes - if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); - } - + fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { // 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 +61,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 +69,30 @@ 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())); - } - - // AES-GCM nonce is 12 bytes - if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::CiphertextTooShort { + minimum: 16, + actual: ciphertext.len() as u8, + }); } // 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 +101,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 +109,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 +133,12 @@ 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: bounded_error_len(key.len()), + }); } let key = chacha20poly1305::Key::from_slice(key); Ok(ChaCha20Poly1305Cipher { @@ -149,44 +148,42 @@ impl ChaCha20Poly1305Cipher { } impl Cipher for ChaCha20Poly1305Cipher { - fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { - if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); - } - - let nonce_array: [u8; 12] = nonce[..12].try_into().map_err(|_| "Invalid nonce")?; + fn encrypt(&mut self, data: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), CryptoError> { + 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())); - } - - if nonce.len() != 12 { - return Err(format!( - "Invalid nonce length: expected 12, got {}", - nonce.len() - )); + return Err(CryptoError::CiphertextTooShort { + minimum: 16, + actual: ciphertext.len() as u8, + }); } - 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 +214,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 +244,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 +274,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 +304,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 +354,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 +392,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 +430,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 +475,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..6593b0dc 100644 --- a/src/crypto/rust_crypto/kx_group.rs +++ b/src/crypto/rust_crypto/kx_group.rs @@ -4,6 +4,7 @@ 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; @@ -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,17 @@ 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::InvalidPublicKey(NamedGroup::X25519)); } out.clear(); out.extend_from_slice(shared_secret.as_bytes()); @@ -112,7 +113,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 +121,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 +148,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 +162,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 +176,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)?)) } } @@ -188,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/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..1bf80084 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,39 @@ 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(|_| CryptoError::CertificateParseFailed)?; 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), }; check_verify_scheme(sig_alg, hash_alg, group)?; @@ -249,26 +252,28 @@ 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())?; + .map_err(|_| CryptoError::InvalidSignatureFormat)?; verifying_key.verify_prehash(&hash, &sig).map_err(|_| { - format!( - "ECDSA signature verification failed for {:?} {:?}", - hash_alg, group - ) + CryptoError::SignatureVerificationFailed { + signature: sig_alg, + hash: hash_alg, + group, + } }) } 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())?; + .map_err(|_| CryptoError::InvalidSignatureFormat)?; verifying_key.verify_prehash(&hash, &sig).map_err(|_| { - format!( - "ECDSA signature verification failed for {:?} {:?}", - hash_alg, group - ) + CryptoError::SignatureVerificationFailed { + signature: sig_alg, + hash: hash_alg, + group, + } }) } // unreachable: OID match above only produces Secp256r1/Secp384r1 @@ -282,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 7d4a7bea..3b19acce 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,16 +170,12 @@ impl CryptoProvider { &mut scratch, hash_alg, ) - .map_err(|e| Error::ConfigError(format!("PRF failed for {:?}: {}", hash_alg, e)))?; - - if result.len() != output_len { - return Err(Error::ConfigError(format!( - "PRF {:?} returned wrong length: expected {}, got {}", - hash_alg, - output_len, - result.len() - ))); - } + .map_err(|e| { + provider_error(CryptoProviderValidationError::PrfFailed { + hash: hash_alg, + source: e, + }) + })?; let maybe_expected = PRF_TEST_VECTORS .iter() @@ -186,16 +183,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 +224,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 +237,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 +251,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 +265,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 +288,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 +299,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 +344,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 +365,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 +382,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 +417,11 @@ impl CryptoProvider { let result = self .hmac_provider .hmac_sha256(key, data) - .map_err(|e| Error::ConfigError(format!("HMAC provider failed: {}", 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() - ))); - } + .map_err(|e| provider_error(CryptoProviderValidationError::HmacFailed(e)))?; // 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 a39a3bff..7f962b6d 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,10 @@ 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()); } debug!( @@ -425,7 +426,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 +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 - ))); + 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 - ))); + 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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::ServerSelectedUnknownCipherSuite, + )) + .into()); } // Enforce cipher suite is compatible with our private key and allowed by config @@ -475,17 +481,17 @@ 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()); } // 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()); @@ -526,8 +533,9 @@ impl State { // reusing the same master secret is possible. if !extended_master_secret { return Err(Error::SecurityError( - "Extended Master Secret not negotiated".to_string(), - )); + crate::SecurityError::ExtendedMasterSecretNotNegotiated, + ) + .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,10 @@ impl State { }; if certificate.certificate_list.is_empty() { - return Err(Error::CertificateError( - "No server certificate received".to_string(), - )); + return Err((Error::CertificateError( + crate::CertificateError::NoServerCertificateReceived, + )) + .into()); } debug!( @@ -587,11 +596,10 @@ impl State { Ok(Self::AwaitServerKeyExchange) } - 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()))?; + fn await_server_key_exchange(self, client: &mut Client) -> Result { + 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) @@ -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, @@ -621,8 +629,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(), - )); + crate::UnexpectedMessageError::ServerKeyExchangeWithoutSignature, + ) + .into()); }; let signature_range = d_signed.signature_range.clone(); @@ -637,8 +646,9 @@ impl State { ), ServerKeyExchangeParams::Psk(_) => { return Err(Error::UnexpectedMessage( - "PSK ServerKeyExchange in ECDHE path".to_string(), - )); + crate::UnexpectedMessageError::PskServerKeyExchangeInEcdhePath, + ) + .into()); } }; @@ -672,26 +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(), - )); + 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 - ))); + return Err( + Error::CryptoError(crate::CryptoError::SignatureAlgorithmMismatch { + expected: expected_sig, + actual: signature_algorithm.signature, + }) + .into(), + ); } } @@ -708,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: {:?}", @@ -727,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) @@ -737,7 +743,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 @@ -765,8 +771,9 @@ impl State { ServerKeyExchangeParams::Psk(psk) => psk.hint_range.clone(), _ => { return Err(Error::UnexpectedMessage( - "ECDHE ServerKeyExchange in PSK path".to_string(), - )); + crate::UnexpectedMessageError::EcdheServerKeyExchangeInPskPath, + ) + .into()); } }; @@ -780,7 +787,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 +820,10 @@ 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()); } debug!( @@ -830,7 +837,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)?; @@ -846,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 @@ -858,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(), - )); + return Err((Error::CertificateError( + crate::CertificateError::NoServerCertificateReceived, + )) + .into()); } // Send the server certificate as an event @@ -875,7 +882,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 +896,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 @@ -908,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(); @@ -925,7 +931,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 +943,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 @@ -955,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, )); }; @@ -981,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::ExtendedMasterSecretSessionHashMissing, + ))?; trace!( "Using captured session hash for Extended Master Secret (length: {})", session_hash.len() @@ -996,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 @@ -1011,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); @@ -1019,7 +1026,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 +1045,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 +1064,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 +1091,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 +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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::ServerFinishedVerificationFailed, + )) + .into()); } trace!("Server Finished verified successfully"); @@ -1175,7 +1183,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. @@ -1260,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(); @@ -1284,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); @@ -1295,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, )); } } @@ -1353,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()); @@ -1431,7 +1439,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/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 82a3d960..3bd86e99 100644 --- a/src/dtls12/engine.rs +++ b/src/dtls12/engine.rs @@ -10,9 +10,10 @@ 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, Output, SeededRng}; +use crate::{Config, Error, InternalError, Output, SeededRng}; const MAX_DEFRAGMENT_PACKETS: usize = 50; @@ -218,7 +219,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 { @@ -382,7 +383,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 +403,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)); } } @@ -644,7 +645,7 @@ impl Engine { &mut self, wanted: MessageType, defragment_buffer: &mut Buf, - ) -> Result, Error> { + ) -> Result, InternalError> { if !self.has_complete_handshake(wanted) { return Ok(None); } @@ -729,10 +730,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 +802,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 +819,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: bounded_error_len(explicit_nonce_len), + suite, + }, + )); } }; @@ -922,8 +925,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 +1043,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 +1061,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) } } @@ -1130,7 +1134,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; @@ -1190,7 +1194,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 +1214,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 +1281,9 @@ 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 { + description, + })); } return Ok(None); @@ -1301,10 +1308,9 @@ 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 { + description, + })); } } 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..48e22f1a 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!( - "Unsupported DTLS version from client: {:?}", - ch.client_version - ))); + 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(), - )); + return Err( + Error::SecurityError(crate::SecurityError::UnsupportedClientCompression).into(), + ); } trace!( @@ -400,9 +403,10 @@ impl State { } let Some(cs) = selected else { - return Err(Error::SecurityError( - "No mutually acceptable cipher suite".to_string(), - )); + return Err((Error::SecurityError( + crate::SecurityError::NoMutuallyAcceptableCipherSuite, + )) + .into()); }; server.engine.set_cipher_suite(cs); @@ -419,7 +423,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 +433,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); @@ -450,8 +456,9 @@ impl State { // EMS is mandatory if !client_offers_ems { return Err(Error::SecurityError( - "Extended Master Secret not negotiated".to_string(), - )); + crate::SecurityError::ExtendedMasterSecretNotNegotiated, + ) + .into()); } // Select SRTP profile according to server priority: AES256GCM, AES128GCM, then SHA1 @@ -483,7 +490,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 @@ -509,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() { @@ -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,21 +538,20 @@ 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 - .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(); @@ -566,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) } })?; @@ -624,7 +625,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 +646,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,17 +665,16 @@ 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 .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() { @@ -688,7 +688,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 +735,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, @@ -750,10 +750,9 @@ 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 @@ -761,8 +760,9 @@ impl State { ExchangeKeys::Psk(keys) => keys.identity_range.clone(), _ => { return Err(Error::UnexpectedMessage( - "ECDHE ClientKeyExchange in PSK path".to_string(), - )); + crate::UnexpectedMessageError::EcdheClientKeyExchangeInPskPath, + ) + .into()); } }; @@ -779,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 { @@ -796,15 +796,16 @@ 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(), - )); + crate::UnexpectedMessageError::PskClientKeyExchangeInEcdhePath, + ) + .into()); } }; @@ -819,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); } @@ -844,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::ExtendedMasterSecretSessionHashMissing, + ))?; let mut out = server.engine.pop_buffer(); let mut scratch = server.engine.pop_buffer(); @@ -856,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 @@ -870,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); @@ -888,7 +886,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 @@ -922,8 +920,9 @@ impl State { if server.client_certificates.is_empty() { return Err(Error::CertificateError( - "CertificateVerify received but no client certificate".to_string(), - )); + crate::CertificateError::NoClientCertificateForVerification, + ) + .into()); } // Create temp DigitallySigned for verification @@ -941,16 +940,14 @@ 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"); 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 +966,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 +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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::ClientFinishedVerificationFailed, + )) + .into()); } // Invariant: full PSK handshakes always set psk_valid in @@ -1030,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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::ClientFinishedVerificationFailed, + )) + .into()); } trace!("Client Finished verified successfully"); @@ -1040,7 +1039,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 +1055,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 +1111,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. @@ -1155,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) } @@ -1195,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, @@ -1226,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(); @@ -1242,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={}", @@ -1268,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. @@ -1295,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 7c87632d..092be3bb 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(); @@ -387,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(); @@ -437,7 +443,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 +486,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 +501,10 @@ 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( + crate::SecurityError::HrrSelectedDisallowedCipherSuite, + )) + .into()); } client.engine.set_cipher_suite(server_hello.cipher_suite); @@ -514,7 +521,9 @@ impl State { } } if !hrr_version_ok { - return Err(Error::SecurityError("HRR did not select DTLS 1.3".into())); + return Err( + (Error::SecurityError(crate::SecurityError::HrrDidNotSelectDtls13)).into(), + ); } // Replace transcript with message_hash per RFC 8446 Section 4.4.1. @@ -536,15 +545,17 @@ 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(), - )); + 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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::ServerHelloCompressionMustBeNull, + )) + .into()); } debug!( @@ -555,16 +566,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( + 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()); } client.engine.set_cipher_suite(cs); @@ -575,7 +587,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,36 +618,38 @@ impl State { } if !supported_version_ok { - return Err(Error::SecurityError( - "Server did not negotiate DTLS 1.3 via supported_versions".to_string(), - )); + 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(), - )); + 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() - ))); + return Err( + Error::SecurityError(crate::SecurityError::ServerKeyShareGroupMismatch { + expected: key_exchange.group(), + actual: server_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) = @@ -665,7 +679,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 +697,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 +713,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 +752,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 +766,17 @@ impl State { }; if !certificate.context_range.is_empty() { - return Err(Error::SecurityError( - "Server certificate context must be empty".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(), - )); + return Err((Error::CertificateError( + crate::CertificateError::NoServerCertificateReceived, + )) + .into()); } debug!( @@ -792,7 +809,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 +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 - ))); + return Err( + Error::SecurityError(crate::SecurityError::SignatureSchemeNotOffered(scheme)) + .into(), + ); } // Build the signed content per RFC 8446 Section 4.4.3: @@ -836,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)?; @@ -864,13 +884,16 @@ 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 - .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 @@ -896,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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::ServerFinishedVerificationFailed, + )) + .into()); } trace!("Server Finished verified successfully"); @@ -908,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)?; @@ -931,7 +955,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 +999,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 +1015,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 @@ -1002,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; @@ -1047,7 +1075,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 +1139,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) { @@ -1339,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) { @@ -1353,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, + }, + )); } }; @@ -1391,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)) @@ -1408,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 - ))), } } @@ -1422,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(()) @@ -1449,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(); @@ -1468,7 +1501,7 @@ fn parse_certificate_request(cr_data: &[u8], base_offset: usize) -> Result &[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"); @@ -1564,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(); @@ -1583,6 +1661,40 @@ 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] + 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/dtls13/engine.rs b/src/dtls13/engine.rs index 39ef14cc..d67ccc34 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 { @@ -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)); } } @@ -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); } @@ -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) } @@ -2102,7 +2097,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; @@ -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,11 @@ 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 { + description, + })) + } None => Ok(None), } } @@ -2453,10 +2450,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 +2463,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/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..4711c8d4 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(); @@ -414,8 +420,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(), - )); + crate::SecurityError::ClientHelloLegacyVersionNotDtls12, + ) + .into()); } // Validate null compression is offered @@ -424,8 +431,9 @@ impl State { .contains(&CompressionMethod::Null); if !has_null_compression { return Err(Error::SecurityError( - "ClientHello must offer null compression".to_string(), - )); + crate::SecurityError::ClientHelloMustOfferNullCompression, + ) + .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,14 +486,16 @@ 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()) + Error::SecurityError(crate::SecurityError::InvalidCookieInClientHello) })?); } _ => {} @@ -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( - "ClientHello must include DTLS 1.3 in supported_versions".to_string(), - )); + crate::SecurityError::ClientHelloMissingDtls13SupportedVersions, + ) + .into()); } // Select cipher suite: first from client's list that is in our provider @@ -504,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; @@ -555,8 +571,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(), - )); + crate::SecurityError::InvalidCookieInClientHello, + ) + .into()); } debug!("Cookie validated successfully"); } else { @@ -603,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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::InvalidCookieInClientHello, + )) + .into()); } } @@ -629,8 +647,9 @@ impl State { // Need HRR for key exchange if server.hello_retry { return Err(Error::SecurityError( - "Cannot send second HelloRetryRequest".to_string(), - )); + crate::SecurityError::CannotSendSecondHelloRetryRequest, + ) + .into()); } debug!( @@ -663,24 +682,22 @@ impl State { Ok(Self::AwaitClientHello) } else { - Err(Error::SecurityError( - "No common key exchange group".to_string(), - )) + 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(); @@ -694,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); @@ -746,7 +763,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(); @@ -794,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)?; @@ -821,7 +838,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 +856,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 +870,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 +882,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,12 +898,16 @@ 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(|| { - 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; @@ -900,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)?; @@ -922,7 +943,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 +1003,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 +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 - ))); + return Err( + Error::SecurityError(crate::SecurityError::SignatureSchemeNotOffered(scheme)) + .into(), + ); } // Build the signed content per RFC 8446 Section 4.4.3: @@ -1026,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)?; @@ -1054,13 +1078,16 @@ 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 - .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 @@ -1086,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(), - )); + return Err((Error::SecurityError( + crate::SecurityError::ClientFinishedVerificationFailed, + )) + .into()); } trace!("Client Finished verified successfully"); @@ -1129,7 +1157,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 +1221,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..5856c569 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,32 +1,47 @@ //! 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; + +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. 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,67 +60,1270 @@ 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, - /// Parser requested more data - #[doc(hidden)] +} + +/// Fine-grained reason for an [`Error::UnexpectedMessage`]. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +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, +} + +/// Fine-grained reason for an [`Error::InvalidState`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +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, + /// Extended master secret was negotiated, but the session hash was not captured. + ExtendedMasterSecretSessionHashMissing, +} + +/// Fine-grained reason for an [`Error::CryptoError`]. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +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, + }, + /// 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. + UnsupportedEcCurve, + /// Certificate parsing failed during a crypto operation. + CertificateParseFailed, + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// Values greater than `u16::MAX` are reported as `u16::MAX`. + actual: u16, + }, + /// A nonce was invalid for the selected cipher. + InvalidNonce, + /// A ciphertext was shorter than the selected AEAD permits. + CiphertextTooShort { + /// The minimum accepted ciphertext length in bytes. + minimum: u8, + /// The actual ciphertext length in bytes. + actual: u8, + }, + /// 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. + /// + /// Values greater than `u16::MAX` are reported as `u16::MAX`. + len: u16, + /// 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 cryptographic operation that can fail. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +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] +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, + /// A certificate had an invalid subject public key. + InvalidSubjectPublicKey, +} + +/// Fine-grained reason for an [`Error::SecurityError`]. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +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 expected group. + ServerKeyShareGroupMismatch { + /// The expected key exchange group. + expected: 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 description. + description: u8, + }, +} + +/// Fine-grained reason for an [`Error::PskError`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +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, +} + +/// Fine-grained reason for an [`Error::Timeout`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum TimeoutError { + /// Timeout while waiting for an auto client hybrid ClientHello to resolve. + HybridClientHello, + /// Timeout while connecting. + Connect, + /// Timeout while handshaking. + Handshake, +} + +/// Fine-grained reason for an [`Error::ConfigError`]. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConfigError { + /// The configured MTU is smaller than dimpl permits. + MtuTooSmall { + /// The configured MTU. + mtu: u16, + /// The minimum accepted MTU. + minimum: u16, + }, + /// 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), +} + +/// Fine-grained reason for crypto provider validation failure. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +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, + }, + /// 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 incorrect output. + HmacIncorrect, +} + +#[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(err) => { + debug!("Discarding packet: {err}"); + None + } + Self::Fatal(err) => Some(err), + } } } -impl<'a> From>> for Error { +impl From for InternalError { + fn from(value: Error) -> Self { + Self::Fatal(value) + } +} + +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 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 std::error::Error for Error {} -impl std::fmt::Display for Error { - 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 { - 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), + InternalError::Transient(err) => err.fmt(f), + InternalError::Fatal(err) => err.fmt(f), + } + } +} + +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), + TransientError::TooManyRecords => write!(f, "too many records in packet"), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + 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") + } + } + } +} + +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::ExtendedMasterSecretSessionHashMissing => { + write!( + f, + "extended master secret negotiated but session hash not captured" + ) + } + } + } +} + +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::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"), + 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::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"), + } + } +} + +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 => write!(f, "unsupported EC curve"), + Self::InvalidSubjectPublicKey => { + write!(f, "invalid EC subject_public_key bitstring") + } + } + } +} + +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 { expected, actual } => { + write!( + f, + "server key_share group mismatch: expected {expected:?}, 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 { description } => { + write!(f, "received fatal alert: description={description}") + } + } + } +} + +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"), + } + } +} + +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"), + } + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + 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}"), + } + } +} + +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::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::HmacIncorrect => { + write!(f, "HMAC provider produced incorrect result for HMAC-SHA256") } - 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..e1680c0d 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) //! @@ -110,7 +127,8 @@ //! // Deliver plaintext to application //! } //! Output::CloseNotify => { -//! // Peer initiated graceful shutdown +//! // Peer initiated graceful shutdown — leave the event loop +//! return Ok(()); //! } //! _ => {} //! } @@ -138,7 +156,7 @@ //! # } //! ``` //! -//! ## Example (PSK client) +//! # Example (PSK client) //! //! ```rust,no_run //! use std::sync::Arc; @@ -227,7 +245,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}; @@ -632,7 +654,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:?}"), } } 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) {