Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ jobs:
targets: aarch64-unknown-linux-musl
- name: Build NVRC
run: |
GIT_REV="g$(git rev-parse --short=12 HEAD)"
if [ -n "$(git status --porcelain)" ]; then GIT_REV="${GIT_REV}-dirty"; fi
export GIT_REV
cargo build --release --target=aarch64-unknown-linux-musl
sudo cp ./target/aarch64-unknown-linux-musl/release/NVRC "$ROOTFS_IMAGE_DIR/bin/NVRC-aarch64-unknown-linux-musl"
- name: Create Image with NVRC
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,9 @@ processes, and sockets.
5. **Minimize Rust Crates**: Suggest smaller crates rather then pulling in huge
crates like nix
6. **KISS**: Minimize the cyclomatic complexity, keep code simple and stupid
7. **Self-describing code**: Names carry the WHAT so comments can focus on the
WHY. If you reach for a comment to explain what a function, type, or
variable does, rename it instead. Reserve comments for non-obvious WHY:
hidden constraints, subtle invariants, source citations for magic constants
(e.g. NIST test vectors), or workarounds for specific bugs. Don't restate
what well-named code already says.
72 changes: 72 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ kernlog = "0.3"
rlimit = "0.10.2"
libc = "0.2.178"
once_cell = "1.21.3"
sha2 = { version = "0.10", default-features = false }

[profile.release]
opt-level = "s"
Expand Down
158 changes: 158 additions & 0 deletions src/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) NVIDIA CORPORATION

//! Lets operators correlate dmesg output against the cosign/Rekor digest
//! published in the release evidence bundle, so a running NVRC can be matched
//! to its release artifact independently of the build pipeline (see
//! ARCHITECTURE.md §"Provenance & Supply-Chain Security").
//!
//! Not a security primitive: binary integrity is already enforced before
//! `execve` by dm-verity (block layer), fs-verity (file layer), and IPE
//! (see ARCHITECTURE.md §"Measured Rootfs" and §"Integrity Policy
//! Enforcement"). A compromised NVRC could lie about its own hash — the
//! trustworthy digest is the one in Rekor, not the one in dmesg.

use crate::macros::ResultExt;
use sha2::{Digest, Sha256};
use std::fmt::Write as _;
use std::fs;

// TODO(hardened_std): add to fs path whitelist when hardened_std::fs lands
const SELF_EXE: &str = "/proc/self/exe";
const VERSION: &str = env!("CARGO_PKG_VERSION");

// Untrusted dev-convenience stamp: the short commit the binary was built from,
// plus `-dirty` for an uncommitted tree, set by CI on the build command. Lets a
// dmesg glance tell a dirty/local build from a clean release; absent on a plain
// release build, which logs VERSION alone. A tampered binary can forge this —
// authoritative release identity is the sha256 vs Rekor (see above and
// ARCHITECTURE.md).
const GIT_REV: Option<&str> = option_env!("GIT_REV");

pub fn self_exe() {
info!("{}", version_line());
}
Comment thread
zvonkok marked this conversation as resolved.

pub fn version_line() -> String {
let digest = sha256().or_panic(format_args!("hash {SELF_EXE}"));
boot_line(&digest, GIT_REV)
}

fn boot_line(digest: &str, rev: Option<&str>) -> String {
format!("NVRC version={} sha256={digest}", version(rev))
}

fn version(rev: Option<&str>) -> String {
rev.map_or_else(|| VERSION.to_string(), |rev| format!("{VERSION}+{rev}"))
}

fn sha256() -> std::io::Result<String> {
fs::read(SELF_EXE).map(|data| hex_encode(&Sha256::digest(&data)))
}

