use std::fmt;
use ed25519_dalek::{Signature, SignatureError, Verifier, VerifyingKey};
use melin_app::auth::{AuthorizedKeys, ClientRole, RoleId};
pub(crate) const MAX_AUTH_FRAME: usize = 256;
#[derive(Debug)]
pub enum ClientAuthError {
ReplicationKeyRefused,
UnknownKey,
InvalidKey(SignatureError),
BadSignature(SignatureError),
}
impl fmt::Display for ClientAuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ReplicationKeyRefused => {
f.write_str("replication key may not connect as a client")
}
Self::UnknownKey => f.write_str("unknown public key"),
Self::InvalidKey(e) => write!(f, "invalid public key: {e}"),
Self::BadSignature(e) => write!(f, "signature verification failed: {e}"),
}
}
}
impl std::error::Error for ClientAuthError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidKey(e) | Self::BadSignature(e) => Some(e),
Self::ReplicationKeyRefused | Self::UnknownKey => None,
}
}
}
pub fn verify_client(
authorized_keys: &AuthorizedKeys,
nonce: &[u8; 32],
public_key: &[u8; 32],
signature: &[u8; 64],
) -> Result<ClientRole<RoleId>, ClientAuthError> {
let role = authorized_keys
.lookup(public_key)
.ok_or(ClientAuthError::UnknownKey)?
.client()
.ok_or(ClientAuthError::ReplicationKeyRefused)?;
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(role)
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use ed25519_dalek::{Signer, SigningKey};
use melin_app::auth::KeyRole;
use crate::test_roles::desk_keys;
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());
desk_keys(role, &public)
}
fn verify(
keys: &AuthorizedKeys,
signer: &SigningKey,
) -> Result<ClientRole<RoleId>, 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 in ["operator", "trader", "readonly"] {
let keys = keys_listing(role, &key());
let admitted = verify(&keys, &key()).unwrap();
assert_eq!(keys.token(KeyRole::Client(admitted)), role);
}
}
#[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::ReplicationKeyRefused),
"{err}"
);
assert_eq!(
err.to_string(),
"replication key may not connect as a client"
);
assert!(std::error::Error::source(&err).is_none());
}
#[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 = desk_keys("operator", &listed);
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}");
assert!(std::error::Error::source(&err).is_some());
}
}