use std::path::Path;
use crate::capability::KeyFp;
use crate::error::{Error, Result};
use crate::pki::{self, SigAlgKind};
pub trait SignerProvider: Send + Sync + std::fmt::Debug {
fn sign(&self, msg: &[u8]) -> Result<(SigAlgKind, Vec<u8>, KeyFp)>;
fn fingerprint(&self) -> Result<KeyFp>;
}
#[derive(Debug)]
pub struct PemSigner {
priv_pem: String,
alg: SigAlgKind,
fp: KeyFp,
}
impl PemSigner {
pub fn from_file(priv_path: &Path, alg: SigAlgKind) -> Result<Self> {
let priv_pem = std::fs::read_to_string(priv_path)?;
Self::from_pem(&priv_pem, alg)
}
pub fn from_pem(priv_pem: &str, alg: SigAlgKind) -> Result<Self> {
let botan_priv = botan::Privkey::load_pem(priv_pem).map_err(Error::botan)?;
let botan_pub = botan_priv.pubkey().map_err(Error::botan)?;
let pub_pem = botan_pub.pem_encode().map_err(Error::botan)?;
let fp = KeyFp::from_pem(&pub_pem)?;
Ok(PemSigner {
priv_pem: priv_pem.to_string(),
alg,
fp,
})
}
pub fn pub_pem(&self) -> Result<String> {
let botan_priv = botan::Privkey::load_pem(&self.priv_pem).map_err(Error::botan)?;
let botan_pub = botan_priv.pubkey().map_err(Error::botan)?;
botan_pub.pem_encode().map_err(Error::botan)
}
}
impl SignerProvider for PemSigner {
fn sign(&self, msg: &[u8]) -> Result<(SigAlgKind, Vec<u8>, KeyFp)> {
let mut rng = botan::RandomNumberGenerator::new_system().map_err(Error::botan)?;
let sig = pki::sign(self.alg, &self.priv_pem, msg, &mut rng)?;
Ok((self.alg, sig, self.fp))
}
fn fingerprint(&self) -> Result<KeyFp> {
Ok(self.fp)
}
}
pub fn parse_signer_arg(s: &str, alg: SigAlgKind) -> Result<Box<dyn SignerProvider>> {
if s.starts_with("confium://") {
Err(Error::msg(
"Confium threshold signing not yet implemented (see TODO.roadmap/22)",
))
} else if s.starts_with("pkcs11://") {
Err(Error::msg("PKCS#11 hardware signing not yet implemented"))
} else {
let signer = PemSigner::from_file(Path::new(s), alg)?;
Ok(Box::new(signer))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ed25519_pem() -> String {
let mut rng = botan::RandomNumberGenerator::new_system().unwrap();
let (priv_pem, _) = pki::keygen(SigAlgKind::Ed25519, &mut rng).unwrap();
priv_pem
}
#[test]
fn pem_signer_round_trip() {
let priv_pem = ed25519_pem();
let signer = PemSigner::from_pem(&priv_pem, SigAlgKind::Ed25519).unwrap();
let msg = b"test message";
let (alg, sig, fp) = signer.sign(msg).unwrap();
assert_eq!(alg, SigAlgKind::Ed25519);
assert_eq!(sig.len(), 64);
let pub_pem = signer.pub_pem().unwrap();
assert!(pki::verify(SigAlgKind::Ed25519, &pub_pem, msg, &sig).unwrap());
let expected_fp = KeyFp::from_pem(&pub_pem).unwrap();
assert_eq!(fp, expected_fp);
assert_eq!(signer.fingerprint().unwrap(), expected_fp);
}
#[test]
fn parse_bare_path_returns_pem_signer() {
let mut rng = botan::RandomNumberGenerator::new_system().unwrap();
let (priv_pem, _) = pki::keygen(SigAlgKind::Ed25519, &mut rng).unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("priv.pem");
std::fs::write(&path, &priv_pem).unwrap();
let signer = parse_signer_arg(path.to_str().unwrap(), SigAlgKind::Ed25519);
assert!(signer.is_ok());
}
#[test]
fn parse_confium_uri_returns_not_yet_implemented() {
let result = parse_signer_arg("confium://session-1", SigAlgKind::Ed25519);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not yet implemented"));
}
#[test]
fn parse_pkcs11_uri_returns_not_yet_implemented() {
let result = parse_signer_arg("pkcs11://token-1", SigAlgKind::Ed25519);
assert!(result.is_err());
}
}
pub trait KemProvider: Send + Sync + std::fmt::Debug {
fn encapsulate(
&self,
rng: &mut botan::RandomNumberGenerator,
) -> Result<(Vec<u8>, Vec<u8>, crate::capability::KeyFp)>;
fn decapsulate(&self, ciphertext: &[u8]) -> Result<Vec<u8>>;
fn fingerprint(&self) -> Result<crate::capability::KeyFp>;
}
#[allow(dead_code)]
pub struct PasswordKem {
pub(crate) word: String,
pub(crate) password: String,
}
impl std::fmt::Debug for PasswordKem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PasswordKem")
.field("word", &self.word)
.field("password", &"<redacted>")
.finish()
}
}
impl PasswordKem {
pub fn new(word: impl Into<String>, password: impl Into<String>) -> Self {
PasswordKem {
word: word.into(),
password: password.into(),
}
}
}
#[cfg(test)]
mod kem_tests {
use super::*;
#[test]
fn password_kem_debug_does_not_leak_password() {
let kem = PasswordKem::new("WORD", "secret123");
let dbg = format!("{:?}", kem);
assert!(
!dbg.contains("secret123"),
"PasswordKem Debug leaked password: {}",
dbg
);
}
}