use crate::error::AuthError;
use ed25519_dalek::{
Signature, Signer, SigningKey as EdSigningKey, VerifyingKey as EdVerifyingKey,
};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct SigningKey {
inner: EdSigningKey,
}
impl SigningKey {
pub fn generate() -> Self {
use rand::rngs::OsRng;
use rand::RngCore;
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
Self {
inner: EdSigningKey::from_bytes(&bytes),
}
}
pub fn from_bytes(bytes: &[u8; 32]) -> Self {
Self {
inner: EdSigningKey::from_bytes(bytes),
}
}
pub fn verifying_key(&self) -> VerifyingKey {
VerifyingKey {
inner: self.inner.verifying_key(),
}
}
pub fn key_id(&self) -> String {
self.verifying_key().key_id()
}
pub fn sign(&self, message: &[u8]) -> Signature {
self.inner.sign(message)
}
pub fn to_bytes(&self) -> [u8; 32] {
self.inner.to_bytes()
}
pub fn to_keypair_bytes(&self) -> [u8; 64] {
self.inner.to_keypair_bytes()
}
pub fn from_keypair_bytes(bytes: &[u8; 64]) -> Result<Self, AuthError> {
let key = EdSigningKey::from_keypair_bytes(bytes)
.map_err(|e| AuthError::InvalidKeyFormat(format!("Invalid keypair: {:?}", e)))?;
Ok(Self { inner: key })
}
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, AuthError> {
use ed25519_dalek::pkcs8::EncodePrivateKey;
self.inner
.to_pkcs8_der()
.map(|der| der.as_bytes().to_vec())
.map_err(|e| AuthError::InvalidKeyFormat(format!("PKCS#8 encoding failed: {:?}", e)))
}
}
#[derive(Debug, Clone)]
pub struct VerifyingKey {
pub(crate) inner: EdVerifyingKey,
}
impl VerifyingKey {
pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, AuthError> {
let key = EdVerifyingKey::from_bytes(bytes)
.map_err(|e| AuthError::InvalidKeyFormat(format!("Invalid public key: {:?}", e)))?;
Ok(Self { inner: key })
}
pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), AuthError> {
self.inner
.verify_strict(message, signature)
.map_err(|e| AuthError::InvalidKeyFormat(format!("Verification failed: {:?}", e)))
}
pub fn to_bytes(&self) -> [u8; 32] {
self.inner.to_bytes()
}
pub fn key_id(&self) -> String {
let hex = self
.to_bytes()
.into_iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
hex[..16].to_string()
}
pub fn to_spki_der(&self) -> Result<Vec<u8>, AuthError> {
use ed25519_dalek::pkcs8::EncodePublicKey;
self.inner
.to_public_key_der()
.map(|der| der.as_bytes().to_vec())
.map_err(|e| AuthError::InvalidKeyFormat(format!("SPKI encoding failed: {:?}", e)))
}
}
pub struct KeyLoader;
impl KeyLoader {
pub fn signing_key_from_env(var_name: &str) -> Result<SigningKey, AuthError> {
let b64 = std::env::var(var_name).map_err(|_| {
AuthError::KeyLoadingFailed(format!("Environment variable {} not set", var_name))
})?;
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &b64)
.map_err(|e| AuthError::InvalidKeyFormat(format!("Invalid base64: {}", e)))?;
let key_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| AuthError::InvalidKeyFormat("Invalid key length".to_string()))?;
Ok(SigningKey::from_bytes(&key_bytes))
}
pub fn verifying_key_from_env(var_name: &str) -> Result<VerifyingKey, AuthError> {
let b64 = std::env::var(var_name).map_err(|_| {
AuthError::KeyLoadingFailed(format!("Environment variable {} not set", var_name))
})?;
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &b64)
.map_err(|e| AuthError::InvalidKeyFormat(format!("Invalid base64: {}", e)))?;
let key_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| AuthError::InvalidKeyFormat("Invalid key length".to_string()))?;
VerifyingKey::from_bytes(&key_bytes)
}
pub fn generate_and_save_keys(
signing_key_path: impl AsRef<Path>,
verifying_key_path: impl AsRef<Path>,
) -> Result<(SigningKey, VerifyingKey), AuthError> {
let signing_key = SigningKey::generate();
let verifying_key = signing_key.verifying_key();
let signing_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
signing_key.to_bytes(),
);
write_owner_only(signing_key_path.as_ref(), signing_b64.as_bytes())?;
let verifying_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
verifying_key.to_bytes(),
);
fs::write(verifying_key_path, verifying_b64)?;
Ok((signing_key, verifying_key))
}
pub fn signing_key_from_file(path: impl AsRef<Path>) -> Result<SigningKey, AuthError> {
let b64 = fs::read_to_string(path)?;
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &b64)
.map_err(|e| AuthError::InvalidKeyFormat(format!("Invalid base64: {}", e)))?;
let key_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| AuthError::InvalidKeyFormat("Invalid key length".to_string()))?;
Ok(SigningKey::from_bytes(&key_bytes))
}
pub fn verifying_key_from_file(path: impl AsRef<Path>) -> Result<VerifyingKey, AuthError> {
let b64 = fs::read_to_string(path)?;
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &b64)
.map_err(|e| AuthError::InvalidKeyFormat(format!("Invalid base64: {}", e)))?;
let key_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| AuthError::InvalidKeyFormat("Invalid key length".to_string()))?;
VerifyingKey::from_bytes(&key_bytes)
}
}
fn write_owner_only(path: &Path, contents: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let mut options = fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(fs::Permissions::from_mode(0o600))?;
}
file.write_all(contents)?;
file.sync_all()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
fn mode(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[cfg(unix)]
#[test]
fn generated_signing_key_is_owner_only() {
let dir = tempfile::tempdir().unwrap();
let signing = dir.path().join("signing.key");
let verifying = dir.path().join("verifying.key");
let (signing_key, _) = KeyLoader::generate_and_save_keys(&signing, &verifying).unwrap();
assert_eq!(mode(&signing), 0o600);
let loaded = KeyLoader::signing_key_from_file(&signing).unwrap();
assert_eq!(loaded.to_bytes(), signing_key.to_bytes());
}
#[cfg(unix)]
#[test]
fn regenerating_over_a_world_readable_signing_key_tightens_it() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let signing = dir.path().join("signing.key");
let verifying = dir.path().join("verifying.key");
fs::write(&signing, "stale").unwrap();
fs::set_permissions(&signing, fs::Permissions::from_mode(0o644)).unwrap();
KeyLoader::generate_and_save_keys(&signing, &verifying).unwrap();
assert_eq!(mode(&signing), 0o600);
}
#[test]
fn test_generate_and_sign() {
let signing_key = SigningKey::generate();
let message = b"test message";
let signature = signing_key.sign(message);
let verifying_key = signing_key.verifying_key();
assert!(verifying_key.verify(message, &signature).is_ok());
}
#[test]
fn test_bytes_roundtrip() {
let signing_key = SigningKey::generate();
let bytes = signing_key.to_bytes();
let loaded = SigningKey::from_bytes(&bytes);
assert_eq!(
signing_key.verifying_key().to_bytes(),
loaded.verifying_key().to_bytes()
);
}
#[test]
fn test_keypair_bytes_roundtrip() {
let signing_key = SigningKey::generate();
let keypair_bytes = signing_key.to_keypair_bytes();
let loaded = SigningKey::from_keypair_bytes(&keypair_bytes).unwrap();
assert_eq!(
signing_key.verifying_key().to_bytes(),
loaded.verifying_key().to_bytes()
);
}
}