vyre-sigstore 0.1.0

Keyless sigstore signing + verification for vyre conformance certificates
Documentation
#![deny(unsafe_code)]
#![deny(missing_docs)]

//! # vyre-sigstore — ed25519 signing + verification for vyre conformance certificates
//!
//! This crate operates on serialized certificate bytes and ed25519 keys. It has
//! zero dependency on `vyre-conform` internals — downstream auditors can verify
//! certificates without pulling the maintainer harness.
//!
//! ## Example
//!
//! ```ignore
//! use vyre_sigstore::{sign, verify, SigningKey};
//! use rand::rngs::OsRng;
//!
//! let key = SigningKey::generate(&mut OsRng);
//! let cert_bytes = std::fs::read("certificate.cbor")?;
//!
//! let signature = sign(&cert_bytes, &key);
//! verify(&cert_bytes, &signature, &key.verifying_key())?;
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```

use ed25519_dalek::{Signer, Verifier};
use thiserror::Error;

pub use ed25519_dalek::{Signature, SigningKey, VerifyingKey};

/// Verification failure modes.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VerifyError {
    /// The signature does not verify against the public key and certificate
    /// bytes. Either the cert has been tampered, the signature is stale, or
    /// the wrong public key was supplied.
    #[error("Fix: signature does not match certificate + public key. Check cert bytes are unmodified and public key matches the signing key.")]
    BadSignature,
}

/// Sign certificate bytes with an ed25519 key.
///
/// The returned [`Signature`] is detached — pair it with the certificate
/// bytes and the matching [`VerifyingKey`] to verify.
#[must_use]
pub fn sign(cert_bytes: &[u8], key: &SigningKey) -> Signature {
    key.sign(cert_bytes)
}

/// Verify a detached signature against certificate bytes and a public key.
///
/// # Errors
/// Returns [`VerifyError::BadSignature`] if the signature does not match.
pub fn verify(
    cert_bytes: &[u8],
    sig: &Signature,
    pubkey: &VerifyingKey,
) -> Result<(), VerifyError> {
    pubkey
        .verify(cert_bytes, sig)
        .map_err(|_| VerifyError::BadSignature)
}

/// Compute the canonical blake3 digest of certificate bytes.
///
/// Callers sign this digest rather than raw bytes when the certificate format
/// may evolve: the signature stays stable across cert-format migrations that
/// preserve semantic content.
#[must_use]
pub fn canonical_digest(cert_bytes: &[u8]) -> [u8; 32] {
    *blake3::hash(cert_bytes).as_bytes()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn sample_key() -> SigningKey {
        // deterministic key for tests — not for production use
        let seed: [u8; 32] = [7u8; 32];
        SigningKey::from_bytes(&seed)
    }

    #[test]
    fn round_trip_signs_and_verifies() {
        let key = sample_key();
        let cert = b"example certificate bytes";
        let sig = sign(cert, &key);
        verify(cert, &sig, &key.verifying_key()).expect("Fix: signature should verify");
    }

    #[test]
    fn tampered_cert_fails_verify() {
        let key = sample_key();
        let cert = b"original bytes";
        let sig = sign(cert, &key);
        let tampered = b"tampered bytes";
        let err = verify(tampered, &sig, &key.verifying_key()).unwrap_err();
        assert!(matches!(err, VerifyError::BadSignature));
    }

    #[test]
    fn canonical_digest_is_stable() {
        let a = canonical_digest(b"same input");
        let b = canonical_digest(b"same input");
        assert_eq!(a, b);
    }
}