Skip to main content

melin_server_runtime/
client_auth.rs

1//! The client listener's verdict on a challenge response.
2//!
3//! The kernel TCP and DPDK transports each run the handshake over their
4//! own sockets, but decide it here, so the two cannot disagree about which
5//! keys may connect as a client. The DPDK path cannot be exercised without
6//! the hardware; this is how its decision is tested all the same.
7//!
8//! Public for a listener of the application's own that admits the node's
9//! client keys, an event publisher's subscribers say: calling
10//! [`verify_client`] applies the client listener's rule exactly, rather
11//! than a copy of it that has to be kept in step by hand.
12
13use std::fmt;
14
15use ed25519_dalek::{Signature, SignatureError, Verifier, VerifyingKey};
16use melin_app::auth::{AuthorizedKeys, ClientRole, RoleId};
17
18/// Largest ChallengeResponse frame payload a client listener accepts. The
19/// frame is 1 (tag) + 64 (signature) + 32 (public key) = 97 bytes; a longer
20/// length prefix is refused before its body is read.
21pub(crate) const MAX_AUTH_FRAME: usize = 256;
22
23/// Why the client listener refused a challenge response.
24///
25/// Exhaustive, so a caller matching on it is told by the compiler when a
26/// refusal reason is added (a breaking change, called out in the
27/// changelog).
28#[derive(Debug)]
29pub enum ClientAuthError {
30    /// The key is listed as `replication`, which authorizes streaming
31    /// between nodes and may not open a client connection (see
32    /// [`KeyRole::client`](melin_app::auth::KeyRole::client)).
33    ReplicationKeyRefused,
34    /// The key is not in the authorized keys file.
35    UnknownKey,
36    /// The listed bytes are not a valid Ed25519 public key.
37    InvalidKey(SignatureError),
38    /// The signature over the nonce does not verify.
39    BadSignature(SignatureError),
40}
41
42impl fmt::Display for ClientAuthError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::ReplicationKeyRefused => {
46                f.write_str("replication key may not connect as a client")
47            }
48            Self::UnknownKey => f.write_str("unknown public key"),
49            Self::InvalidKey(e) => write!(f, "invalid public key: {e}"),
50            Self::BadSignature(e) => write!(f, "signature verification failed: {e}"),
51        }
52    }
53}
54
55impl std::error::Error for ClientAuthError {
56    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57        match self {
58            Self::InvalidKey(e) | Self::BadSignature(e) => Some(e),
59            Self::ReplicationKeyRefused | Self::UnknownKey => None,
60        }
61    }
62}
63
64/// Decide a client's challenge response: the key must be listed, under a
65/// role that may connect as a client, and must have signed `nonce`.
66/// Returns the connection's role, as the runtime carries it: the operator,
67/// or an application role as its index in the role table.
68///
69/// `nonce` must be the one this listener sent the client for this
70/// connection, fresh from a cryptographic random source: a nonce reused
71/// across connections lets a recorded response be replayed.
72///
73/// The role is checked before the signature, so a refused key costs no
74/// verification; the answer the client sees is the same failure either
75/// way.
76///
77/// ```
78/// use base64::Engine;
79/// use ed25519_dalek::{Signer, SigningKey};
80/// use melin_app::auth::{AuthorizedKeys, NoRoles};
81/// use melin_server_runtime::client_auth::{ClientAuthError, verify_client};
82///
83/// let client = SigningKey::from_bytes(&[7; 32]);
84/// let public_key = client.verifying_key().to_bytes();
85/// let listed = base64::engine::general_purpose::STANDARD.encode(public_key);
86/// let keys = AuthorizedKeys::parse::<NoRoles>(&format!("operator {listed} ops\n")).unwrap();
87///
88/// // The listener sent this nonce; the client signed it.
89/// let nonce = [0x5A; 32];
90/// let signature = client.sign(&nonce).to_bytes();
91///
92/// let role = verify_client(&keys, &nonce, &public_key, &signature).unwrap();
93/// assert!(role.is_operator());
94///
95/// let impostor = SigningKey::from_bytes(&[8; 32]).sign(&nonce).to_bytes();
96/// assert!(matches!(
97///     verify_client(&keys, &nonce, &public_key, &impostor),
98///     Err(ClientAuthError::BadSignature(_))
99/// ));
100/// ```
101pub fn verify_client(
102    authorized_keys: &AuthorizedKeys,
103    nonce: &[u8; 32],
104    public_key: &[u8; 32],
105    signature: &[u8; 64],
106) -> Result<ClientRole<RoleId>, ClientAuthError> {
107    let role = authorized_keys
108        .lookup(public_key)
109        .ok_or(ClientAuthError::UnknownKey)?
110        .client()
111        .ok_or(ClientAuthError::ReplicationKeyRefused)?;
112    let verifying_key =
113        VerifyingKey::from_bytes(public_key).map_err(ClientAuthError::InvalidKey)?;
114    verifying_key
115        .verify(nonce, &Signature::from_bytes(signature))
116        .map_err(ClientAuthError::BadSignature)?;
117    Ok(role)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    use base64::Engine;
125    use ed25519_dalek::{Signer, SigningKey};
126    use melin_app::auth::KeyRole;
127
128    use crate::test_roles::desk_keys;
129
130    const NONCE: [u8; 32] = [0x5A; 32];
131
132    fn key() -> SigningKey {
133        SigningKey::from_bytes(&[0x11; 32])
134    }
135
136    fn keys_listing(role: &str, key: &SigningKey) -> AuthorizedKeys {
137        let public =
138            base64::engine::general_purpose::STANDARD.encode(key.verifying_key().to_bytes());
139        desk_keys(role, &public)
140    }
141
142    fn verify(
143        keys: &AuthorizedKeys,
144        signer: &SigningKey,
145    ) -> Result<ClientRole<RoleId>, ClientAuthError> {
146        let public_key = key().verifying_key().to_bytes();
147        let signature = signer.sign(&NONCE).to_bytes();
148        verify_client(keys, &NONCE, &public_key, &signature)
149    }
150
151    /// The operator and every application role are admitted, each as the
152    /// role it is listed under.
153    #[test]
154    fn a_listed_client_key_that_signed_the_nonce_is_admitted() {
155        for role in ["operator", "trader", "readonly"] {
156            let keys = keys_listing(role, &key());
157            let admitted = verify(&keys, &key()).unwrap();
158            assert_eq!(keys.token(KeyRole::Client(admitted)), role);
159        }
160    }
161
162    #[test]
163    fn an_unlisted_key_is_refused() {
164        let other = SigningKey::from_bytes(&[0x22; 32]);
165        let err = verify(&keys_listing("operator", &other), &key()).unwrap_err();
166        assert!(matches!(err, ClientAuthError::UnknownKey), "{err}");
167    }
168
169    /// A replication key authorizes node-to-node streaming only: refused
170    /// even though it is listed and signed correctly.
171    #[test]
172    fn a_replication_key_is_refused() {
173        let err = verify(&keys_listing("replication", &key()), &key()).unwrap_err();
174        assert!(
175            matches!(err, ClientAuthError::ReplicationKeyRefused),
176            "{err}"
177        );
178        // Named by its token, as the operator wrote it in the keys file.
179        assert_eq!(
180            err.to_string(),
181            "replication key may not connect as a client"
182        );
183        assert!(std::error::Error::source(&err).is_none());
184    }
185
186    /// A keys file can list any 32 bytes; ones that are not a point on
187    /// the curve cannot verify anything and are refused as such.
188    #[test]
189    fn a_listed_key_that_is_not_a_curve_point_is_refused() {
190        let not_a_point = [0x02; 32];
191        assert!(
192            VerifyingKey::from_bytes(&not_a_point).is_err(),
193            "the fixture must not decompress to a point"
194        );
195        let listed = base64::engine::general_purpose::STANDARD.encode(not_a_point);
196        let keys = desk_keys("operator", &listed);
197        let signature = key().sign(&NONCE).to_bytes();
198        let err = verify_client(&keys, &NONCE, &not_a_point, &signature).unwrap_err();
199        assert!(matches!(err, ClientAuthError::InvalidKey(_)), "{err}");
200    }
201
202    #[test]
203    fn a_signature_by_another_key_is_refused() {
204        let impostor = SigningKey::from_bytes(&[0x33; 32]);
205        let err = verify(&keys_listing("operator", &key()), &impostor).unwrap_err();
206        assert!(matches!(err, ClientAuthError::BadSignature(_)), "{err}");
207        // The verifier's own error stays reachable for a caller's report.
208        assert!(std::error::Error::source(&err).is_some());
209    }
210}