klieo-ops 0.41.2

Operational layer above klieo-core: supervisor, governor, gates, escalation, worklog, handoff.
Documentation
//! Pluggable signing for handoff envelopes.
//!
//! `SignerProvider` abstracts the act of producing an Ed25519 signature
//! over a canonical-JSON payload. The default `SoftwareSigner` wraps
//! `ed25519_dalek::SigningKey` directly. HSM- / KMS-backed adapters
//! implement the trait against their respective vendor SDK without
//! modifying klieo-ops.
//!
//! Closes spec § 5.4 OQ-3.

use async_trait::async_trait;
use ed25519_dalek::{Signer as EdSigner, SigningKey};
use thiserror::Error;

/// Output of a sign call: the raw 64-byte Ed25519 signature plus the
/// 32-byte verifying key that should be embedded in the envelope so the
/// receiver can resolve it against its trust registry.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct SignedPayload {
    /// 64-byte Ed25519 signature.
    pub signature: [u8; 64],
    /// 32-byte Ed25519 verifying key.
    pub verifying_key: [u8; 32],
}

impl SignedPayload {
    /// Construct a `SignedPayload`. Prefer this over struct-literal syntax
    /// — `#[non_exhaustive]` blocks cross-crate struct literals.
    #[must_use]
    pub fn new(signature: [u8; 64], verifying_key: [u8; 32]) -> Self {
        Self {
            signature,
            verifying_key,
        }
    }
}

/// Errors raised by `SignerProvider` impls.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SignerError {
    /// Underlying HSM / KMS rejected the request.
    #[error("signer backend: {0}")]
    Backend(String),
    /// Other internal error.
    #[error("internal: {0}")]
    Internal(String),
}

/// Pluggable Ed25519 signer.
#[async_trait]
pub trait SignerProvider: Send + Sync {
    /// Sign `payload` and return both the signature and the verifying key.
    async fn sign_handoff(&self, payload: &[u8]) -> Result<SignedPayload, SignerError>;
}

/// Default impl backed by an in-process `ed25519_dalek::SigningKey`.
///
/// Suitable for dev and non-regulated deployments. Production tenants
/// requiring HSM-backed signing implement `SignerProvider` against
/// their vendor SDK.
pub struct SoftwareSigner {
    key: SigningKey,
}

impl SoftwareSigner {
    /// Build from an existing `SigningKey`.
    #[must_use]
    pub fn new(key: SigningKey) -> Self {
        Self { key }
    }

    /// Build deterministically from 32 bytes — convenience for tests.
    #[must_use]
    pub fn from_bytes(seed: [u8; 32]) -> Self {
        Self::new(SigningKey::from_bytes(&seed))
    }
}

#[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};

    #[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");
    }
}