Gunnar

nordisk/heimdall

crates/heimdall-core/tests/pinned_key.rs

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB

//! The pinned public key record a caller receives at boot. Heimdall is a
//! defensive sandbox.

mod common;
use common::*;
use heimdall_core::*;

#[test]
fn pinned_key_text_round_trips() {
    let p = key_a().pinned();
    assert_eq!(PinnedKey::from_text(&p.to_text()).unwrap(), p);
}

#[test]
fn pinned_key_text_has_the_documented_shape() {
    let p = key_a().pinned();
    let t = p.to_text();
    assert!(t.ends_with('\n'));
    let parts: Vec<&str> = t.trim_end().split(' ').collect();
    assert_eq!(parts.len(), 5);
    assert_eq!(parts[0], "heimdall-boot-key");
    assert_eq!(parts[1], "v1");
    assert_eq!(parts[2], p.key_id.to_hex());
    assert_eq!(parts[3], p.boot_id.to_hex());
    assert_eq!(parts[4], p.public_key.to_hex());
}

#[test]
fn pinned_key_without_trailing_newline_is_accepted() {
    let p = key_a().pinned();
    assert_eq!(PinnedKey::from_text(p.to_text().trim_end()).unwrap(), p);
}

#[test]
fn pinned_key_with_a_wrong_key_id_is_refused() {
    let mut p = key_a().pinned();
    p.key_id = key_b().key_id();
    assert_eq!(
        PinnedKey::from_text(&p.to_text()).unwrap_err(),
        PinError::KeyIdMismatch
    );
    assert_eq!(Verifier::new(p).unwrap_err(), PinError::KeyIdMismatch);
}

#[test]
fn pinned_key_with_version_two_is_refused() {
    let t = key_a().pinned().to_text().replace(" v1 ", " v2 ");
    assert_eq!(PinnedKey::from_text(&t).unwrap_err(), PinError::BadFormat);
}

#[test]
fn pinned_key_with_a_wrong_label_is_refused() {
    let t = key_a()
        .pinned()
        .to_text()
        .replace("heimdall-boot-key", "other-key");
    assert_eq!(PinnedKey::from_text(&t).unwrap_err(), PinError::BadFormat);
}

#[test]
fn pinned_key_with_short_hex_is_refused() {
    let p = key_a().pinned();
    let t = p.to_text();
    let cut = t.trim_end();
    assert_eq!(
        PinnedKey::from_text(&cut[..cut.len() - 2]).unwrap_err(),
        PinError::BadFormat
    );
}

#[test]
fn pinned_key_with_an_extra_field_is_refused() {
    let t = format!("{} extra\n", key_a().pinned().to_text().trim_end());
    assert_eq!(PinnedKey::from_text(&t).unwrap_err(), PinError::BadFormat);
}

#[test]
fn pinned_key_with_two_lines_is_refused() {
    let t = format!("{0}{0}", key_a().pinned().to_text());
    assert_eq!(PinnedKey::from_text(&t).unwrap_err(), PinError::BadFormat);
}

#[test]
fn pinned_key_with_non_hex_is_refused() {
    let t = key_a().pinned().to_text();
    let bad = format!("{}g\n", &t.trim_end()[..t.trim_end().len() - 1]);
    assert_eq!(PinnedKey::from_text(&bad).unwrap_err(), PinError::BadFormat);
}

#[test]
fn pinned_key_with_double_spaces_is_refused() {
    let t = key_a().pinned().to_text().replacen(' ', "  ", 1);
    assert_eq!(PinnedKey::from_text(&t).unwrap_err(), PinError::BadFormat);
}

#[test]
fn pinned_identity_point_is_refused_as_weak_key() {
    // The identity point (y = 1) is a valid encoding but a weak key: every
    // signature check against it is meaningless.
    let mut pk = [0u8; 32];
    pk[0] = 1;
    let p = PinnedKey {
        key_id: key_id_of(&pk),
        boot_id: BOOT_A,
        public_key: Ed25519PublicKey(pk),
    };
    assert_eq!(Verifier::new(p).unwrap_err(), PinError::BadPublicKey);
    assert_eq!(
        PinnedKey::from_text(&p.to_text()).unwrap_err(),
        PinError::BadPublicKey
    );
}

#[test]
fn pinned_key_that_is_not_a_curve_point_is_refused() {
    // y = 2 is not on the curve.
    let mut pk = [0u8; 32];
    pk[0] = 2;
    let p = PinnedKey {
        key_id: key_id_of(&pk),
        boot_id: BOOT_A,
        public_key: Ed25519PublicKey(pk),
    };
    assert_eq!(Verifier::new(p).unwrap_err(), PinError::BadPublicKey);
}

#[test]
fn key_id_is_domain_separated_sha256_prefix() {
    use sha2::{Digest, Sha256};
    let pk = key_a().pinned().public_key.0;
    let mut h = Sha256::new();
    h.update(KEY_ID_DOMAIN);
    h.update(pk);
    let full: [u8; 32] = h.finalize().into();
    assert_eq!(key_id_of(&pk).0, full[..16]);
    assert_ne!(key_id_of(&pk).0, Sha256::digest(pk)[..16]);
}

#[test]
fn key_id_differs_between_keys() {
    assert_ne!(key_a().key_id(), key_b().key_id());
}

#[test]
fn pinned_key_json_round_trips() {
    let p = key_a().pinned();
    let j = serde_json::to_string(&p).unwrap();
    assert_eq!(serde_json::from_str::<PinnedKey>(&j).unwrap(), p);
}