From 6d9f33fb871346d02a4d8add2592b85506c08299 Mon Sep 17 00:00:00 2001
From: Ruben Nijveld <ruben@tweedegolf.com>
Date: Thu, 18 Apr 2024 14:29:09 +0200
Subject: [PATCH] Remove thiserror from dependencies and replace with
 handwritten implementation

---
 Cargo.lock                        | 23 ----------
 Cargo.toml                        |  3 +-
 ntp-proto/Cargo.toml              |  1 -
 ntp-proto/src/nts_record.rs       | 74 +++++++++++++++++++++++--------
 ntp-proto/src/packet/crypto.rs    | 25 ++++++++---
 ntp-proto/src/peer.rs             | 37 +++++++++++++---
 ntp-proto/src/server.rs           | 27 ++++++++---
 ntpd/Cargo.toml                   |  1 -
 ntpd/src/daemon/config/mod.rs     | 33 +++++++++++---
 ntpd/src/daemon/config/subnet.rs  | 31 ++++++++++---
 ntpd/src/daemon/spawn/dummy.rs    | 12 ++++-
 ntpd/src/daemon/spawn/nts.rs      | 23 ++++++++--
 ntpd/src/daemon/spawn/nts_pool.rs | 23 ++++++++--
 ntpd/src/daemon/spawn/pool.rs     | 12 ++++-
 ntpd/src/daemon/spawn/standard.rs | 23 ++++++++--
 nts-pool-ke/Cargo.toml            |  3 +-
 nts-pool-ke/src/config.rs         | 33 +++++++++++---
 17 files changed, 285 insertions(+), 99 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 89cac78b5..53836671f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -360,7 +360,6 @@ dependencies = [
  "rustls-pemfile",
  "serde",
  "serde_test",
- "thiserror",
  "tracing",
  "zeroize",
 ]