fn hex_encode(bytes: &[u8]) -> String {
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_hex_encode_empty() {
assert_eq!(hex_encode(&[]), "");
}

#[test]
fn test_hex_encode_known_vector() {
assert_eq!(hex_encode(&[0x00, 0xff, 0xab, 0x12, 0x9c]), "00ffab129c");
}

#[test]
fn test_sha256_self_returns_64_hex_chars() {
let digest = sha256().expect("hash self");
assert_eq!(digest.len(), 64);
assert!(digest.chars().all(|c| c.is_ascii_hexdigit()));
}

#[test]
fn test_sha256_empty_string_known_vector() {
// NIST: SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
assert_eq!(
hex_encode(&Sha256::digest(b"")),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}

#[test]
fn test_sha256_abc_known_vector() {
// NIST: SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
assert_eq!(
hex_encode(&Sha256::digest(b"abc")),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}

#[test]
fn test_self_exe_runs_to_completion() {
self_exe();
}

#[test]
fn test_version_without_rev_is_bare_cargo_version() {
assert_eq!(version(None), VERSION);
}

#[test]
fn test_version_with_rev_appends_plus_metadata() {
assert_eq!(
version(Some("g3ccba213b033")),
format!("{VERSION}+g3ccba213b033")
);
}

#[test]
fn test_version_with_dirty_rev_preserves_dirty_suffix() {
assert_eq!(
version(Some("g3ccba213b033-dirty")),
format!("{VERSION}+g3ccba213b033-dirty")
);
}

#[test]
fn test_boot_line_of_self_carries_cargo_version_and_real_digest() {
let digest = sha256().expect("hash self");
let line = boot_line(&digest, GIT_REV);

assert!(line.starts_with(&format!("NVRC version={}", env!("CARGO_PKG_VERSION"))));
assert!(line.ends_with(&format!("sha256={digest}")));
assert_eq!(
line,
format!("NVRC version={} sha256={digest}", version(GIT_REV))
);
}

#[test]
fn test_boot_line_release_build_logs_bare_version() {
assert_eq!(
boot_line("deadbeef", None),
format!("NVRC version={} sha256=deadbeef", env!("CARGO_PKG_VERSION"))
);
}

#[test]
fn test_boot_line_dev_build_appends_git_rev() {
assert_eq!(
boot_line("deadbeef", Some("gabc123-dirty")),
format!(
"NVRC version={}+gabc123-dirty sha256=deadbeef",
env!("CARGO_PKG_VERSION")
)
);
}
}
24 changes: 24 additions & 0 deletions src/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) NVIDIA CORPORATION

use crate::hash;

/// NVRC's init duties (mounts, module loads, daemon forks, the poweroff panic
/// hook) would wreck a normal host, so they must only run as PID 1. Anywhere
/// else (CI smoke test, dev shell) report identity and exit before the caller
/// touches anything.
pub fn as_pid1() {
if running_as_init() {
return;
}
// No logger on this path, so print to stdout rather than via the dropped
// log macros; this is the CI smoke test's only observable output.
println!("{}", hash::version_line());
std::process::exit(0);
}

// Raw SYS_getpid syscall: stays on the pure-syscall path hardened_std targets,
// and needs no /proc (unmounted this early, mount::setup runs later).
fn running_as_init() -> bool {
unsafe { libc::syscall(libc::SYS_getpid) == 1 }
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
pub mod config;
pub mod daemon;
pub mod execute;
pub mod hash;
pub mod kata_agent;
pub mod kernel_params;
pub mod kmsg;
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
mod config;
mod daemon;
mod execute;
mod hash;
mod infiniband;
mod init;
mod kata_agent;
mod kernel_params;
mod kmsg;
Expand Down Expand Up @@ -92,13 +94,16 @@ fn mode_nvl5(init: &mut NVRC, fabric_mode: u8) {
}

fn main() {
init::as_pid1();

lockdown::set_panic_hook();
let mut init = NVRC::default();
mount::setup();
net::loopback_up();
kmsg::kernlog_setup();
syslog::poll();
init.process_kernel_params(None);
hash::self_exe();

let detected = mode::detect();
match detected.mode {
Expand Down
Loading