runner-core 0.4.72

Core helpers for Greentic runner: pack resolution, signing, session/state glue, and utilities
Documentation
use std::convert::TryInto;

use anyhow::{Context, Result, anyhow, bail};
use base64::Engine;
use base64::engine::general_purpose::STANDARD_NO_PAD;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};

pub struct PackVerifier {
    key: VerifyingKey,
}

impl PackVerifier {
    pub fn from_env_value(value: &str) -> Result<Self> {
        let (alg, key_b64) = value
            .split_once(':')
            .ok_or_else(|| anyhow!("invalid PACK_PUBLIC_KEY format"))?;
        if !alg.eq_ignore_ascii_case("ed25519") {
            bail!("unsupported public key algorithm `{alg}`");
        }
        let raw = STANDARD_NO_PAD
            .decode(key_b64.trim())
            .or_else(|_| base64::engine::general_purpose::STANDARD.decode(key_b64.trim()))
            .context("PACK_PUBLIC_KEY is not valid base64")?;
        let key_bytes: &[u8; 32] = raw
            .as_slice()
            .try_into()
            .map_err(|_| anyhow!("PACK_PUBLIC_KEY must be 32 bytes"))?;
        let key = VerifyingKey::from_bytes(key_bytes)
            .map_err(|_| anyhow!("PACK_PUBLIC_KEY is not a valid Ed25519 key"))?;
        Ok(Self { key })
    }

    pub fn verify(&self, message: &[u8], signature_b64: &str) -> Result<()> {
        let sig_bytes = STANDARD_NO_PAD
            .decode(signature_b64.trim())
            .or_else(|_| base64::engine::general_purpose::STANDARD.decode(signature_b64.trim()))
            .context("pack signature is not valid base64")?;
        let signature = Signature::from_slice(&sig_bytes)
            .map_err(|_| anyhow!("pack signature is not a valid Ed25519 signature"))?;
        self.key
            .verify(message, &signature)
            .context("pack signature verification failed")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use base64::engine::general_purpose::STANDARD;
    use ed25519_dalek::{Signer, SigningKey};

    #[test]
    fn accepts_valid_signatures() {
        let secret = SigningKey::from_bytes(&[7u8; 32]);
        let public_b64 = STANDARD.encode(secret.verifying_key().as_bytes());
        let verifier = PackVerifier::from_env_value(&format!("ed25519:{public_b64}")).unwrap();
        let message = b"sha256:deadbeef";
        let signature = secret.sign(message);
        let signature_b64 = STANDARD.encode(signature.to_bytes());
        verifier.verify(message, &signature_b64).unwrap();
    }

    #[test]
    fn rejects_invalid_signatures() {
        let secret = SigningKey::from_bytes(&[1u8; 32]);
        let public_b64 = STANDARD.encode(secret.verifying_key().as_bytes());
        let verifier = PackVerifier::from_env_value(&format!("ed25519:{public_b64}")).unwrap();
        let bad_sig = STANDARD.encode([0xAAu8; 64]);
        assert!(verifier.verify(b"msg", &bad_sig).is_err());
    }

    #[test]
    fn rejects_unsupported_public_key_algorithms() {
        let err = PackVerifier::from_env_value("rsa:Zm9v")
            .err()
            .expect("algorithm should be rejected");
        assert!(err.to_string().contains("unsupported public key algorithm"));
    }

    #[test]
    fn rejects_public_keys_with_wrong_length() {
        let err = PackVerifier::from_env_value("ed25519:Zm9v")
            .err()
            .expect("short public key should be rejected");
        assert!(err.to_string().contains("must be 32 bytes"));
    }

    #[test]
    fn rejects_malformed_signature_encoding() {
        let secret = SigningKey::from_bytes(&[9u8; 32]);
        let public_b64 = STANDARD.encode(secret.verifying_key().as_bytes());
        let verifier = PackVerifier::from_env_value(&format!("ed25519:{public_b64}")).unwrap();

        let err = verifier
            .verify(b"msg", "!not-base64!")
            .expect_err("malformed signature should be rejected");
        assert!(err.to_string().contains("not valid base64"));
    }
}