Gunnar

nordisk/heimdall

guests/verify-bundle/src/cert.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! Signer certificates and signatures, as Holger uses them: an X.509 (DER)
//! certificate carrying a 32-byte Ed25519 key, identity = sha256(DER), detached
//! Ed25519 signatures over exact bytes (`verify_strict`), and a one-level chain
//! (leaf signed by a trusted root that is a CA) under `TrustMode::RootedChain`.
//!
//! Holger checks the chain with `x509-parser`'s ring-backed `verify` feature;
//! ring needs C/asm and does not belong in a pure-Rust guest, so the chain
//! signature is checked here with `ed25519-dalek` over the leaf's exact TBS
//! bytes. Holger's signers and roots are Ed25519 by construction (a non-32-byte
//! key is refused at parse), so this is the same check, not a narrower one.

use chrono::{DateTime, TimeZone, Utc};
use ed25519_dalek::{Signature, VerifyingKey};
use x509_parser::prelude::*;

use crate::digest::sha256_hex;
use crate::model::{CertificateValidity, TrustMode};
use crate::refusal::{Refusal, Result};

/// The Ed25519 signature-algorithm OID, 1.3.101.112.
const OID_ED25519: &str = "1.3.101.112";

#[derive(Debug, Clone)]
pub struct SignerCert {
    der: Vec<u8>,
    fingerprint: String,
    subject: String,
    pubkey: [u8; 32],
    is_ca: bool,
    not_before: DateTime<Utc>,
    not_after: DateTime<Utc>,
}

impl SignerCert {
    /// Parse an X.509 DER certificate carrying a 32-byte Ed25519 key. Trailing
    /// bytes after the certificate are refused (Holger ignores them; a
    /// fingerprint over bytes nobody parsed is a second identity for one key).
    pub fn parse(der: &[u8]) -> Result<Self> {
        let (rest, cert) = X509Certificate::from_der(der)
            .map_err(|_| Refusal::Certificate("cannot parse certificate DER".into()))?;
        if !rest.is_empty() {
            return Err(Refusal::Certificate(
                "trailing bytes after the certificate DER".into(),
            ));
        }
        let key = cert.public_key().subject_public_key.data.as_ref();
        if key.len() != 32 {
            return Err(Refusal::Certificate(
                "signer certificate is not ed25519 (expected a 32-byte key)".into(),
            ));
        }
        let mut pubkey = [0u8; 32];
        pubkey.copy_from_slice(key);
        let not_before = unix_to_utc(cert.validity().not_before.timestamp())
            .ok_or_else(|| Refusal::Certificate("certificate notBefore is out of range".into()))?;
        let not_after = unix_to_utc(cert.validity().not_after.timestamp())
            .ok_or_else(|| Refusal::Certificate("certificate notAfter is out of range".into()))?;
        Ok(SignerCert {
            der: der.to_vec(),
            fingerprint: sha256_hex(der),
            subject: cert.subject().to_string(),
            pubkey,
            is_ca: cert.is_ca(),
            not_before,
            not_after,
        })
    }

    pub fn fingerprint(&self) -> &str {
        &self.fingerprint
    }

    pub fn subject(&self) -> &str {
        &self.subject
    }

    pub fn ensure_valid_at(&self, now: DateTime<Utc>) -> Result<()> {
        if now < self.not_before {
            return Err(Refusal::CertificateNotYetValid {
                fingerprint: self.fingerprint.clone(),
                not_before: self.not_before.to_rfc3339(),
            });
        }
        if now > self.not_after {
            return Err(Refusal::CertificateExpired {
                fingerprint: self.fingerprint.clone(),
                not_after: self.not_after.to_rfc3339(),
            });
        }
        Ok(())
    }

    pub fn ensure_valid_under(
        &self,
        now: DateTime<Utc>,
        posture: CertificateValidity,
    ) -> Result<()> {
        match posture {
            CertificateValidity::Enforce => self.ensure_valid_at(now),
            CertificateValidity::IgnoreUnreliableClock => Ok(()),
        }
    }

    /// Verify a detached Ed25519 signature over `msg` (strict: no malleable or
    /// small-order forms).
    pub fn verify(&self, msg: &[u8], sig: &[u8]) -> Result<()> {
        verify_ed25519(&self.pubkey, msg, sig)
    }
}

pub fn verify_ed25519(pubkey: &[u8; 32], msg: &[u8], sig: &[u8]) -> Result<()> {
    let sig: [u8; 64] = sig
        .try_into()
        .map_err(|_| Refusal::Certificate("signature length is not 64 bytes".into()))?;
    let vk = VerifyingKey::from_bytes(pubkey)
        .map_err(|_| Refusal::Certificate("invalid ed25519 public key".into()))?;
    vk.verify_strict(msg, &Signature::from_bytes(&sig))
        .map_err(|_| Refusal::Certificate("signature verification failed".into()))
}

fn unix_to_utc(secs: i64) -> Option<DateTime<Utc>> {
    Utc.timestamp_opt(secs, 0).single()
}

/// True if `leaf` is anchored under `mode` (Holger's `chains_to_root`).
pub fn chains_to_root(leaf: &SignerCert, roots: &[SignerCert], mode: TrustMode) -> bool {
    if mode == TrustMode::PinnedSigners {
        return true;
    }
    if roots.is_empty() {
        return false;
    }
    if roots.iter().any(|r| r.fingerprint == leaf.fingerprint) {
        return true;
    }
    let Ok((_, leaf_cert)) = X509Certificate::from_der(&leaf.der) else {
        return false;
    };
    if leaf_cert.signature_algorithm.algorithm.to_id_string() != OID_ED25519 {
        return false;
    }
    let tbs: &[u8] = leaf_cert.tbs_certificate.as_ref();
    let sig = leaf_cert.signature_value.data.as_ref();
    roots
        .iter()
        .any(|root| root.is_ca && verify_ed25519(&root.pubkey, tbs, sig).is_ok())
}