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())
}