use std::path::PathBuf;
#[derive(Clone)]
pub struct Ed25519SecretKey(ed25519_dalek::SigningKey);
impl Ed25519SecretKey {
pub fn generate() -> Self {
Self(ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng))
}
pub fn from_bytes(bytes: &[u8; 32]) -> Self {
Self(ed25519_dalek::SigningKey::from_bytes(bytes))
}
pub fn as_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
pub fn public(&self) -> ed25519_dalek::VerifyingKey {
self.0.verifying_key()
}
pub fn sign(&self, message: &[u8]) -> ed25519_dalek::Signature {
use ed25519_dalek::Signer;
self.0.sign(message)
}
}
impl std::fmt::Debug for Ed25519SecretKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ed25519SecretKey").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AcmeDirectory {
Production,
Staging,
Custom(String),
}
impl AcmeDirectory {
pub fn url(&self) -> &str {
match self {
AcmeDirectory::Production => "https://acme-v02.api.letsencrypt.org/directory",
AcmeDirectory::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
AcmeDirectory::Custom(url) => url,
}
}
}
#[derive(Debug, Clone)]
pub enum TlsIdentity {
X509 {
cert: PathBuf,
key: PathBuf,
},
RawKey(Ed25519SecretKey),
SelfSigned,
Acme {
domains: Vec<String>,
cache_dir: PathBuf,
directory: AcmeDirectory,
contact: Vec<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::Verifier;
#[test]
fn ed25519_secret_key_round_trips_bytes() {
let key = Ed25519SecretKey::generate();
let bytes = key.as_bytes();
let restored = Ed25519SecretKey::from_bytes(&bytes);
assert_eq!(restored.as_bytes(), bytes);
}
#[test]
fn ed25519_secret_key_sign_verifies_against_public_key() {
let key = Ed25519SecretKey::generate();
let public = key.public();
let message = b"alktls identity coverage check";
let signature: ed25519_dalek::Signature = key.sign(message);
assert_eq!(signature.to_bytes().len(), 64);
assert!(
public.verify(message, &signature).is_ok(),
"signature produced by Ed25519SecretKey::sign must verify under its public key"
);
}
#[test]
fn ed25519_secret_key_sign_rejects_tampered_message() {
let key = Ed25519SecretKey::generate();
let public = key.public();
let signature: ed25519_dalek::Signature = key.sign(b"original message");
assert!(
public.verify(b"tampered message", &signature).is_err(),
"signature must not verify against a different message"
);
}
#[test]
fn ed25519_secret_key_debug_does_not_leak_material() {
let key = Ed25519SecretKey::generate();
let dbg = format!("{key:?}");
assert!(dbg.contains("Ed25519SecretKey"));
assert!(!dbg.contains("SigningKey"));
let raw = hex::encode(key.as_bytes());
assert!(
!dbg.contains(&raw),
"Debug output must not contain the raw key bytes"
);
}
#[test]
fn ed25519_secret_key_public_matches_underlying_signing_key() {
let key = Ed25519SecretKey::generate();
let public = key.public();
assert_eq!(public.to_bytes().len(), 32);
}
#[test]
fn acme_directory_urls_are_pinned() {
assert_eq!(
AcmeDirectory::Production.url(),
"https://acme-v02.api.letsencrypt.org/directory"
);
assert_eq!(
AcmeDirectory::Staging.url(),
"https://acme-staging-v02.api.letsencrypt.org/directory"
);
assert_eq!(
AcmeDirectory::Custom("https://acme.example/dir".to_string()).url(),
"https://acme.example/dir"
);
}
#[test]
fn tls_identity_x509_construct() {
let id = TlsIdentity::X509 {
cert: PathBuf::from("/etc/cert.pem"),
key: PathBuf::from("/etc/key.pem"),
};
match id {
TlsIdentity::X509 { cert, key } => {
assert_eq!(cert, PathBuf::from("/etc/cert.pem"));
assert_eq!(key, PathBuf::from("/etc/key.pem"));
}
_ => panic!("expected X509"),
}
}
#[test]
fn tls_identity_self_signed() {
let id = TlsIdentity::SelfSigned;
let s = format!("{id:?}");
assert!(s.contains("SelfSigned"));
}
#[test]
fn tls_identity_acme_construct() {
let id = TlsIdentity::Acme {
domains: vec!["example.com".to_string()],
cache_dir: PathBuf::from("/var/cache/alktls-acme"),
directory: AcmeDirectory::Staging,
contact: vec!["mailto:ops@example.com".to_string()],
};
match id {
TlsIdentity::Acme {
domains,
cache_dir,
directory,
contact,
} => {
assert_eq!(domains, vec!["example.com".to_string()]);
assert_eq!(cache_dir, PathBuf::from("/var/cache/alktls-acme"));
assert_eq!(directory, AcmeDirectory::Staging);
assert_eq!(contact, vec!["mailto:ops@example.com".to_string()]);
}
_ => panic!("expected Acme"),
}
}
}