use std::fmt;
use ed25519_dalek::{Signature, SignatureError, Verifier, VerifyingKey};
use melin_app::auth::{AuthorizedKeys, Permission};
#[derive(Debug)]
pub(crate) enum ClientAuthError {
UnknownKey,
RoleRefused(Permission),
InvalidKey(SignatureError),
BadSignature(SignatureError),
}
impl fmt::Display for ClientAuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownKey => f.write_str("unknown public key"),
Self::RoleRefused(permission) => {
write!(f, "{permission:?} key refused on the client listener")
}
Self::InvalidKey(e) => write!(f, "invalid public key: {e}"),
Self::BadSignature(e) => write!(f, "signature verification failed: {e}"),
}
}
}
impl std::error::Error for ClientAuthError {}
pub(crate) fn verify_client(
authorized_keys: &AuthorizedKeys,
nonce: &[u8; 32],
public_key: &[u8; 32],
signature: &[u8; 64],
) -> Result<Permission, ClientAuthError> {
let permission = authorized_keys
.lookup(public_key)
.ok_or(ClientAuthError::UnknownKey)?;
if !permission.may_connect_as_client() {
return Err(ClientAuthError::RoleRefused(permission));
}
let verifying_key =
VerifyingKey::from_bytes(public_key).map_err(ClientAuthError::InvalidKey)?;
verifying_key
.verify(nonce, &Signature::from_bytes(signature))
.map_err(ClientAuthError::BadSignature)?;
Ok(permission)
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use ed25519_dalek::{Signer, SigningKey};
const NONCE: [u8; 32] = [0x5A; 32];
fn key() -> SigningKey {
SigningKey::from_bytes(&[0x11; 32])
}
fn keys_listing(role: &str, key: &SigningKey) -> AuthorizedKeys {
let public =
base64::engine::general_purpose::STANDARD.encode(key.verifying_key().to_bytes());
AuthorizedKeys::parse(&format!("{role} {public} test\n")).unwrap()
}
fn verify(keys: &AuthorizedKeys, signer: &SigningKey) -> Result<Permission, ClientAuthError> {
let public_key = key().verifying_key().to_bytes();
let signature = signer.sign(&NONCE).to_bytes();
verify_client(keys, &NONCE, &public_key, &signature)
}
#[test]
fn a_listed_client_key_that_signed_the_nonce_is_admitted() {
for (role, permission) in [
("operator", Permission::Operator),
("trader", Permission::Trader),
("custodian", Permission::Custodian),
("readonly", Permission::ReadOnly),
] {
let admitted = verify(&keys_listing(role, &key()), &key()).unwrap();
assert_eq!(admitted, permission);
}
}
#[test]
fn an_unlisted_key_is_refused() {
let other = SigningKey::from_bytes(&[0x22; 32]);
let err = verify(&keys_listing("operator", &other), &key()).unwrap_err();
assert!(matches!(err, ClientAuthError::UnknownKey), "{err}");
}
#[test]
fn a_replication_key_is_refused() {
let err = verify(&keys_listing("replication", &key()), &key()).unwrap_err();
assert!(
matches!(err, ClientAuthError::RoleRefused(Permission::Replication)),
"{err}"
);
assert!(err.to_string().contains("client listener"), "{err}");
}
#[test]
fn a_listed_key_that_is_not_a_curve_point_is_refused() {
let not_a_point = [0x02; 32];
assert!(
VerifyingKey::from_bytes(¬_a_point).is_err(),
"the fixture must not decompress to a point"
);
let listed = base64::engine::general_purpose::STANDARD.encode(not_a_point);
let keys = AuthorizedKeys::parse(&format!("operator {listed} test\n")).unwrap();
let signature = key().sign(&NONCE).to_bytes();
let err = verify_client(&keys, &NONCE, ¬_a_point, &signature).unwrap_err();
assert!(matches!(err, ClientAuthError::InvalidKey(_)), "{err}");
}
#[test]
fn a_signature_by_another_key_is_refused() {
let impostor = SigningKey::from_bytes(&[0x33; 32]);
let err = verify(&keys_listing("operator", &key()), &impostor).unwrap_err();
assert!(matches!(err, ClientAuthError::BadSignature(_)), "{err}");
}
}