melin_server_runtime/
client_auth.rs1use std::fmt;
14
15use ed25519_dalek::{Signature, SignatureError, Verifier, VerifyingKey};
16use melin_app::auth::{AuthorizedKeys, ClientRole, RoleId};
17
18pub(crate) const MAX_AUTH_FRAME: usize = 256;
22
23#[derive(Debug)]
29pub enum ClientAuthError {
30 ReplicationKeyRefused,
34 UnknownKey,
36 InvalidKey(SignatureError),
38 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
64pub 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 #[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 #[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 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 #[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(¬_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, ¬_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 assert!(std::error::Error::source(&err).is_some());
209 }
210}