use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use crate::error::{TrustError, TrustResult};
pub(crate) fn validate_note_name(name: &str) -> TrustResult<()> {
if name.is_empty() {
return Err(TrustError::VerifierKey {
reason: "key name must not be empty".to_string(),
});
}
if name.chars().any(char::is_whitespace) {
return Err(TrustError::VerifierKey {
reason: "key name must not contain whitespace".to_string(),
});
}
if name.contains('+') {
return Err(TrustError::VerifierKey {
reason: "key name must not contain '+'".to_string(),
});
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NoteVerifierKey {
name: String,
key_id: [u8; 4],
public_key: [u8; 32],
}
impl NoteVerifierKey {
pub fn new(name: &str, public_key: [u8; 32]) -> TrustResult<Self> {
let key_id = super::note::key_id(name, &public_key)?;
Ok(Self {
name: name.to_string(),
key_id,
public_key,
})
}
pub fn from_spec(spec: &str) -> TrustResult<Self> {
let mut parts = spec.splitn(3, '+');
let (Some(name), Some(hex_id), Some(key_b64)) = (parts.next(), parts.next(), parts.next())
else {
return Err(TrustError::VerifierKey {
reason: "expected <name>+<hex keyid>+<base64 key>".to_string(),
});
};
validate_note_name(name)?;
let declared_id = parse_key_id_hex(hex_id)?;
let key_bytes = STANDARD
.decode(key_b64)
.map_err(|e| TrustError::VerifierKey {
reason: format!("key part is not canonical standard base64: {e}"),
})?;
let (Some((&alg, public_key_slice)), 33) = (key_bytes.split_first(), key_bytes.len())
else {
return Err(TrustError::VerifierKey {
reason: format!(
"key part must decode to 33 bytes (algorithm byte + 32-byte public key), \
got {}",
key_bytes.len()
),
});
};
if alg != 0x01 {
return Err(TrustError::VerifierKey {
reason: format!("unsupported algorithm byte {alg:#04x}, expected 0x01 (Ed25519)"),
});
}
let mut public_key = [0u8; 32];
public_key.copy_from_slice(public_key_slice);
let computed_id = super::note::key_id(name, &public_key)?;
if declared_id != computed_id {
return Err(TrustError::VerifierKey {
reason: "declared key ID does not match the key ID computed from \
the name and public key"
.to_string(),
});
}
Ok(Self {
name: name.to_string(),
key_id: computed_id,
public_key,
})
}
#[must_use]
pub fn to_spec(&self) -> String {
let mut key_bytes = Vec::with_capacity(33);
key_bytes.push(0x01);
key_bytes.extend_from_slice(&self.public_key);
format!(
"{}+{}+{}",
self.name,
crate::hex_lower(&self.key_id),
STANDARD.encode(&key_bytes)
)
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn key_id(&self) -> [u8; 4] {
self.key_id
}
#[must_use]
pub fn public_key(&self) -> [u8; 32] {
self.public_key
}
}
fn parse_key_id_hex(hex_id: &str) -> TrustResult<[u8; 4]> {
if hex_id.len() != 8 || !hex_id.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(TrustError::VerifierKey {
reason: "key ID must be exactly 8 hex characters".to_string(),
});
}
let value = u32::from_str_radix(hex_id, 16).map_err(|e| TrustError::VerifierKey {
reason: format!("key ID is not valid hex: {e}"),
})?;
Ok(value.to_be_bytes())
}
#[cfg(test)]
#[path = "verifier_key_tests.rs"]
mod tests;