@@ -379,7 +378,6 @@ dependencies = [
  "rustls-pemfile",
  "serde",
  "serde_json",
- "thiserror",
  "timestamped-socket",
  "tokio",
  "tokio-rustls",
@@ -397,7 +395,6 @@ dependencies = [
  "rustls-native-certs",
  "rustls-pemfile",
  "serde",
- "thiserror",
  "tokio",
  "tokio-rustls",
  "toml",
@@ -715,26 +712,6 @@ dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "thiserror"
-version = "1.0.58"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.58"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "thread_local"
 version = "1.1.7"
diff --git a/Cargo.toml b/Cargo.toml
index 2f4fea0ab..f46c0cea1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,10 +39,9 @@ serde_json = "1.0"
 serde_test = "1.0.176"
 rand = "0.8.0"
 arbitrary = { version = "1.0" }
-thiserror = "1.0.10"
 libc = "0.2.145"
 tokio = "1.32"
-toml = ">=0.5.0,<0.9.0"
+toml = { version = ">=0.5.0,<0.9.0", default-features = false, features = ["parse"] }
 async-trait = "0.1.22"
 timestamped-socket = "0.2.1"
 clock-steering = "0.2.0"
diff --git a/ntp-proto/Cargo.toml b/ntp-proto/Cargo.toml
index 4ae7d0c62..3f6dcdbb8 100644
--- a/ntp-proto/Cargo.toml
+++ b/ntp-proto/Cargo.toml
@@ -28,7 +28,6 @@ tracing.workspace = true
 serde.workspace = true
 arbitrary = { workspace = true, optional = true }
 rustls.workspace = true
-thiserror.workspace = true
 aead.workspace = true
 aes-siv.workspace = true
 zeroize.workspace = true
diff --git a/ntp-proto/src/nts_record.rs b/ntp-proto/src/nts_record.rs
index 9dd3dc1d0..cb749235d 100644
--- a/ntp-proto/src/nts_record.rs
+++ b/ntp-proto/src/nts_record.rs
@@ -1,4 +1,5 @@
 use std::{
+    fmt::Display,
     io::{Read, Write},
     ops::ControlFlow,
     sync::Arc,
@@ -642,38 +643,75 @@ impl NtsRecordDecoder {
     }
 }
 
-#[derive(Debug, thiserror::Error)]
+#[derive(Debug)]
 pub enum KeyExchangeError {
-    #[error("Unrecognized record is marked as critical")]
     UnrecognizedCriticalRecord,
-    #[error("Remote: Bad request")]
     BadRequest,
-    #[error("Remote: Internal server error")]
     InternalServerError,
-    #[error("Remote: Error with unknown code {0}")]
     UnknownErrorCode(u16),
-    #[error("The server response is invalid")]
     BadResponse,
-    #[error("No continuation protocol supported by both us and server")]
     NoValidProtocol,
-    #[error("No encryption algorithm supported by both us and server")]
     NoValidAlgorithm,
-    #[error("The length of a fixed key does not match the algorithm used")]
     InvalidFixedKeyLength,
-    #[error("Missing cookies")]
     NoCookies,
-    #[error("{0}")]
-    Io(#[from] std::io::Error),
-    #[error("{0}")]
-    Tls(#[from] rustls::Error),
-    #[error("{0}")]
+    Io(std::io::Error),
+    Tls(rustls::Error),
     Certificate(rustls::Error),
-    #[error("{0}")]
-    DnsName(#[from] rustls::pki_types::InvalidDnsNameError),
-    #[error("Incomplete response")]
+    DnsName(rustls::pki_types::InvalidDnsNameError),
     IncompleteResponse,
 }
 
+impl Display for KeyExchangeError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::UnrecognizedCriticalRecord => {
+                write!(f, "Unrecognized record is marked as critical")
+            }
+            Self::BadRequest => write!(f, "Remote: Bad request"),
+            Self::InternalServerError => write!(f, "Remote: Internal server error"),
+            Self::UnknownErrorCode(e) => write!(f, "Remote: Error with unknown code {e}"),
+            Self::BadResponse => write!(f, "The server response is invalid"),
+            Self::NoValidProtocol => write!(
+                f,
+                "No continuation protocol supported by both us and server"
+            ),
+            Self::NoValidAlgorithm => {
+                write!(f, "No encryption algorithm supported by both us and server")
+            }
+            Self::InvalidFixedKeyLength => write!(
+                f,
+                "The length of a fixed key does not match the algorithm used"
+            ),
+            Self::NoCookies => write!(f, "Missing cookies"),
+            Self::Io(e) => write!(f, "{e}"),
+            Self::Tls(e) => write!(f, "{e}"),
+            Self::Certificate(e) => write!(f, "{e}"),
+            Self::DnsName(e) => write!(f, "{e}"),
+            Self::IncompleteResponse => write!(f, "Incomplete response"),
+        }
+    }
+}
+
+impl From<std::io::Error> for KeyExchangeError {
+    fn from(value: std::io::Error) -> Self {
+        Self::Io(value)
+    }
+}
+
+impl From<rustls::Error> for KeyExchangeError {
+    fn from(value: rustls::Error) -> Self {
+        Self::Tls(value)
+    }
+}
+
+impl From<rustls::pki_types::InvalidDnsNameError> for KeyExchangeError {
+    fn from(value: rustls::pki_types::InvalidDnsNameError) -> Self {
+        Self::DnsName(value)
+    }
+}
+
+impl std::error::Error for KeyExchangeError {}
+
 impl KeyExchangeError {
     pub(crate) fn from_error_code(error_code: u16) -> Self {
         match error_code {
diff --git a/ntp-proto/src/packet/crypto.rs b/ntp-proto/src/packet/crypto.rs
index d0da64171..6eb52f2ff 100644
--- a/ntp-proto/src/packet/crypto.rs
+++ b/ntp-proto/src/packet/crypto.rs
@@ -1,20 +1,35 @@
+use std::fmt::Display;
+
 use aes_siv::{siv::Aes128Siv, siv::Aes256Siv, Key, KeyInit};
 use rand::Rng;
-use tracing::error;
 use zeroize::{Zeroize, ZeroizeOnDrop};
 
 use crate::keyset::DecodedServerCookie;
 
 use super::extension_fields::ExtensionField;
 
-#[derive(Debug, thiserror::Error)]
-#[error("Could not decrypt ciphertext")]
+#[derive(Debug)]
 pub struct DecryptError;
 
-#[derive(Debug, thiserror::Error)]
-#[error("Invalid key")]
+impl Display for DecryptError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Could not decrypt ciphertext")
+    }
+}
+
+impl std::error::Error for DecryptError {}
+
+#[derive(Debug)]
 pub struct KeyError;
 
+impl Display for KeyError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Invalid key")
+    }
+}
+
+impl std::error::Error for KeyError {}
+
 struct Buffer<'a> {
     buffer: &'a mut [u8],
     valid: usize,
diff --git a/ntp-proto/src/peer.rs b/ntp-proto/src/peer.rs
index d0a875bfd..b77b8818f 100644
--- a/ntp-proto/src/peer.rs
+++ b/ntp-proto/src/peer.rs
@@ -13,6 +13,7 @@ use crate::{
 };
 use serde::{Deserialize, Serialize};
 use std::{
+    fmt::Display,
     io::Cursor,
     net::{IpAddr, SocketAddr},
 };
@@ -22,12 +23,21 @@ const MAX_STRATUM: u8 = 16;
 const POLL_WINDOW: std::time::Duration = std::time::Duration::from_secs(5);
 const STARTUP_TRIES_THRESHOLD: usize = 3;
 
-#[derive(Debug, thiserror::Error)]
+#[derive(Debug)]
 pub enum NtsError {
-    #[error("Ran out of nts cookies")]
     OutOfCookies,
 }
 
+impl Display for NtsError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::OutOfCookies => write!(f, "Ran out of NTS cookies"),
+        }
+    }
+}
+
+impl std::error::Error for NtsError {}
+
 pub struct PeerNtsData {
     pub(crate) cookies: CookieStash,
     // Note: we use Box<dyn Cipher> to support the use
@@ -323,14 +333,29 @@ pub enum Update {
     NewMeasurement(PeerSnapshot, Measurement),
 }
 
-#[derive(Debug, thiserror::Error)]
+#[derive(Debug)]
 pub enum PollError {
-    #[error("{0}")]
-    Io(#[from] std::io::Error),
-    #[error("peer unreachable")]
+    Io(std::io::Error),
     PeerUnreachable,
 }
 
+impl Display for PollError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Io(e) => write!(f, "{e}"),
+            Self::PeerUnreachable => write!(f, "peer unreachable"),
+        }
+    }
+}
+
+impl std::error::Error for PollError {}
+
+impl From<std::io::Error> for PollError {
+    fn from(value: std::io::Error) -> Self {
+        Self::Io(value)
+    }
+}
+
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 pub enum ProtocolVersion {
     V4,
diff --git a/ntp-proto/src/server.rs b/ntp-proto/src/server.rs
index dbabeb3cf..349c71497 100644
--- a/ntp-proto/src/server.rs
+++ b/ntp-proto/src/server.rs
@@ -1,5 +1,6 @@
 use std::{
     collections::hash_map::RandomState,
+    fmt::Display,
     io::Cursor,
     net::{AddrParseError, IpAddr},
     sync::Arc,
@@ -7,7 +8,6 @@ use std::{
 };
 
 use serde::{de, Deserialize, Deserializer};
-use thiserror::Error;
 
 use crate::{
     ipfilter::IpFilter, KeySet, NoCipher, NtpClock, NtpPacket, NtpTimestamp, PacketParsingError,
@@ -341,16 +341,31 @@ pub struct IpSubnet {
     pub mask: u8,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Error)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum SubnetParseError {
-    #[error("Invalid subnet syntax")]
     Subnet,
-    #[error("{0} in subnet")]
-    Ip(#[from] AddrParseError),
-    #[error("Invalid subnet mask")]
+    Ip(AddrParseError),
     Mask,
 }
 
+impl std::error::Error for SubnetParseError {}
+
+impl Display for SubnetParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Subnet => write!(f, "Invalid subnet syntax"),
+            Self::Ip(e) => write!(f, "{e} in subnet"),
+            Self::Mask => write!(f, "Invalid subnet mask"),
+        }
+    }
+}
+
+impl From<AddrParseError> for SubnetParseError {
+    fn from(value: AddrParseError) -> Self {
+        Self::Ip(value)
+    }
+}
+
 impl std::str::FromStr for IpSubnet {
     type Err = SubnetParseError;
 
diff --git a/ntpd/Cargo.toml b/ntpd/Cargo.toml
index 32352a002..cf1a1cb33 100644
--- a/ntpd/Cargo.toml
+++ b/ntpd/Cargo.toml
@@ -18,7 +18,6 @@ tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "io-std",
 tracing.workspace = true
 tracing-subscriber.workspace = true
 toml.workspace = true
-thiserror.workspace = true
 rand.workspace = true
 libc.workspace = true
 async-trait.workspace = true
diff --git a/ntpd/src/daemon/config/mod.rs b/ntpd/src/daemon/config/mod.rs
index 4295bdb2b..01a1a682c 100644
--- a/ntpd/src/daemon/config/mod.rs
+++ b/ntpd/src/daemon/config/mod.rs
@@ -8,13 +8,13 @@ pub use peer::*;
 use serde::{Deserialize, Deserializer};
 pub use server::*;
 use std::{
+    fmt::Display,
     io::ErrorKind,
     net::SocketAddr,
     os::unix::fs::PermissionsExt,
     path::{Path, PathBuf},
     str::FromStr,
 };
-use thiserror::Error;
 use timestamped_socket::interface::InterfaceName;
 use tokio::{fs::read_to_string, io};
 use tracing::{info, warn};
@@ -454,12 +454,33 @@ impl Config {
     }
 }
 
-#[derive(Error, Debug)]
+#[derive(Debug)]
 pub enum ConfigError {
-    #[error("io error while reading config: {0}")]
-    Io(#[from] io::Error),
-    #[error("config toml parsing error: {0}")]
-    Toml(#[from] toml::de::Error),
+    Io(io::Error),
+    Toml(toml::de::Error),
+}
+
+impl std::error::Error for ConfigError {}
+
+impl Display for ConfigError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Io(e) => write!(f, "io error while reading config: {e}"),
+            Self::Toml(e) => write!(f, "config toml parsing error: {e}"),
+        }
+    }
+}
+
+impl From<io::Error> for ConfigError {
+    fn from(value: io::Error) -> Self {
+        Self::Io(value)
+    }
+}
+
+impl From<toml::de::Error> for ConfigError {
+    fn from(value: toml::de::Error) -> Self {
+        Self::Toml(value)
+    }
 }
 
 #[cfg(test)]
