1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Crate-wide error type.
use core::fmt;
/// Convenience `Result` alias used throughout the crate.
pub type Result<T> = core::result::Result<T, Error>;
/// Errors produced by `puressh`.
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
/// A wire-format value was malformed or truncated.
Format(&'static str),
/// Negotiation failed: no common algorithm between peers.
NoCommonAlgorithm(&'static str),
/// A protocol-level invariant was violated by the peer.
Protocol(&'static str),
/// MAC verification failed on an inbound packet.
BadMac,
/// AEAD authentication tag did not match.
BadTag,
/// Decryption produced an invalid plaintext (e.g. bad padding).
BadPadding,
/// Host-key signature failed to verify.
BadSignature,
/// Peer's host key was rejected by policy / known-hosts.
HostKeyRejected,
/// Authentication was refused or exhausted.
AuthFailed,
/// Requested channel operation is not valid in the current state.
BadChannelState,
/// A feature was requested that this build does not support.
Unsupported(&'static str),
/// I/O error (only with `std`).
#[cfg(feature = "std")]
Io(std::io::Error),
/// Error originating from the `purecrypto` backend.
Crypto(&'static str),
/// A configuration value supplied to the library was internally
/// inconsistent or insufficient (e.g. a `HostKeyPolicy::KnownHosts`
/// without a connect target). Distinct from `Protocol` so callers can
/// tell "I configured this wrong" from "the peer is misbehaving".
Config(&'static str),
/// An OpenSSH certificate's validity window has already ended
/// (`now >= valid_before`).
CertExpired,
/// An OpenSSH certificate is not yet valid (`now < valid_after`).
CertNotYetValid,
/// The requested principal (host name or login user) is not listed in
/// the certificate's `valid principals`.
CertPrincipalMismatch,
/// The certificate's `type` field does not match the context it is being
/// used in (a host cert presented for user auth, or vice versa).
CertTypeMismatch,
/// The certificate carries a critical option this build does not
/// understand; per the spec such a certificate MUST be rejected.
CertUnknownCriticalOption,
/// The CA signature over the certificate failed to verify, or the CA
/// signature algorithm is not in the permitted `CASignatureAlgorithms`
/// set.
CertBadCaSignature,
/// Negotiation failed: no algorithm of `category` (`"kex algorithm"`,
/// `"host-key algorithm"`, `"cipher c2s"`, `"mac s2c"`, …) is on both
/// sides' lists. Carries both offers (comma-joined), for messages like
/// OpenSSH's ([`Error::openssh_negotiation_message`]).
#[cfg(feature = "alloc")]
NoMatch {
/// What failed to match.
category: &'static str,
/// The client's offer.
client: alloc::string::String,
/// The server's offer.
server: alloc::string::String,
},
/// The peer ended the connection with `SSH_MSG_DISCONNECT`.
#[cfg(feature = "alloc")]
Disconnected {
/// Reason code (RFC 4253 §11.1), e.g. 11 for "by application".
reason: u32,
/// The peer's description, control characters replaced.
description: alloc::string::String,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Format(s) => write!(f, "ssh wire format: {s}"),
Error::NoCommonAlgorithm(s) => write!(f, "no common {s}"),
Error::Protocol(s) => write!(f, "protocol error: {s}"),
Error::BadMac => f.write_str("bad MAC"),
Error::BadTag => f.write_str("bad AEAD tag"),
Error::BadPadding => f.write_str("bad padding"),
Error::BadSignature => f.write_str("bad signature"),
Error::HostKeyRejected => f.write_str("host key rejected"),
Error::AuthFailed => f.write_str("authentication failed"),
Error::BadChannelState => f.write_str("bad channel state"),
Error::Unsupported(s) => write!(f, "unsupported: {s}"),
#[cfg(feature = "std")]
Error::Io(e) => write!(f, "io: {e}"),
Error::Crypto(s) => write!(f, "crypto: {s}"),
Error::Config(s) => write!(f, "config error: {s}"),
Error::CertExpired => f.write_str("certificate has expired"),
Error::CertNotYetValid => f.write_str("certificate is not yet valid"),
Error::CertPrincipalMismatch => f.write_str("certificate principal not authorized"),
Error::CertTypeMismatch => f.write_str("certificate type mismatch"),
Error::CertUnknownCriticalOption => {
f.write_str("certificate has an unknown critical option")
}
Error::CertBadCaSignature => f.write_str("certificate CA signature invalid"),
#[cfg(feature = "alloc")]
Error::NoMatch {
category,
client,
server,
} => write!(
f,
"no common {category} (client offer: {client}; server offer: {server})"
),
#[cfg(feature = "alloc")]
Error::Disconnected {
reason,
description,
} => write!(f, "disconnected by peer: {reason}: {description}"),
}
}
}
#[cfg(feature = "alloc")]
impl Error {
/// A negotiation failure as OpenSSH words it, with the *peer's* offer
/// (the server's for a client, `peer_is_server`; the client's for a
/// server): `no matching key exchange method found. Their offer: …`.
/// `None` for any other error.
pub fn openssh_negotiation_message(
&self,
peer_is_server: bool,
) -> Option<alloc::string::String> {
let Error::NoMatch {
category,
client,
server,
} = self
else {
return None;
};
let what = match category.split(' ').next().unwrap_or("") {
"kex" => "key exchange method",
"host-key" => "host key type",
"cipher" => "cipher",
"mac" => "MAC",
"compression" => "compression method",
_ => category,
};
let offer = if peer_is_server { server } else { client };
Some(alloc::format!(
"no matching {what} found. Their offer: {offer}"
))
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}