Skip to content

Commit

Permalink
Merge pull request #475 from betrusted-io/prehash
Browse files Browse the repository at this point in the history
Add prehash signature check and prepare for env-variable hash passing
  • Loading branch information
bunnie authored Jan 7, 2024
2 parents 0750644 + 055231c commit 6e65cbd
Show file tree
Hide file tree
Showing 20 changed files with 846 additions and 74 deletions.
16 changes: 16 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 loader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ curve25519-dalek-loader = { path = "curve25519-dalek-loader", default-features =
utralib = { version = "0.1.23", optional = true, default-features = false }
armv7 = { git = "https://github.com/Foundation-Devices/armv7.git", branch = "update", optional = true }
atsama5d27 = { git = "https://github.com/Foundation-Devices/atsama5d27.git", branch = "master", optional = true }
sha2-loader = { path = "./sha2-loader", default-features = false }

[dependencies.com_rs]
git = "https://github.com/betrusted-io/com_rs"
Expand Down
8 changes: 4 additions & 4 deletions loader/ed25519-dalek-loader/src/keypair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,14 @@ impl Keypair {
/// ```
///
/// [rfc8032]: https://tools.ietf.org/html/rfc8032#section-5.1
pub fn verify_prehashed<D>(
pub fn verify_prehashed(
&self,
prehashed_message: D,
// note: this used to be D: Digest<OutputSize = U64>, but because we have only
// one hardware hasher, this has to be finalized before handing down to the function.
prehashed_message: &[u8],
context: Option<&[u8]>,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
D: Digest<OutputSize = U64>,
{
self.public.verify_prehashed(prehashed_message, context, signature)
}
Expand Down
17 changes: 11 additions & 6 deletions loader/ed25519-dalek-loader/src/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use core::convert::TryFrom;
use core::fmt::Debug;

use curve25519_dalek_loader::constants;
use curve25519_dalek_loader::digest::generic_array::typenum::U64;
use curve25519_dalek_loader::digest::Digest;
use curve25519_dalek_loader::edwards::CompressedEdwardsY;
use curve25519_dalek_loader::edwards::EdwardsPoint;
Expand Down Expand Up @@ -179,15 +178,21 @@ impl PublicKey {
///
/// [rfc8032]: https://tools.ietf.org/html/rfc8032#section-5.1
#[allow(non_snake_case)]
pub fn verify_prehashed<D>(
pub fn verify_prehashed(
&self,
prehashed_message: D,
// note: this used to be D: Digest<OutputSize = U64>, but because we have only
// one hardware hasher, this has to be finalized before handing down to the function.
prehashed_message: &[u8],
context: Option<&[u8]>,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
D: Digest<OutputSize = U64>,
{
// This assert does a run-time check on what would otherwise be a static check in Rust
// due to the original type bounds on this function. However, because we have to finalize
// the hash (due to there being only a single hardware hash engine) prior to passing into
// this function, we also erase the type information. This assert somewhat makes up for that.
assert!(prehashed_message.len() == 64, "Incorrect pre-hash length");

let signature = InternalSignature::try_from(signature)?;

let mut h: Sha512 = Sha512::default();
Expand All @@ -205,7 +210,7 @@ impl PublicKey {
h.update(ctx);
h.update(signature.R.as_bytes());
h.update(self.as_bytes());
h.update(prehashed_message.finalize().as_slice());
h.update(prehashed_message);

k = Scalar::from_hash(h);
R = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s);
Expand Down
12 changes: 7 additions & 5 deletions loader/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ pub unsafe extern "C" fn rust_entry(signed_buffer: *const usize, signature: u32)
// kernel args must be validated because tampering with them can change critical assumptions about
// how data is loaded into memory
#[cfg(feature = "secboot")]
if !secboot::validate_xous_img(signed_buffer as *const u32) {
let mut fs_prehash = [0u8; 64];
#[cfg(feature = "secboot")]
if !secboot::validate_xous_img(signed_buffer as *const u32, &mut fs_prehash) {
loop {}
};
// the kernel arg buffer is SIG_BLOCK_SIZE into the signed region
Expand All @@ -95,10 +97,10 @@ pub unsafe extern "C" fn rust_entry(signed_buffer: *const usize, signature: u32)
// But for now, the basic "validate everything as a blob" is perhaps good enough to
// armor code-at-rest against front-line patching attacks.
let kab = KernelArguments::new(arg_buffer);
boot_sequence(kab, signature);
boot_sequence(kab, signature, fs_prehash);
}

fn boot_sequence(args: KernelArguments, _signature: u32) -> ! {
fn boot_sequence(args: KernelArguments, _signature: u32, fs_prehash: [u8; 64]) -> ! {
// Store the initial boot config on the stack. We don't know
// where in heap this memory will go.
#[allow(clippy::cast_ptr_alignment)] // This test only works on 32-bit systems
Expand All @@ -124,7 +126,7 @@ fn boot_sequence(args: KernelArguments, _signature: u32) -> ! {
#[cfg(not(feature = "simulation-only"))]
clear_ram(&mut cfg);
phase_1(&mut cfg);
phase_2(&mut cfg);
phase_2(&mut cfg, &fs_prehash);
#[cfg(feature = "debug-print")]
if VDBG {
check_load(&mut cfg);
Expand All @@ -138,7 +140,7 @@ fn boot_sequence(args: KernelArguments, _signature: u32) -> ! {
println!("No suspend marker found, doing a cold boot!");
clear_ram(&mut cfg);
phase_1(&mut cfg);
phase_2(&mut cfg);
phase_2(&mut cfg, &fs_prehash);
#[cfg(feature = "debug-print")]
if VDBG {
check_load(&mut cfg);
Expand Down
20 changes: 18 additions & 2 deletions loader/src/phase2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::*;
///
/// Set up all the page tables, allocating new root page tables for SATPs and corresponding
/// sub-pages starting from the base of previously copied process data.
pub fn phase_2(cfg: &mut BootConfig) {
pub fn phase_2(cfg: &mut BootConfig, fs_prehash: &[u8; 64]) {
let args = cfg.args;

// This is the offset in RAM where programs are loaded from.
Expand Down Expand Up @@ -38,8 +38,24 @@ pub fn phase_2(cfg: &mut BootConfig) {
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
0x30, 0x41, 0x42, 0x43, 0x44, 0x45, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
];
// Convert the fs_prehash into a hex ascii string, suitable for environment variables
// The initial loader environment is hard-coded, so we use a hard-coded offset for the string
// destination. It was a deliberate decision to not include a generic environment variable
// handler in the loader because we want to keep the loader compact and with a small attack
// surface; a generic environment handling routine would add significant size without much
// benefit.
//
// Note that the main purpose for environment variables is for test & debug tooling, where
// single programs are run in hosted or emulated environments with arguments passed for fast
// debugging. The sole purpose for the environment variable in the loader's context is to
// pass this computed hash onto the userspace environments.
const HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
for (i, &byte) in fs_prehash.iter().enumerate() {
env[env.len() - 128 + i * 2] = HEX_DIGITS[(byte >> 4) as usize];
env[env.len() - 128 + i * 2 + 1] = HEX_DIGITS[(byte & 0xF) as usize];
}

// Go through all Init processes and the kernel, setting up their
// page tables and mapping memory to them.
Expand Down
35 changes: 26 additions & 9 deletions loader/src/secboot.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use crate::println;
use sha2_loader::Sha512;
use ed25519_dalek_loader::Digest;

use crate::SIGBLOCK_SIZE;

const VERSION_STR: &'static str = "Xous OS Loader v0.9.5\n\r";
const VERSION_STR: &'static str = "Xous OS Loader v0.9.6\n\r";
// v0.9.0 -- initial version
// v0.9.1 -- booting with hw acceleration, and "simplest signature" check on the entire xous.img blob
// v0.9.2 -- add version and length check between header and signed area
// v0.9.3 -- add lockout of key ROM in die() routine
// v0.9.4 -- monorepo conversion
// v0.9.5 -- multiplatform conversion and phase 1 optimization
// v0.9.6 -- convert signature check to pre-hash signature (see #472 https://github.com/betrusted-io/xous-core/issues/472)

pub const STACK_LEN: u32 = 8192 - (7 * 4); // 7 words for backup kernel args
pub const STACK_TOP: u32 = 0x4100_0000 - STACK_LEN;
Expand Down Expand Up @@ -268,7 +271,7 @@ impl Keyrom {

// returns true if the kernel is valid
// side-effects the "devboot" register in the gfx engine if devkeys were detected
pub fn validate_xous_img(xous_img_offset: *const u32) -> bool {
pub fn validate_xous_img(xous_img_offset: *const u32, fs_prehash: &mut [u8; 64]) -> bool {
// reset the SHA block, in case we're coming out of a warm reset
let mut sha = CSR::new(utra::sha512::HW_SHA512_BASE as *mut u32);
sha.wfo(utra::sha512::POWER_ON, 1);
Expand Down Expand Up @@ -403,11 +406,18 @@ pub fn validate_xous_img(xous_img_offset: *const u32) -> bool {
let protected_len =
u32::from_le_bytes(image[signed_len as usize - 4..].try_into().unwrap());
// check that the signed versions match the version reported in the header
if sig.version != 1 || (sig.version != protected_version) {
gfx.msg(
"Check fail: mismatch on signature record version numbering.\n\r",
&mut cursor,
);
if sig.version != 2 || (sig.version != protected_version) {
if sig.version == 1 {
gfx.msg(
"v1 signature found, but v2 required.\n\rIs your kernel up to date?\n\r",
&mut cursor,
);
} else {
gfx.msg(
"Check fail: mismatch on signature record version numbering.\n\r",
&mut cursor,
);
}
println!("Check fail: mismatch on signature record version numbering.\n\r");
println!("sig.version: {}", sig.version);
println!("protected_version: {}", protected_version);
Expand Down Expand Up @@ -435,13 +445,20 @@ pub fn validate_xous_img(xous_img_offset: *const u32) -> bool {
}

let ed25519_signature = ed25519_dalek_loader::Signature::from(sig.signature);
use ed25519_dalek_loader::Verifier;
gfx.msg("Checking signature...\n\r", &mut cursor);
if pubkey.verify(image, &ed25519_signature).is_ok() {
let mut h: Sha512 = Sha512::new();
h.update(&image);
// The prehash needs to be finalized before we create a new hasher instance. We
// only have one hardware hasher available.
let prehash = h.finalize();
if pubkey.verify_prehashed(prehash.as_slice(), None, &ed25519_signature).is_ok() {
fs_prehash.copy_from_slice(prehash.as_slice());
gfx.msg("Signature check passed\n\r", &mut cursor);
println!("Signature check passed");
break;
} else {
gfx.msg("Downgrading security...\n\r", &mut cursor);
println!("Downgrading security...");
// signature didn't work out, setup the next key and try it
match keyloc {
KeyLoc::SelfSignPub => {
Expand Down
1 change: 1 addition & 0 deletions services/engine-sha512/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ precursor = ["utralib/precursor"]
hosted = ["utralib/hosted"]
renode = ["utralib/renode"]
event_wait = [] # in theory, event_wait should be more efficient, but the OS overhead is greater than the computation.
std = []
default = []
3 changes: 2 additions & 1 deletion services/engine-sha512/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub(crate) const SERVER_NAME_SHA512: &str = "_Sha512 hardware accelerator server_";
#[allow(dead_code)]
pub(crate) const SERVER_NAME_SHA512: &str = "_Sha512 hardware accelerator server_"; // not used in hosted config
mod rkyv_enum;
pub use rkyv_enum::*;

Expand Down
5 changes: 5 additions & 0 deletions services/engine-sha512/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ pub use api::*;
mod consts;

mod sha256;
#[cfg(target_os = "xous")]
mod sha512;
#[cfg(not(target_os = "xous"))]
mod sha512_hosted;
#[cfg(not(target_os = "xous"))]
use sha512_hosted as sha512;

pub use digest::{self, Digest};
pub use sha256::{Sha224, Sha256};
Expand Down
Loading

0 comments on commit 6e65cbd

Please sign in to comment.