use async_trait::async_trait;
use ed25519_dalek::{Signer as EdSigner, SigningKey, Verifier as EdVerifier, VerifyingKey};
use klieo_provenance::{BundleSigner, ProvenanceError, SignatureBytes};
use rand::rngs::OsRng;
use thiserror::Error;
const ED25519_ALGORITHM: &str = "ed25519";
const KEY_ID_PREFIX_LEN: usize = 8;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct SignedPayload {
pub signature: [u8; 64],
pub verifying_key: [u8; 32],
}
impl SignedPayload {
#[must_use]
pub fn new(signature: [u8; 64], verifying_key: [u8; 32]) -> Self {
Self {
signature,
verifying_key,
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SignerError {
#[error("signer backend: {0}")]
Backend(String),
#[error("internal: {0}")]
Internal(String),
}
#[async_trait]
pub trait SignerProvider: Send + Sync {
async fn sign_handoff(&self, payload: &[u8]) -> Result<SignedPayload, SignerError>;
}
pub struct SoftwareSigner {
key: SigningKey,
}
impl SoftwareSigner {
#[must_use]
pub fn new(key: SigningKey) -> Self {
Self { key }
}
#[must_use]
pub fn from_bytes(seed: [u8; 32]) -> Self {
Self::new(SigningKey::from_bytes(&seed))
}
#[must_use]
pub fn generate() -> Self {
Self::new(SigningKey::generate(&mut OsRng))
}
#[must_use]
pub fn public_key_hex(&self) -> String {
hex::encode(self.key.verifying_key().to_bytes())
}
#[must_use]
pub fn key_id(&self) -> String {
let pub_hex = self.public_key_hex();
format!("klieo-ops-{}", &pub_hex[..KEY_ID_PREFIX_LEN])
}
#[must_use]
pub fn verify(&self, payload: &[u8], signature_hex: &str) -> bool {
let Ok(sig_bytes) = hex::decode(signature_hex) else {
return false;
};
let Ok(sig) = ed25519_dalek::Signature::from_slice(&sig_bytes) else {
return false;
};
self.verifying_key().verify(payload, &sig).is_ok()
}
fn verifying_key(&self) -> VerifyingKey {
self.key.verifying_key()
}
}
#[async_trait]
impl BundleSigner for SoftwareSigner {
async fn sign(&self, payload: &[u8]) -> Result<SignatureBytes, ProvenanceError> {
let signature = self.key.sign(payload);
Ok(SignatureBytes {
algorithm: ED25519_ALGORITHM.to_string(),
signature_hex: hex::encode(signature.to_bytes()),
public_key_hex: self.public_key_hex(),
key_id: Some(self.key_id()),
})
}
}
#[async_trait]
impl SignerProvider for SoftwareSigner {
async fn sign_handoff(&self, payload: &[u8]) -> Result<SignedPayload, SignerError> {
let sig = self.key.sign(payload);
Ok(SignedPayload {
signature: sig.to_bytes(),
verifying_key: self.key.verifying_key().to_bytes(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use klieo_provenance::BundleSigner;
#[tokio::test]
async fn software_signer_produces_verifiable_signature() {
let signer = SoftwareSigner::from_bytes([7u8; 32]);
let payload = b"hello";
let signed = signer.sign_handoff(payload).await.expect("sign");
let vk = VerifyingKey::from_bytes(&signed.verifying_key).expect("vk");
let sig = Signature::from_bytes(&signed.signature);
vk.verify(payload, &sig).expect("verify");
}
#[tokio::test]
async fn bundle_signer_emits_ed25519_signature_bytes() {
let signer = SoftwareSigner::from_bytes([7u8; 32]);
let payload = b"evidence-bundle-payload";
let sig = BundleSigner::sign(&signer, payload).await.expect("sign");
assert_eq!(sig.algorithm, "ed25519");
assert_eq!(sig.public_key_hex, signer.public_key_hex());
let key_id = signer.key_id();
assert_eq!(
sig.key_id.as_deref(),
Some(key_id.as_str()),
"bundle key_id must match the signer's stable key_id"
);
const KEY_ID_PREFIX: &str = "klieo-ops-";
assert!(
key_id.starts_with(KEY_ID_PREFIX),
"key_id must use the documented prefix"
);
assert_eq!(
key_id.len(),
KEY_ID_PREFIX.len() + KEY_ID_PREFIX_LEN,
"key_id is the prefix plus {KEY_ID_PREFIX_LEN} hex chars of the verifying key"
);
}
#[tokio::test]
async fn bundle_signature_round_trips_against_public_key() {
let signer = SoftwareSigner::from_bytes([7u8; 32]);
let payload = b"evidence-bundle-payload";
let sig = BundleSigner::sign(&signer, payload).await.expect("sign");
assert!(
signer.verify(payload, &sig.signature_hex),
"real payload must verify"
);
assert!(
!signer.verify(b"tampered-payload", &sig.signature_hex),
"tampered payload must be rejected"
);
assert!(
!signer.verify(payload, "deadbeef"),
"malformed signature hex must be rejected"
);
}
#[tokio::test]
async fn generated_signer_round_trips() {
let signer = SoftwareSigner::generate();
let payload = b"fresh-keypair-payload";
let sig = BundleSigner::sign(&signer, payload).await.expect("sign");
assert!(signer.verify(payload, &sig.signature_hex));
assert!(!signer.verify(b"other", &sig.signature_hex));
}
}