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>;
}
pub trait AsyncSignerProvider: Send + Sync + std::fmt::Debug {
#[allow(clippy::type_complexity)]
fn sign_async(
&self,
msg: &[u8],
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(SigAlgKind, Vec<u8>, KeyFp)>> + Send + '_>,
>;
fn fingerprint(&self) -> Result<KeyFp>;
}
#[allow(clippy::type_complexity)]
pub enum AnySigner {
Sync(Box<dyn SignerProvider>),
Async(Box<dyn AsyncSignerProvider>),
}
impl AnySigner {
pub fn sign_blocking(&self, msg: &[u8]) -> Result<(SigAlgKind, Vec<u8>, KeyFp)> {
match self {
AnySigner::Sync(s) => s.sign(msg),
AnySigner::Async(s) => {
let fut = s.sign_async(msg);
futures_block_on::block_on(fut)
}
}
}
pub fn fingerprint(&self) -> Result<KeyFp> {
match self {
AnySigner::Sync(s) => s.fingerprint(),
AnySigner::Async(s) => s.fingerprint(),
}
}
}
mod futures_block_on {
use std::future::Future;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
pub fn block_on<F: Future>(fut: F) -> F::Output {
fn noop_clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &NOOP_VTABLE)
}
fn noop(_: *const ()) {}
static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) };
let mut cx = Context::from_waker(&waker);
let mut fut = Box::pin(fut);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => std::thread::yield_now(),
}
}
}
}
#[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: daemon CLI not yet shipped (see TODO.finalize/38)",
))
} 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_daemon_pending_error() {
let result = parse_signer_arg("confium://session-1", SigAlgKind::Ed25519);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("daemon"), "should reference the daemon: {err}");
assert!(
err.contains("TODO.finalize/38"),
"should link the tracking TODO: {err}"
);
}
#[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
);
}
}