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 type SignFuture<'a> = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(SigAlgKind, Vec<u8>, KeyFp)>> + Send + 'a>,
>;
pub trait AsyncSignerProvider: Send + Sync + std::fmt::Debug {
fn sign_async(&self, msg: &[u8]) -> SignFuture<'_>;
fn fingerprint(&self) -> Result<KeyFp>;
}
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::InvalidArg {
arg: "signer",
reason: "Confium daemon not found. Install and start the daemon from \
https://github.com/confium/confium, then use \
--signer confium://<session-id> (see TODO.finalize/38)"
.to_string(),
})
} else if s.starts_with("pkcs11://") {
Err(Error::InvalidArg {
arg: "signer",
reason: "PKCS#11 signing requires the 'pkcs11' cargo feature. \
Rebuild with: cargo build --features pkcs11. \
Then use --signer pkcs11://<module-path>/<key-label>"
.to_string(),
})
} else {
let signer = PemSigner::from_file(Path::new(s), alg)?;
Ok(Box::new(signer))
}
}
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>;
}
#[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());
}
}
#[cfg(test)]
mod bridge_tests {
use super::*;
#[derive(Debug)]
struct TestAsyncSigner {
fp: KeyFp,
pending_polls: usize,
}
impl AsyncSignerProvider for TestAsyncSigner {
fn sign_async(&self, msg: &[u8]) -> SignFuture<'_> {
let fp = self.fp;
let msg = msg.to_vec();
let pending_polls = self.pending_polls;
Box::pin(async move {
let mut left = pending_polls;
let pending = std::future::poll_fn(move |cx| {
if left > 0 {
left -= 1;
cx.waker().wake_by_ref();
std::task::Poll::<()>::Pending
} else {
std::task::Poll::<()>::Ready(())
}
});
let _: () = pending.await;
let sig = msg.iter().map(|b| !b).collect();
Ok((SigAlgKind::Ed25519, sig, fp))
})
}
fn fingerprint(&self) -> Result<KeyFp> {
Ok(self.fp)
}
}
fn test_fp(byte: u8) -> KeyFp {
KeyFp::from_bytes([byte; 32])
}
#[test]
fn sync_variant_delegates_directly() {
let signer = PemSigner::from_pem(&ed_pem(), SigAlgKind::Ed25519).unwrap();
let expected_fp = signer.fingerprint().unwrap();
let any = AnySigner::Sync(Box::new(signer));
let (_, _, fp) = any.sign_blocking(b"msg").unwrap();
assert_eq!(fp, expected_fp);
assert_eq!(any.fingerprint().unwrap(), expected_fp);
}
#[test]
fn async_variant_blocks_until_ready() {
let fp = test_fp(7);
let s = TestAsyncSigner {
fp,
pending_polls: 2,
};
let any = AnySigner::Async(Box::new(s));
let (alg, sig, got) = any.sign_blocking(&[0x00, 0xFF]).unwrap();
assert_eq!(alg, SigAlgKind::Ed25519);
assert_eq!(sig, vec![0xFF, 0x00], "test signer inverts bytes");
assert_eq!(got, fp);
assert_eq!(any.fingerprint().unwrap(), fp);
}
#[test]
fn block_on_returns_immediately_for_ready_futures() {
let out = futures_block_on::block_on(async { 42 });
assert_eq!(out, 42);
}
fn ed_pem() -> String {
let mut rng = botan::RandomNumberGenerator::new_system().unwrap();
let (priv_pem, _) = pki::keygen(SigAlgKind::Ed25519, &mut rng).unwrap();
priv_pem
}
}