diff --git a/ntpd/src/daemon/config/subnet.rs b/ntpd/src/daemon/config/subnet.rs
index e35c83132..bfc789f36 100644
--- a/ntpd/src/daemon/config/subnet.rs
+++ b/ntpd/src/daemon/config/subnet.rs
@@ -1,6 +1,8 @@
 use serde::{de, Deserialize, Deserializer};
-use std::net::{AddrParseError, IpAddr};
-use thiserror::Error;
+use std::{
+    fmt::Display,
+    net::{AddrParseError, IpAddr},
+};
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct IpSubnet {
@@ -8,16 +10,31 @@ pub struct IpSubnet {
     pub mask: u8,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Error)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum SubnetParseError {
-    #[error("Invalid subnet syntax")]
     Subnet,
-    #[error("{0} in subnet")]
-    Ip(#[from] AddrParseError),
-    #[error("Invalid subnet mask")]
+    Ip(AddrParseError),
     Mask,
 }
 
+impl std::error::Error for SubnetParseError {}
+
+impl Display for SubnetParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Subnet => write!(f, "Invalid subnet syntax"),
+            Self::Ip(e) => write!(f, "{e} in subnet"),
+            Self::Mask => write!(f, "Invalid subnet mask"),
+        }
+    }
+}
+
+impl From<AddrParseError> for SubnetParseError {
+    fn from(value: AddrParseError) -> Self {
+        Self::Ip(value)
+    }
+}
+
 impl std::str::FromStr for IpSubnet {
     type Err = SubnetParseError;
 
diff --git a/ntpd/src/daemon/spawn/dummy.rs b/ntpd/src/daemon/spawn/dummy.rs
index 56c1548b2..37b313942 100644
--- a/ntpd/src/daemon/spawn/dummy.rs
+++ b/ntpd/src/daemon/spawn/dummy.rs
@@ -1,4 +1,4 @@
-use std::net::SocketAddr;
+use std::{fmt::Display, net::SocketAddr};
 
 use super::{
     BasicSpawner, PeerCreateParameters, PeerRemovedEvent, SpawnAction, SpawnEvent, SpawnerId,
@@ -11,9 +11,17 @@ pub struct DummySpawner {
     to_activate: isize,
 }
 
-#[derive(Debug, thiserror::Error)]
+#[derive(Debug)]
 pub enum DummySpawnerError {}
 
+impl Display for DummySpawnerError {
+    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        unreachable!()
+    }
+}
+
+impl std::error::Error for DummySpawnerError {}
+
 impl DummySpawner {
     pub fn new(to_spawn: Vec<PeerCreateParameters>, keep_active: usize) -> DummySpawner {
         DummySpawner {
diff --git a/ntpd/src/daemon/spawn/nts.rs b/ntpd/src/daemon/spawn/nts.rs
index 9e2fb2280..ae1b97a19 100644
--- a/ntpd/src/daemon/spawn/nts.rs
+++ b/ntpd/src/daemon/spawn/nts.rs
@@ -1,7 +1,7 @@
+use std::fmt::Display;
 use std::net::SocketAddr;
 use std::ops::Deref;
 
-use thiserror::Error;
 use tokio::sync::mpsc;
 use tracing::warn;
 
@@ -15,10 +15,25 @@ pub struct NtsSpawner {
     has_spawned: bool,
 }
 
-#[derive(Error, Debug)]
+#[derive(Debug)]
 pub enum NtsSpawnError {
-    #[error("Channel send error: {0}")]
-    SendError(#[from] mpsc::error::SendError<SpawnEvent>),
+    SendError(mpsc::error::SendError<SpawnEvent>),
+}
+
+impl std::error::Error for NtsSpawnError {}
+
+impl Display for NtsSpawnError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::SendError(e) => write!(f, "Channel send error: {e}"),
+        }
+    }
+}
+
+impl From<mpsc::error::SendError<SpawnEvent>> for NtsSpawnError {
+    fn from(value: mpsc::error::SendError<SpawnEvent>) -> Self {
+        Self::SendError(value)
+    }
 }
 
 pub(super) async fn resolve_addr(address: (&str, u16)) -> Option<SocketAddr> {
diff --git a/ntpd/src/daemon/spawn/nts_pool.rs b/ntpd/src/daemon/spawn/nts_pool.rs
index 65a3b26b0..95b62fa27 100644
--- a/ntpd/src/daemon/spawn/nts_pool.rs
+++ b/ntpd/src/daemon/spawn/nts_pool.rs
@@ -1,6 +1,6 @@
+use std::fmt::Display;
 use std::ops::Deref;
 
-use thiserror::Error;
 use tokio::sync::mpsc;
 use tracing::warn;
 
@@ -23,10 +23,25 @@ pub struct NtsPoolSpawner {
     current_peers: Vec<PoolPeer>,
 }
 
-#[derive(Error, Debug)]
+#[derive(Debug)]
 pub enum NtsPoolSpawnError {
-    #[error("Channel send error: {0}")]
-    SendError(#[from] mpsc::error::SendError<SpawnEvent>),
+    SendError(mpsc::error::SendError<SpawnEvent>),
+}
+
+impl std::error::Error for NtsPoolSpawnError {}
+
+impl Display for NtsPoolSpawnError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::SendError(e) => write!(f, "Channel send error: {e}"),
+        }
+    }
+}
+
+impl From<mpsc::error::SendError<SpawnEvent>> for NtsPoolSpawnError {
+    fn from(value: mpsc::error::SendError<SpawnEvent>) -> Self {
+        Self::SendError(value)
+    }
 }
 
 impl NtsPoolSpawner {
diff --git a/ntpd/src/daemon/spawn/pool.rs b/ntpd/src/daemon/spawn/pool.rs
index f4bd659b7..7b8431946 100644
--- a/ntpd/src/daemon/spawn/pool.rs
+++ b/ntpd/src/daemon/spawn/pool.rs
@@ -1,7 +1,7 @@
+use std::fmt::Display;
 use std::{net::SocketAddr, ops::Deref};
 
 use ntp_proto::ProtocolVersion;
-use thiserror::Error;
 use tokio::sync::mpsc;
 use tracing::warn;
 
@@ -21,9 +21,17 @@ pub struct PoolSpawner {
     known_ips: Vec<SocketAddr>,
 }
 
-#[derive(Error, Debug)]
+#[derive(Debug)]
 pub enum PoolSpawnError {}
 
+impl Display for PoolSpawnError {
+    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        unreachable!()
+    }
+}
+
+impl std::error::Error for PoolSpawnError {}
+
 impl PoolSpawner {
     pub fn new(config: PoolPeerConfig) -> PoolSpawner {
         PoolSpawner {
diff --git a/ntpd/src/daemon/spawn/standard.rs b/ntpd/src/daemon/spawn/standard.rs
index 467a22e19..1a79390bf 100644
--- a/ntpd/src/daemon/spawn/standard.rs
+++ b/ntpd/src/daemon/spawn/standard.rs
@@ -1,7 +1,7 @@
+use std::fmt::Display;
 use std::{net::SocketAddr, ops::Deref};
 
 use ntp_proto::ProtocolVersion;
-use thiserror::Error;
 use tokio::sync::mpsc;
 use tracing::warn;
 
@@ -18,12 +18,27 @@ pub struct StandardSpawner {
     has_spawned: bool,
 }
 
-#[derive(Error, Debug)]
+#[derive(Debug)]
 pub enum StandardSpawnError {
-    #[error("Channel send error: {0}")]
-    SendError(#[from] mpsc::error::SendError<SpawnEvent>),
+    SendError(mpsc::error::SendError<SpawnEvent>),
 }
 
+impl Display for StandardSpawnError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::SendError(e) => write!(f, "Channel send error: {e}"),
+        }
+    }
+}
+
+impl From<mpsc::error::SendError<SpawnEvent>> for StandardSpawnError {
+    fn from(value: mpsc::error::SendError<SpawnEvent>) -> Self {
+        Self::SendError(value)
+    }
+}
+
+impl std::error::Error for StandardSpawnError {}
+
 impl StandardSpawner {
     pub fn new(config: StandardPeerConfig) -> StandardSpawner {
         StandardSpawner {
diff --git a/nts-pool-ke/Cargo.toml b/nts-pool-ke/Cargo.toml
index 8f9f69a3d..6b8de61ff 100644
--- a/nts-pool-ke/Cargo.toml
+++ b/nts-pool-ke/Cargo.toml
@@ -11,7 +11,7 @@ publish.workspace = true
 rust-version.workspace = true
 
 [dependencies]
-tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "io-std", "fs", "sync", "net", "macros", "time"] }
+tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "fs", "net", "macros", "time" ] }
 toml.workspace = true
 tracing.workspace = true
 tracing-subscriber = { version = "0.3.0", default-features = false, features = ["std", "fmt", "ansi"] }
@@ -19,7 +19,6 @@ rustls.workspace = true
 rustls-pemfile.workspace = true
 rustls-native-certs.workspace = true
 serde.workspace = true
-thiserror.workspace = true
 ntp-proto = { workspace = true, features = ["nts-pool"] }
 tokio-rustls.workspace = true
 
diff --git a/nts-pool-ke/src/config.rs b/nts-pool-ke/src/config.rs
index bac65bcdd..b8eb8034f 100644
--- a/nts-pool-ke/src/config.rs
+++ b/nts-pool-ke/src/config.rs
@@ -1,11 +1,11 @@
 use std::{
+    fmt::Display,
     net::SocketAddr,
     os::unix::fs::PermissionsExt,
     path::{Path, PathBuf},
 };
 
 use serde::Deserialize;
-use thiserror::Error;
 use tracing::{info, warn};
 
 #[derive(Deserialize, Debug)]
@@ -16,14 +16,35 @@ pub struct Config {
     pub observability: ObservabilityConfig,
 }
 
-#[derive(Error, Debug)]
+#[derive(Debug)]
 pub enum ConfigError {
-    #[error("io error while reading config: {0}")]
-    Io(#[from] std::io::Error),
-    #[error("config toml parsing error: {0}")]
-    Toml(#[from] toml::de::Error),
+    Io(std::io::Error),
+    Toml(toml::de::Error),
 }
 
+impl From<std::io::Error> for ConfigError {
+    fn from(value: std::io::Error) -> Self {
+        Self::Io(value)
+    }
+}
+
+impl From<toml::de::Error> for ConfigError {
+    fn from(value: toml::de::Error) -> Self {
+        Self::Toml(value)
+    }
+}
+
+impl Display for ConfigError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Io(e) => write!(f, "io error while reading config: {e}"),
+            Self::Toml(e) => write!(f, "config toml parsing error: {e}"),
+        }
+    }
+}
+
+impl std::error::Error for ConfigError {}
+
 impl Config {
     pub fn check(&self) -> bool {
         true