vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Ed25519 signing and verification for [`Certificate`].
//!
//! A conformance certificate is only trustworthy if a consumer can
//! prove which backend produced it and that the JSON has not been
//! modified after the run. Plain JSON — even the canonical form
//! produced by `to_json` — carries no such proof; a tampered
//! "passed" record is indistinguishable from a real one.
//!
//! This module supplies the narrow signing contract:
//!
//! * [`canonical_bytes`] serializes a certificate deterministically
//!   so signers and verifiers agree on exactly which bytes get
//!   signed. Field order is fixed by the `Certificate`
//!   struct layout; `serde_json` preserves that order for structs.
//! * [`sign`] produces an Ed25519 signature over those bytes.
//! * [`verify`] returns `Ok(())` when the signature is valid for
//!   the supplied public key and the byte exact canonicalization of
//!   the certificate, or `Err(SignatureError)` otherwise.
//!
//! Key provisioning is intentionally out of scope. The caller
//! supplies a `SigningKey` / `VerifyingKey` pair — typically
//! a deployment surface like KMS, an HSM, or sigstore keyless
//! signing. The repository must never carry a private key, per the
//! "never store credentials in project files" invariant.

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};

use super::Certificate;

/// Error returned from [`verify`] when the signature does not
/// authenticate the certificate.
#[derive(Debug)]
pub struct SignatureError {
    inner: String,
}

impl core::fmt::Display for SignatureError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "Fix: ed25519 signature failed to verify: {}", self.inner)
    }
}

impl std::error::Error for SignatureError {}

/// Serialize the certificate into the exact byte sequence that must
/// be signed. Identical to [`super::report::to_json`] modulo error
/// handling and pretty-print settings. The output is compact
/// (`serde_json::to_vec`) because a cryptographic signature only
/// needs bytewise stability, not human readability.
///
/// # Errors
///
/// Returns the underlying `serde_json::Error` if the certificate
/// contains a value that cannot be serialized (should never happen
/// for the types in `Certificate`).
#[inline]
pub fn canonical_bytes(cert: &Certificate) -> Result<Vec<u8>, serde_json::Error> {
    serde_json::to_vec(cert)
}

/// Sign the canonical bytes of `cert` with `signing_key`.
///
/// # Errors
///
/// Returns `serde_json::Error` if the certificate fails to
/// serialize. Ed25519 signing itself is infallible in `dalek` for
/// well-formed keys.
#[inline]
pub fn sign(cert: &Certificate, signing_key: &SigningKey) -> Result<Signature, serde_json::Error> {
    let bytes = canonical_bytes(cert)?;
    Ok(signing_key.sign(&bytes))
}

/// Verify `signature` against the canonical bytes of `cert` using
/// `verifying_key`.
///
/// # Errors
///
/// Returns [`SignatureError`] if the signature does not verify or
/// the certificate fails to canonicalize.
#[inline]
pub fn verify(
    cert: &Certificate,
    signature: &Signature,
    verifying_key: &VerifyingKey,
) -> Result<(), SignatureError> {
    let bytes = canonical_bytes(cert).map_err(|err| SignatureError {
        inner: err.to_string(),
    })?;
    verifying_key
        .verify(&bytes, signature)
        .map_err(|err| SignatureError {
            inner: err.to_string(),
        })
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::SigningKey;
    use std::collections::BTreeMap;

    /// Deterministic SigningKey for tests. Seeded from a fixed byte
    /// pattern so the test does not pull in a randomness source.
    fn test_signing_key() -> SigningKey {
        SigningKey::from_bytes(&[7u8; 32])
    }

    /// Build a minimal certificate directly (bypassing the full
    /// `certify()` pipeline) so signing tests do not wait for the
    /// workspace-wide GPU-mandatory scan on every iteration. The
    /// field contents are unimportant for signing semantics — the
    /// bytes-in / bytes-out round trip is what's being exercised.
    fn fixture(backend_name: &str) -> Certificate {
        use super::super::{CertificateLevels, CertificateStrength, CoverageMetrics, TrackReport};
        let levels = CertificateLevels {
            integer: None,
            float: None,
            approximate: None,
        };
        let track = TrackReport {
            level: None,
            ops: Vec::new(),
            unsupported_ops: Vec::new(),
            coverage: CoverageMetrics::default(),
        };
        Certificate::new(
            backend_name.to_string(),
            "ed25519-test".to_string(),
            1,
            levels,
            "2026-04-17T00:00:00Z".to_string(),
            0,
            CertificateStrength::FastCheck,
            0,
            BTreeMap::new(),
            "EXPLORATORY -- NOT A PROOF".to_string(),
            track,
            None,
            None,
            Vec::new(),
            Vec::new(),
            Vec::new(),
            [0u8; 32],
            CoverageMetrics::default(),
        )
    }

    #[test]
    fn sign_then_verify_round_trips() {
        let cert = fixture("ed25519-mirror");
        let key = test_signing_key();
        let signature = sign(&cert, &key).expect("signing must succeed");
        verify(&cert, &signature, &key.verifying_key()).expect("honest signature must verify");
    }

    #[test]
    fn verify_rejects_tampered_certificate() {
        let original = fixture("ed25519-mirror");
        let key = test_signing_key();
        let signature = sign(&original, &key).expect("signing must succeed");

        // Build a second certificate with a different backend name;
        // the canonical bytes diverge, so the original signature
        // must NOT verify the new one. This proves the
        // tamper-detection path actually fires — the signature is
        // load-bearing, not a decorative field.
        let tampered = fixture("ed25519-tampered");

        let err = verify(&tampered, &signature, &key.verifying_key())
            .expect_err("signature from original cert must NOT verify the tampered one");
        assert!(
            err.to_string().to_lowercase().contains("signature"),
            "error must name the verification failure, got: {err}"
        );
    }

    #[test]
    fn canonical_bytes_are_deterministic() {
        let cert = fixture("ed25519-mirror");
        let a = canonical_bytes(&cert).expect("serialize a");
        let b = canonical_bytes(&cert).expect("serialize b");
        assert_eq!(a, b, "canonical serialization must be deterministic");
    }
}