use std::fmt;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ring::rand::SystemRandom;
use ring::signature::{self, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ControllerKeyError {
Unusable {
reason: String,
},
}
impl fmt::Display for ControllerKeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unusable { reason } => write!(
f,
"the controller's cluster signing key is unusable ({reason}). Every office was \
issued the public half of this key inside its join token, so it cannot be \
replaced silently: restore the file, or re-mint every office's token with \
\"rotate\": true after generating a new one."
),
}
}
}
impl std::error::Error for ControllerKeyError {}
pub struct ControllerSigningKey {
pair: Ed25519KeyPair,
verify_key: String,
}
impl fmt::Debug for ControllerSigningKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ControllerSigningKey")
.field("verify_key", &self.verify_key)
.finish_non_exhaustive()
}
}
impl ControllerSigningKey {
pub fn generate() -> Result<(Vec<u8>, Self), ControllerKeyError> {
let rng = SystemRandom::new();
let document =
Ed25519KeyPair::generate_pkcs8(&rng).map_err(|err| ControllerKeyError::Unusable {
reason: format!("key generation failed: {err}"),
})?;
let bytes = document.as_ref().to_vec();
let key = Self::from_pkcs8(&bytes)?;
Ok((bytes, key))
}
pub fn from_pkcs8(document: &[u8]) -> Result<Self, ControllerKeyError> {
let pair =
Ed25519KeyPair::from_pkcs8(document).map_err(|err| ControllerKeyError::Unusable {
reason: format!("not a PKCS#8 Ed25519 key: {err}"),
})?;
let verify_key = URL_SAFE_NO_PAD.encode(pair.public_key().as_ref());
Ok(Self { pair, verify_key })
}
pub fn verify_key(&self) -> &str {
&self.verify_key
}
pub fn sign(&self, message: &str) -> String {
URL_SAFE_NO_PAD.encode(self.pair.sign(message.as_bytes()).as_ref())
}
}
pub fn verify(verify_key: &str, message: &str, signature_b64: &str) -> bool {
let (Ok(key), Ok(sig)) = (
URL_SAFE_NO_PAD.decode(verify_key.trim()),
URL_SAFE_NO_PAD.decode(signature_b64.trim()),
) else {
return false;
};
UnparsedPublicKey::new(&signature::ED25519, key)
.verify(message.as_bytes(), &sig)
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_signature_verifies_under_the_public_half_and_nothing_else() {
let (_, key) = ControllerSigningKey::generate().expect("generate");
let signed = key.sign("zfjoin2/vouch:office-a:superadmin:1:n");
assert!(verify(
key.verify_key(),
"zfjoin2/vouch:office-a:superadmin:1:n",
&signed
));
assert!(!verify(
key.verify_key(),
"zfjoin2/vouch:office-a:root:1:n",
&signed
));
let (_, other) = ControllerSigningKey::generate().expect("generate");
assert!(!verify(
other.verify_key(),
"zfjoin2/vouch:office-a:superadmin:1:n",
&signed
));
assert!(!verify("not-base64!!", "m", &signed));
assert!(!verify(key.verify_key(), "m", "not-base64!!"));
assert!(!verify("", "", ""));
}
#[test]
fn the_public_half_forges_nothing() {
let (document, key) = ControllerSigningKey::generate().expect("generate");
let published = key.verify_key().to_string();
assert_eq!(
URL_SAFE_NO_PAD.decode(&published).expect("decode").len(),
32
);
assert!(!document.windows(32).any(|window| {
window
== URL_SAFE_NO_PAD
.decode(&published)
.expect("decode")
.as_slice()
&& document.len() == 32
}));
assert!(
ControllerSigningKey::from_pkcs8(&URL_SAFE_NO_PAD.decode(&published).expect("decode"))
.is_err(),
"the verification key must not load as a signing key"
);
}
#[test]
fn a_stored_key_round_trips_and_a_damaged_one_names_the_fix() {
let (document, key) = ControllerSigningKey::generate().expect("generate");
let reloaded = ControllerSigningKey::from_pkcs8(&document).expect("reload");
assert_eq!(reloaded.verify_key(), key.verify_key());
assert!(verify(key.verify_key(), "m", &reloaded.sign("m")));
let err = ControllerSigningKey::from_pkcs8(b"not a key").expect_err("damaged");
let message = err.to_string();
assert!(
message.contains("re-mint every office's token"),
"{message}"
);
}
}