mirror of
https://github.com/saymrwulf/xous-core.git
synced 2026-11-13 07:48:30 +00:00
3392 lines
153 KiB
Rust
3392 lines
153 KiB
Rust
mod oracle;
|
|
use oracle::*;
|
|
mod keywrap;
|
|
use core::cell::RefCell;
|
|
use core::convert::TryInto;
|
|
use core::mem::size_of;
|
|
use core::num::NonZeroUsize;
|
|
|
|
use aes::cipher::{BlockDecrypt, BlockEncrypt};
|
|
use aes::Aes256;
|
|
use cipher::generic_array::GenericArray;
|
|
use curve25519_dalek::scalar::Scalar;
|
|
use curve25519_dalek::{edwards::CompressedEdwardsY, EdwardsPoint};
|
|
use ed25519_dalek::{DigestSigner, Signature, Signer, SigningKey, VerifyingKey};
|
|
use gam::modal::{ActionType, Modal, ProgressBar, Slider};
|
|
use graphics_server::BulkRead;
|
|
use keywrap::*;
|
|
use locales::t;
|
|
use num_traits::*;
|
|
pub use oracle::FpgaKeySource;
|
|
use rand_core::RngCore;
|
|
use root_keys::key2bits::*;
|
|
use sha2::{Digest, Sha256, Sha512Hw, Sha512Sw, Sha512_256Sw};
|
|
use utralib::generated::*;
|
|
use xous::KERNEL_BACKUP_OFFSET;
|
|
use xous_semver::SemVer;
|
|
|
|
use crate::api::PasswordType;
|
|
use crate::bcrypt::*;
|
|
use crate::sha512_digest::Sha512Prehash;
|
|
use crate::{api::*, backups};
|
|
use crate::{GatewareRegion, MetadataInFlash, SignatureResult, UpdateType};
|
|
|
|
// TODO: add hardware acceleration for BCRYPT so we can hit the OWASP target without excessive UX delay
|
|
const BCRYPT_COST: u32 = 7; // 10 is the minimum recommended by OWASP; takes 5696 ms to verify @ 10 rounds; 804 ms to verify 7 rounds
|
|
|
|
/// Maximum number of times the global rollback limiter can be updated. Every time this is updated,
|
|
/// the firmware has to be re-signed, the gateware ROM re-injected, and the PDDB system key updated.
|
|
///
|
|
/// N.B.: As of Xous 0.9.6 we don't have a call to update the anti-rollback count, we have only provisioned
|
|
/// for that call to exist sometime in the future.
|
|
const MAX_ROLLBACK_LIMIT: u8 = 255;
|
|
|
|
/// Size of the total area allocated for signatures. It is equal to the size of one FLASH sector, which is the
|
|
/// smallest increment that can be erased.
|
|
const SIGBLOCK_SIZE: u32 = 0x1000;
|
|
/// location of the csr.csv that's appended on gateware images, used for USB updates.
|
|
const METADATA_OFFSET: usize = 0x27_6000;
|
|
#[allow(dead_code)]
|
|
/// location of the csr.csv that's appended on gateware images, used for USB updates.
|
|
const CSR_CSV_OFFSET: usize = 0x27_7000;
|
|
/// offset of the gateware self-signature area
|
|
const SELFSIG_OFFSET: usize = 0x27_F000;
|
|
|
|
/// This structure is mapped into the password cache page and can be zero-ized at any time
|
|
/// we avoid using fancy Rust structures because everything has to "make sense" after a forced zero-ization
|
|
/// The "password" here is generated as follows:
|
|
/// `user plaintext (up to first 72 bytes) -> bcrypt (24 bytes) -> Sha512_256 -> [u8; 32]`
|
|
/// The final Sha512_256 expansion is because we will use this to XOR against secret keys stored in
|
|
/// the KEYROM that may be up to 256 bits in length. For shorter keys, the hashed password is simply
|
|
/// truncated.
|
|
#[repr(C)]
|
|
struct PasswordCache {
|
|
hashed_boot_pw: [u8; 32],
|
|
hashed_boot_pw_valid: u32, // non-zero for valid
|
|
hashed_update_pw: [u8; 32],
|
|
hashed_update_pw_valid: u32,
|
|
fpga_key: [u8; 32],
|
|
fpga_key_valid: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct SignatureInFlash {
|
|
pub version: u32,
|
|
pub signed_len: u32,
|
|
pub signature: [u8; 64],
|
|
}
|
|
pub enum SignatureType {
|
|
Loader,
|
|
Gateware,
|
|
Kernel,
|
|
}
|
|
|
|
struct KeyRomLocs {}
|
|
#[allow(dead_code)]
|
|
impl KeyRomLocs {
|
|
const CONFIG: u8 = 0xff;
|
|
const DEVELOPER_PUBKEY: u8 = 0x18;
|
|
const FPGA_KEY: u8 = 0x00;
|
|
const FPGA_MIN_REV: u8 = 0xfc;
|
|
const GLOBAL_ROLLBACK: u8 = 0xfe;
|
|
const LOADER_MIN_REV: u8 = 0xfd;
|
|
const PEPPER: u8 = 0xf8;
|
|
const SELFSIGN_PRIVKEY: u8 = 0x08;
|
|
const SELFSIGN_PUBKEY: u8 = 0x10;
|
|
const THIRDPARTY_PUBKEY: u8 = 0x20;
|
|
const USER_KEY: u8 = 0x28;
|
|
}
|
|
|
|
pub struct KeyField {
|
|
mask: u32,
|
|
offset: u32,
|
|
}
|
|
impl KeyField {
|
|
pub const fn new(width: u32, offset: u32) -> Self {
|
|
let mask = (1 << width) - 1;
|
|
KeyField { mask, offset }
|
|
}
|
|
|
|
pub fn ms(&self, value: u32) -> u32 {
|
|
let ms_le = (value & self.mask) << self.offset;
|
|
ms_le.to_be()
|
|
}
|
|
}
|
|
#[allow(dead_code)]
|
|
pub(crate) mod keyrom_config {
|
|
use crate::KeyField;
|
|
pub const VERSION_MINOR: KeyField = KeyField::new(8, 0);
|
|
pub const VERSION_MAJOR: KeyField = KeyField::new(8, 8);
|
|
pub const DEVBOOT_DISABLE: KeyField = KeyField::new(1, 16);
|
|
pub const ANTIROLLBACK_ENA: KeyField = KeyField::new(1, 17);
|
|
pub const ANTIROLLFORW_ENA: KeyField = KeyField::new(1, 18);
|
|
pub const FORWARD_REV_LIMIT: KeyField = KeyField::new(4, 19);
|
|
pub const FORWARD_MINOR_LIMIT: KeyField = KeyField::new(4, 23);
|
|
pub const INITIALIZED: KeyField = KeyField::new(1, 27);
|
|
}
|
|
|
|
/// helper routine that will reverse the order of bits. uses a divide-and-conquer approach.
|
|
pub(crate) fn bitflip(input: &[u8], output: &mut [u8]) {
|
|
assert!((input.len() % 4 == 0) && (output.len() % 4 == 0) && (input.len() == output.len()));
|
|
for (src, dst) in input.chunks(4).into_iter().zip(output.chunks_mut(4).into_iter()) {
|
|
let mut word = u32::from_le_bytes(src.try_into().unwrap()); // read in as LE
|
|
word = ((word >> 1) & 0x5555_5555) | ((word & (0x5555_5555)) << 1);
|
|
word = ((word >> 2) & 0x3333_3333) | ((word & (0x3333_3333)) << 2);
|
|
word = ((word >> 4) & 0x0F0F_0F0F) | ((word & (0x0F0F_0F0F)) << 4);
|
|
// copy out as BE, this performs the final byte-level swap needed by the divde-and-conquer algorithm
|
|
for (&s, d) in word.to_be_bytes().iter().zip(dst.iter_mut()) {
|
|
*d = s;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct RootKeys {
|
|
keyrom: utralib::CSR<u32>,
|
|
gateware_mr: xous::MemoryRange,
|
|
gateware_base: u32,
|
|
staging_mr: xous::MemoryRange,
|
|
staging_base: u32,
|
|
loader_code_mr: xous::MemoryRange,
|
|
loader_code_base: u32,
|
|
/// Window to the signature region of the kernel
|
|
kernel_sig_mr: xous::MemoryRange,
|
|
/// Window for backup data page
|
|
kernel_backup_mr: xous::MemoryRange,
|
|
/// Window for end of kernel. Needed for updating the appended kernel length field in the signing
|
|
/// process.
|
|
kernel_end_mr: xous::MemoryRange,
|
|
kernel_end_base_offset: u32,
|
|
kernel_base: u32,
|
|
/// regions of RAM that holds all plaintext passwords, keys, and temp data. stuck in two well-defined
|
|
/// page so we can zero-ize it upon demand, without guessing about stack frames and/or Rust
|
|
/// optimizers removing writes
|
|
sensitive_data: RefCell<xous::MemoryRange>, /* this gets purged at least on every suspend, but ideally
|
|
* purged sooner than that */
|
|
pass_cache: xous::MemoryRange, // this can be purged based on a policy, as set below
|
|
boot_password_policy: PasswordRetentionPolicy,
|
|
update_password_policy: PasswordRetentionPolicy,
|
|
cur_password_type: Option<PasswordType>, /* for tracking which password we're dealing with at the UX
|
|
* layer */
|
|
susres: susres::Susres, // for disabling suspend/resume
|
|
trng: trng::Trng,
|
|
gfx: graphics_server::Gfx, // for reading out font planes for signing verification
|
|
spinor: spinor::Spinor,
|
|
ticktimer: ticktimer_server::Ticktimer,
|
|
gam: gam::Gam,
|
|
xns: xous_names::XousNames,
|
|
jtag: jtag::Jtag,
|
|
fake_key: [u8; 32], /* a base set of random numbers used to respond to invalid keyloc requests in AES
|
|
* operations */
|
|
restore_running: bool,
|
|
}
|
|
|
|
impl<'a> RootKeys {
|
|
pub fn new() -> RootKeys {
|
|
let xns = xous_names::XousNames::new().unwrap();
|
|
let keyrom = xous::syscall::map_memory(
|
|
xous::MemoryAddress::new(utra::keyrom::HW_KEYROM_BASE),
|
|
None,
|
|
4096,
|
|
xous::MemoryFlags::R | xous::MemoryFlags::W,
|
|
)
|
|
.expect("couldn't map keyrom CSR range");
|
|
// read-only memory maps. even if we don't refer to them, we map them into our process
|
|
// so that no other processes can claim them
|
|
let gateware = xous::syscall::map_memory(
|
|
Some(NonZeroUsize::new((xous::SOC_MAIN_GW_LOC + xous::FLASH_PHYS_BASE) as usize).unwrap()),
|
|
None,
|
|
xous::SOC_MAIN_GW_LEN as usize,
|
|
xous::MemoryFlags::R,
|
|
)
|
|
.expect("couldn't map in the SoC gateware region");
|
|
let staging = xous::syscall::map_memory(
|
|
Some(NonZeroUsize::new((xous::SOC_STAGING_GW_LOC + xous::FLASH_PHYS_BASE) as usize).unwrap()),
|
|
None,
|
|
xous::SOC_STAGING_GW_LEN as usize,
|
|
xous::MemoryFlags::R,
|
|
)
|
|
.expect("couldn't map in the SoC staging region");
|
|
let loader_code = xous::syscall::map_memory(
|
|
Some(NonZeroUsize::new((xous::LOADER_LOC + xous::FLASH_PHYS_BASE) as usize).unwrap()),
|
|
None,
|
|
xous::LOADER_CODE_LEN as usize,
|
|
xous::MemoryFlags::R,
|
|
)
|
|
.expect("couldn't map in the loader code region");
|
|
let kernel_sig = xous::syscall::map_memory(
|
|
Some(NonZeroUsize::new((xous::KERNEL_LOC + xous::FLASH_PHYS_BASE) as usize).unwrap()),
|
|
None,
|
|
4096, // map just the signature header region, so we can use it to check values
|
|
xous::MemoryFlags::R,
|
|
)
|
|
.expect("couldn't map in the kernel signature region");
|
|
let kernel_backup = xous::syscall::map_memory(
|
|
Some(
|
|
NonZeroUsize::new((xous::KERNEL_LOC + xous::FLASH_PHYS_BASE + KERNEL_BACKUP_OFFSET) as usize)
|
|
.unwrap(),
|
|
),
|
|
None,
|
|
4096, // map just the backup block
|
|
xous::MemoryFlags::R,
|
|
)
|
|
.expect("couldn't map in the kernel backup region");
|
|
// This is a good assumption, but calling it out here -- we're mapping a region that is assumed to
|
|
// match HEADER_TOTAL_SIZE, but part of the reason we hard code the number is because
|
|
// KERNEL_BACKUP_OFFSET is in the xous crate (maybe it shouldn't be?) and is hard-coded to
|
|
// 4096 (0x1000), so for now we have this assert to catch a consistency problem in assumptions
|
|
// down the road.
|
|
assert!(HEADER_TOTAL_SIZE == 4096);
|
|
// determine the ostensible length of the kernel from an unprotected length hint. This is
|
|
// later verified to match the signed version that is contained within the block that is mapped
|
|
// in this address.
|
|
let kernel_region = unsafe { kernel_sig.as_slice::<u8>() };
|
|
let sig_region = &kernel_region[..core::mem::size_of::<SignatureInFlash>()];
|
|
let sig_rec: &SignatureInFlash =
|
|
unsafe { (sig_region.as_ptr() as *const SignatureInFlash).as_ref().unwrap() }; // this pointer better not be null, we just created it!
|
|
let kernel_len = sig_rec.signed_len as usize; // this length is an unchecked guideline
|
|
let map_len = 0x2000; // map two blocks, because the record could straddle the block boundary
|
|
let kernel_end_base_offset = (kernel_len + SIGBLOCK_SIZE as usize - 8) & !(map_len - 1);
|
|
let kernel_end_mr = xous::syscall::map_memory(
|
|
Some(
|
|
NonZeroUsize::new(
|
|
(xous::KERNEL_LOC + xous::FLASH_PHYS_BASE) as usize + kernel_end_base_offset,
|
|
)
|
|
.unwrap(),
|
|
),
|
|
None,
|
|
map_len,
|
|
xous::MemoryFlags::R,
|
|
)
|
|
.expect("couldn't map in the kernel end region");
|
|
|
|
let mut sensitive_data =
|
|
xous::syscall::map_memory(None, None, 0x1000, xous::MemoryFlags::R | xous::MemoryFlags::W)
|
|
.expect("couldn't map sensitive data page");
|
|
let mut pass_cache =
|
|
xous::syscall::map_memory(None, None, 0x1000, xous::MemoryFlags::R | xous::MemoryFlags::W)
|
|
.expect("couldn't map sensitive data page");
|
|
// make sure the caches start out as zeros
|
|
for w in unsafe { pass_cache.as_slice_mut::<u32>().iter_mut() } {
|
|
*w = 0;
|
|
}
|
|
for w in unsafe { sensitive_data.as_slice_mut::<u32>().iter_mut() } {
|
|
*w = 0;
|
|
}
|
|
|
|
let spinor = spinor::Spinor::new(&xns).expect("couldn't connect to spinor server");
|
|
spinor
|
|
.register_soc_token()
|
|
.expect("couldn't register rootkeys as the one authorized writer to the gateware update area!");
|
|
let jtag = jtag::Jtag::new(&xns).expect("couldn't connect to JTAG server");
|
|
let trng = trng::Trng::new(&xns).expect("couldn't connect to TRNG server");
|
|
// respond to invalid key indices with a "fake" AES key. We try to foil attempts to "probe out" the
|
|
// oracle to discover the presence of null keys.
|
|
let mut fake_key: [u8; 32] = [0; 32];
|
|
for k in fake_key.chunks_exact_mut(8) {
|
|
k.clone_from_slice(&trng.get_u64().unwrap().to_be_bytes());
|
|
}
|
|
|
|
let keys = RootKeys {
|
|
keyrom: CSR::new(keyrom.as_mut_ptr() as *mut u32),
|
|
gateware_mr: gateware,
|
|
gateware_base: xous::SOC_MAIN_GW_LOC,
|
|
staging_mr: staging,
|
|
staging_base: xous::SOC_STAGING_GW_LOC,
|
|
loader_code_mr: loader_code,
|
|
loader_code_base: xous::LOADER_LOC,
|
|
kernel_sig_mr: kernel_sig,
|
|
kernel_backup_mr: kernel_backup,
|
|
kernel_end_mr,
|
|
kernel_end_base_offset: kernel_end_base_offset as u32,
|
|
kernel_base: xous::KERNEL_LOC,
|
|
sensitive_data: RefCell::new(sensitive_data),
|
|
pass_cache,
|
|
update_password_policy: PasswordRetentionPolicy::AlwaysPurge,
|
|
boot_password_policy: PasswordRetentionPolicy::AlwaysKeep,
|
|
cur_password_type: None,
|
|
susres: susres::Susres::new_without_hook(&xns).expect("couldn't connect to susres without hook"),
|
|
trng,
|
|
gfx: graphics_server::Gfx::new(&xns).expect("couldn't connect to gfx"),
|
|
spinor,
|
|
ticktimer: ticktimer_server::Ticktimer::new().expect("couldn't connect to ticktimer"),
|
|
gam: gam::Gam::new(&xns).expect("couldn't connect to GAM"),
|
|
xns,
|
|
jtag,
|
|
fake_key,
|
|
restore_running: false,
|
|
};
|
|
/*
|
|
// dumps the key enclave -- in a format for Renode integration. Or if you just wanted to steal all the keys.
|
|
// PS: you still need to know the passwords to decrypt the keys
|
|
for i in 0..256 {
|
|
keys.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, i);
|
|
log::info!("this.data[0x{:x}] = 0x{:x};", i, keys.keyrom.rf(utra::keyrom::DATA_DATA));
|
|
} */
|
|
|
|
keys
|
|
}
|
|
|
|
pub fn gateware(&self) -> &[u8] { unsafe { self.gateware_mr.as_slice::<u8>() } }
|
|
|
|
pub fn gateware_base(&self) -> u32 { self.gateware_base }
|
|
|
|
pub fn staging(&self) -> &[u8] { unsafe { self.staging_mr.as_slice::<u8>() } }
|
|
|
|
pub fn staging_base(&self) -> u32 { self.staging_base }
|
|
|
|
pub fn loader_code(&self) -> &[u8] { unsafe { self.loader_code_mr.as_slice::<u8>() } }
|
|
|
|
pub fn loader_base(&self) -> u32 { self.loader_code_base }
|
|
|
|
pub fn kernel_sig(&self) -> &[u8] { unsafe { self.kernel_sig_mr.as_slice::<u8>() } }
|
|
|
|
pub fn kernel_backup(&self) -> &[u8] { unsafe { self.kernel_backup_mr.as_slice::<u8>() } }
|
|
|
|
pub fn kernel_end(&self) -> &[u8] { unsafe { self.kernel_end_mr.as_slice::<u8>() } }
|
|
|
|
pub fn kernel_hash(&self) -> Sha512Prehash {
|
|
let mut ph = Sha512Prehash::new();
|
|
if let Some(hash_str) =
|
|
std::env::vars().find(|(key, _value)| key == "ROOT_FILESYSTEM_HASH").map(|(_key, value)| value)
|
|
{
|
|
log::info!("loader-generated hash environment string: {}", hash_str);
|
|
let hash =
|
|
hex::decode(hash_str).expect("Couldn't decode loader hash from environment variables!");
|
|
for &b in hash.iter() {
|
|
if b != 0 {
|
|
ph.set_prehash(hash[..64].try_into().unwrap());
|
|
return ph;
|
|
}
|
|
}
|
|
} else {
|
|
panic!("No hash was set by the loader!");
|
|
}
|
|
panic!("Hash was found, but invalid (contains default value)");
|
|
}
|
|
|
|
pub fn kernel_sig_base(&self) -> u32 { self.kernel_base }
|
|
|
|
pub fn kernel_backup_base(&self) -> u32 { self.kernel_base + KERNEL_BACKUP_OFFSET }
|
|
|
|
pub fn kernel_end_base(&self) -> u32 { self.kernel_base + self.kernel_end_base_offset }
|
|
|
|
/// takes a root key and computes the current rollback state of the key by hashing it
|
|
/// MAX_ROLLBACK_LIMIT - GLOBAL_ROLLBACK times.
|
|
fn compute_key_rollback(&mut self, key: &mut [u8]) {
|
|
assert!(key.len() == 32, "Key length is incorrect");
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, KeyRomLocs::GLOBAL_ROLLBACK as u32);
|
|
let mut rollback_limit = self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
if rollback_limit > 255 {
|
|
rollback_limit = 255;
|
|
} // prevent increment-up attacks that roll over
|
|
log::debug!("rollback_limit: {}", rollback_limit);
|
|
for _i in 0..MAX_ROLLBACK_LIMIT - rollback_limit as u8 {
|
|
let mut hasher = Sha512_256Sw::new();
|
|
hasher.update(&key);
|
|
let digest = hasher.finalize();
|
|
assert!(digest.len() == 32, "Digest had an incorrect length");
|
|
key.copy_from_slice(&digest);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
if _i >= (MAX_ROLLBACK_LIMIT - 3) {
|
|
log::info!("iter {} key {:x?}", _i, key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This implementation creates and destroys the AES key schedule on every function call
|
|
/// However, Rootkey operations are not meant to be used for streaming operations; they are typically
|
|
/// used to secure subkeys, so a bit of overhead on each call is OK in order to not keep excess secret
|
|
/// data laying around.
|
|
/// ASSUME: the caller has confirmed that the user password is valid and in cache
|
|
pub fn aes_op(&mut self, key_index: u8, op_type: AesOpType, block: &mut [u8; 16]) {
|
|
use aes::cipher::KeyInit;
|
|
let mut key = match key_index {
|
|
KeyRomLocs::USER_KEY => {
|
|
let mut key_enc = self.read_key_256(KeyRomLocs::USER_KEY);
|
|
let pcache: &PasswordCache = unsafe { &*(self.pass_cache.as_ptr() as *const PasswordCache) };
|
|
if pcache.hashed_boot_pw_valid == 0 {
|
|
self.purge_password(PasswordType::Boot);
|
|
log::warn!("boot password isn't valid! Returning bogus results.");
|
|
}
|
|
for (key, &pw) in key_enc.iter_mut().zip(pcache.hashed_boot_pw.iter()) {
|
|
*key = *key ^ pw;
|
|
}
|
|
if self.boot_password_policy == PasswordRetentionPolicy::AlwaysPurge {
|
|
self.purge_password(PasswordType::Boot);
|
|
}
|
|
key_enc
|
|
}
|
|
_ => {
|
|
// within a single boot, return a stable, non-changing fake key based off of a single root
|
|
// fake key. This will make it a bit harder for an attacker to "probe out" an oracle and see
|
|
// which keys are null or which are populated.
|
|
self.fake_key[0] = key_index;
|
|
self.fake_key
|
|
}
|
|
};
|
|
self.compute_key_rollback(&mut key);
|
|
let cipher = Aes256::new(GenericArray::from_slice(&key));
|
|
match op_type {
|
|
AesOpType::Decrypt => cipher.decrypt_block(block.try_into().unwrap()),
|
|
AesOpType::Encrypt => cipher.encrypt_block(block.try_into().unwrap()),
|
|
}
|
|
}
|
|
|
|
pub fn aes_par_op(&mut self, key_index: u8, op_type: AesOpType, blocks: &mut [[u8; 16]; PAR_BLOCKS]) {
|
|
use aes::cipher::KeyInit;
|
|
let mut key = match key_index {
|
|
KeyRomLocs::USER_KEY => {
|
|
let mut key_enc = self.read_key_256(KeyRomLocs::USER_KEY);
|
|
let pcache: &PasswordCache = unsafe { &*(self.pass_cache.as_ptr() as *const PasswordCache) };
|
|
if pcache.hashed_boot_pw_valid == 0 {
|
|
self.purge_password(PasswordType::Boot);
|
|
log::warn!("boot password isn't valid! Returning bogus results.");
|
|
}
|
|
for (key, &pw) in key_enc.iter_mut().zip(pcache.hashed_boot_pw.iter()) {
|
|
*key = *key ^ pw;
|
|
}
|
|
if self.boot_password_policy == PasswordRetentionPolicy::AlwaysPurge {
|
|
self.purge_password(PasswordType::Boot);
|
|
}
|
|
key_enc
|
|
}
|
|
_ => {
|
|
self.fake_key[0] = key_index;
|
|
self.fake_key
|
|
}
|
|
};
|
|
self.compute_key_rollback(&mut key);
|
|
let cipher = Aes256::new(GenericArray::from_slice(&key));
|
|
match op_type {
|
|
AesOpType::Decrypt => {
|
|
for block in blocks.iter_mut() {
|
|
cipher.decrypt_block(block.try_into().unwrap());
|
|
}
|
|
}
|
|
AesOpType::Encrypt => {
|
|
for block in blocks.iter_mut() {
|
|
cipher.encrypt_block(block.try_into().unwrap());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn kwp_op(&mut self, kwp: &mut KeyWrapper) {
|
|
let mut key = match kwp.key_index {
|
|
KeyRomLocs::USER_KEY => {
|
|
let mut key_enc = self.read_key_256(KeyRomLocs::USER_KEY);
|
|
let pcache: &PasswordCache = unsafe { &*(self.pass_cache.as_ptr() as *const PasswordCache) };
|
|
if pcache.hashed_boot_pw_valid == 0 {
|
|
self.purge_password(PasswordType::Boot);
|
|
log::warn!("boot password isn't valid! Returning bogus results.");
|
|
}
|
|
for (key, &pw) in key_enc.iter_mut().zip(pcache.hashed_boot_pw.iter()) {
|
|
*key = *key ^ pw;
|
|
}
|
|
if self.boot_password_policy == PasswordRetentionPolicy::AlwaysPurge {
|
|
self.purge_password(PasswordType::Boot);
|
|
}
|
|
key_enc
|
|
}
|
|
_ => {
|
|
self.fake_key[0] = kwp.key_index;
|
|
self.fake_key
|
|
}
|
|
};
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("root user key: {:x?}", key);
|
|
self.compute_key_rollback(&mut key);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("root user key (anti-rollback): {:x?}", key);
|
|
use aes_kw::Kek;
|
|
use aes_kw::KekAes256;
|
|
let keywrapper: KekAes256 = Kek::from(key);
|
|
match kwp.op {
|
|
KeyWrapOp::Wrap => {
|
|
match keywrapper.wrap_with_padding_vec(&kwp.data[..kwp.len as usize]) {
|
|
Ok(wrapped) => {
|
|
for (&src, dst) in wrapped.iter().zip(kwp.data.iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
kwp.len = wrapped.len() as u32;
|
|
kwp.result = None;
|
|
// this is an un-used field but...why not?
|
|
kwp.expected_len = wrapped.len() as u32;
|
|
}
|
|
Err(e) => {
|
|
kwp.result = Some(match e {
|
|
aes_kw::Error::IntegrityCheckFailed => KeywrapError::IntegrityCheckFailed,
|
|
aes_kw::Error::InvalidDataSize => KeywrapError::InvalidDataSize,
|
|
aes_kw::Error::InvalidKekSize { size } => {
|
|
log::info!("invalid size {}", size); // weird. can't name this _size
|
|
KeywrapError::InvalidKekSize
|
|
}
|
|
aes_kw::Error::InvalidOutputSize { expected } => {
|
|
log::info!("invalid output size {}", expected);
|
|
KeywrapError::InvalidOutputSize
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
KeyWrapOp::Unwrap => {
|
|
match keywrapper.unwrap_with_padding_vec(&kwp.data[..kwp.len as usize]) {
|
|
Ok(wrapped) => {
|
|
for (&src, dst) in wrapped.iter().zip(kwp.data.iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
kwp.len = wrapped.len() as u32;
|
|
kwp.result = None;
|
|
// this is an un-used field but...why not?
|
|
kwp.expected_len = wrapped.len() as u32;
|
|
}
|
|
Err(e) => {
|
|
kwp.result = Some(match e {
|
|
aes_kw::Error::IntegrityCheckFailed => {
|
|
// try the legacy version, if it unwraps, send back the key + an error that
|
|
// indicates the caller needs to update their version
|
|
let legacy_kw = Aes256KeyWrap::new(&key);
|
|
match legacy_kw
|
|
.decapsulate(&kwp.data[..kwp.len as usize], kwp.expected_len as usize)
|
|
{
|
|
Ok(unwrapped) => {
|
|
for (&src, dst) in unwrapped.iter().zip(kwp.data.iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
kwp.len = unwrapped.len() as u32;
|
|
// hand the caller back a new version of the wrapped key that meets
|
|
// NIST specs.
|
|
let corrected = keywrapper
|
|
.wrap_with_padding_vec(&unwrapped)
|
|
.expect("couldn't convert to correct version of AES keywrapping");
|
|
let mut upgrade = [0u8; 40];
|
|
assert!(
|
|
corrected.len() == 40,
|
|
"Correctly wrapped key has a different length than the legacy wrapped key"
|
|
);
|
|
upgrade.copy_from_slice(&corrected);
|
|
|
|
let mut unwrapped_key = [0u8; 32];
|
|
assert!(
|
|
kwp.len == 32,
|
|
"Unwrapped key from legacy version of algorithm has the wrong length"
|
|
);
|
|
unwrapped_key.copy_from_slice(&unwrapped);
|
|
log::warn!(
|
|
"Keywrap from incorrect version of algorithm; sending message to correct the problem"
|
|
);
|
|
KeywrapError::UpgradeToNew((unwrapped_key, upgrade))
|
|
}
|
|
Err(e) => e,
|
|
}
|
|
}
|
|
aes_kw::Error::InvalidDataSize => KeywrapError::InvalidDataSize,
|
|
aes_kw::Error::InvalidKekSize { size } => {
|
|
log::info!("invalid size {}", size); // weird. can't name this _size
|
|
KeywrapError::InvalidKekSize
|
|
}
|
|
aes_kw::Error::InvalidOutputSize { expected } => {
|
|
log::info!("invalid output size {}", expected);
|
|
KeywrapError::InvalidOutputSize
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// returns None if there is an obvious problem with the JTAG interface
|
|
/// otherwise returns the result. "secured" would be the most paranoid setting
|
|
/// which is all the bits burned. There are other combinations that are also
|
|
/// totally valid based on your usage scenario, however, but the have yet to
|
|
/// be implemented (see the JTAG crate for more info); however, we reflect
|
|
/// all of the calls through rootkeys so we aren't exposing JTAG attack surface
|
|
/// to the rest of the world.
|
|
pub fn is_efuse_secured(&self) -> Option<bool> {
|
|
if self.jtag.get_id().unwrap() != jtag::XCS750_IDCODE {
|
|
return None;
|
|
}
|
|
if (self.jtag.get_raw_control_bits().expect("couldn't get control bits") & 0x3f) != 0x3F {
|
|
return Some(false);
|
|
} else {
|
|
return Some(true);
|
|
}
|
|
}
|
|
|
|
pub fn fpga_key_source(&self) -> FpgaKeySource {
|
|
let mut words = self.gateware()[..4096].chunks(4);
|
|
loop {
|
|
if let Some(word) = words.next() {
|
|
let cwd = u32::from_be_bytes(word[0..4].try_into().unwrap());
|
|
if cwd == BITSTREAM_CTL0_CMD {
|
|
let ctl0 = u32::from_be_bytes(words.next().unwrap()[0..4].try_into().unwrap());
|
|
if ctl0 & 0x8000_0000 == 0 {
|
|
return FpgaKeySource::Bbram;
|
|
} else {
|
|
return FpgaKeySource::Efuse;
|
|
}
|
|
}
|
|
} else {
|
|
log::error!("didn't find FpgaKeySource in plaintext header");
|
|
panic!("didn't find FpgaKeySource in plaintext header");
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn is_zero_key(&self) -> Option<bool> {
|
|
if let Some(secured) = self.is_efuse_secured() {
|
|
if !secured {
|
|
if self.fpga_key_source() == FpgaKeySource::Efuse {
|
|
match self.jtag.efuse_fetch() {
|
|
Ok(record) => {
|
|
if record.key == [0u8; 32] {
|
|
Some(true) // yep, we booted from this and it's 0.
|
|
} else {
|
|
log::warn!(
|
|
"Efuse key was set, and we're booting from it, but the readback protection was NOT enabled. The key is not secured."
|
|
);
|
|
Some(false) // we booted from this, and we can definitively say it's not 0 (but also, we could read it out!!!)
|
|
}
|
|
}
|
|
_ => None, // error fetching key. can't say anything
|
|
}
|
|
} else {
|
|
None // booting from BBRAM. maybe it's zero, but we can't read BBRAM keys.
|
|
}
|
|
} else {
|
|
// this is borderline. Someone bothered to burn the readback protection fuses.
|
|
// we can't prove it's not zero, but for purposes of updates and provisioning, we should treat
|
|
// it as non-zero.
|
|
Some(false)
|
|
}
|
|
} else {
|
|
None // couldn't read anything, so we can't be sure
|
|
}
|
|
}
|
|
|
|
pub fn is_jtag_working(&self) -> bool {
|
|
if self.jtag.get_id().unwrap() == jtag::XCS750_IDCODE { true } else { false }
|
|
}
|
|
|
|
/// Checks that various registries are "fully populated", to ensure that the trusted set of servers
|
|
/// have completely loaded before trying to move on. Many of the security properties of the system
|
|
/// rely upon a trusted set of servers claiming unique and/or enumerated tokens or slots, and then
|
|
/// disallowing any new registrations after that point. This call prevents trusted operations from
|
|
/// occurring if some of these servers have failed to check in.
|
|
fn xous_init_interlock(&self) {
|
|
loop {
|
|
if self.xns.trusted_init_done().expect("couldn't query init done status on xous-names") {
|
|
break;
|
|
} else {
|
|
log::warn!(
|
|
"trusted init of xous-names not finished, rootkeys is holding off on sensitive operations"
|
|
);
|
|
self.ticktimer.sleep_ms(650).expect("couldn't sleep");
|
|
}
|
|
}
|
|
loop {
|
|
if self.gam.trusted_init_done().expect("couldn't query init done status on GAM") {
|
|
break;
|
|
} else {
|
|
log::warn!(
|
|
"trusted init of GAM not finished, rootkeys is holding off on sensitive operations"
|
|
);
|
|
self.ticktimer.sleep_ms(650).expect("couldn't sleep");
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn purge_user_password(&mut self, pw_type: AesRootkeyType) {
|
|
match pw_type {
|
|
AesRootkeyType::User0 => self.purge_password(PasswordType::Boot),
|
|
_ => log::warn!("Requested to purge a password for a key that we don't have. Ignoring."),
|
|
}
|
|
}
|
|
|
|
pub fn purge_password(&mut self, pw_type: PasswordType) {
|
|
unsafe {
|
|
let pcache_ptr: *mut PasswordCache = self.pass_cache.as_mut_ptr() as *mut PasswordCache;
|
|
match pw_type {
|
|
PasswordType::Boot => {
|
|
for p in (*pcache_ptr).hashed_boot_pw.iter_mut() {
|
|
*p = 0;
|
|
}
|
|
(*pcache_ptr).hashed_boot_pw_valid = 0;
|
|
}
|
|
PasswordType::Update => {
|
|
for p in (*pcache_ptr).hashed_update_pw.iter_mut() {
|
|
*p = 0;
|
|
}
|
|
(*pcache_ptr).hashed_update_pw_valid = 0;
|
|
|
|
for p in (*pcache_ptr).fpga_key.iter_mut() {
|
|
*p = 0;
|
|
}
|
|
(*pcache_ptr).fpga_key_valid = 0;
|
|
}
|
|
}
|
|
}
|
|
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
|
|
pub fn purge_sensitive_data(&mut self) {
|
|
for d in unsafe { self.sensitive_data.borrow_mut().as_slice_mut::<u32>().iter_mut() } {
|
|
*d = 0;
|
|
}
|
|
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
|
|
fn populate_sensitive_data(&mut self) {
|
|
for (addr, d) in
|
|
unsafe { self.sensitive_data.borrow_mut().as_slice_mut::<u32>().iter_mut().enumerate() }
|
|
{
|
|
if addr > 255 {
|
|
break;
|
|
}
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, addr as u32);
|
|
let keyword = self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
*d = keyword;
|
|
}
|
|
}
|
|
|
|
fn replace_fpga_key(&mut self) {
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
if self.is_initialized() {
|
|
assert!(
|
|
pcache.hashed_update_pw_valid != 0,
|
|
"update password was not set before calling replace_fpga_key"
|
|
);
|
|
}
|
|
// build a new key into the pcache
|
|
for dst_lw in pcache.fpga_key.chunks_mut(8).into_iter() {
|
|
// 64 bit chunks
|
|
for (dst, &src) in dst_lw.iter_mut().zip(self.trng.get_u64().unwrap().to_be_bytes().iter()) {
|
|
*dst = src;
|
|
}
|
|
}
|
|
|
|
// now encrypt the key, and store it into the sensitive_data for access later on by the patching
|
|
// routine
|
|
if self.is_initialized() {
|
|
for (dst, (key, pw)) in unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()
|
|
[KeyRomLocs::FPGA_KEY as usize..KeyRomLocs::FPGA_KEY as usize + 8]
|
|
.iter_mut()
|
|
}
|
|
.zip(pcache.fpga_key.chunks(4).into_iter().zip(pcache.hashed_update_pw.chunks(4).into_iter()))
|
|
{
|
|
*dst = u32::from_be_bytes(key[0..4].try_into().unwrap())
|
|
^ u32::from_be_bytes(pw[0..4].try_into().unwrap());
|
|
}
|
|
} else {
|
|
for (dst, key) in unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()
|
|
[KeyRomLocs::FPGA_KEY as usize..KeyRomLocs::FPGA_KEY as usize + 8]
|
|
.iter_mut()
|
|
}
|
|
.zip(pcache.fpga_key.chunks(4).into_iter())
|
|
{
|
|
*dst = u32::from_be_bytes(key[0..4].try_into().unwrap());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn suspend(&mut self) {
|
|
match self.boot_password_policy {
|
|
PasswordRetentionPolicy::AlwaysKeep => (),
|
|
_ => {
|
|
self.purge_password(PasswordType::Boot);
|
|
}
|
|
}
|
|
match self.update_password_policy {
|
|
PasswordRetentionPolicy::AlwaysKeep => (),
|
|
_ => {
|
|
self.purge_password(PasswordType::Update);
|
|
}
|
|
}
|
|
self.purge_sensitive_data();
|
|
}
|
|
|
|
pub fn resume(&mut self) {}
|
|
|
|
pub fn update_policy(&mut self, policy: Option<PasswordRetentionPolicy>) {
|
|
let pw_type = if let Some(cur_type) = self.cur_password_type {
|
|
cur_type
|
|
} else {
|
|
log::error!("got an unexpected policy update from the UX");
|
|
return;
|
|
};
|
|
if let Some(p) = policy {
|
|
match pw_type {
|
|
PasswordType::Boot => self.boot_password_policy = p,
|
|
PasswordType::Update => self.update_password_policy = p,
|
|
};
|
|
} else {
|
|
match pw_type {
|
|
PasswordType::Boot => PasswordRetentionPolicy::AlwaysPurge,
|
|
PasswordType::Update => PasswordRetentionPolicy::AlwaysPurge,
|
|
};
|
|
}
|
|
// once the policy has been set, revert the current type to None
|
|
self.cur_password_type = None;
|
|
}
|
|
|
|
/// Plaintext password is passed as a &str. Any copies internally are destroyed. Caller is responsible for
|
|
/// destroying the &str original. Performs a bcrypt hash of the password, with the currently set salt;
|
|
/// does not store the plaintext after exit.
|
|
pub fn hash_and_save_password(&mut self, pw: &str, verify: bool) -> bool {
|
|
let pw_type = if let Some(cur_type) = self.cur_password_type {
|
|
cur_type
|
|
} else {
|
|
log::error!("got an unexpected password from the UX");
|
|
return false;
|
|
};
|
|
let mut hashed_password: [u8; 24] = [0; 24];
|
|
let mut salt = self.get_salt();
|
|
// we change the salt ever-so-slightly for every password. This doesn't make any one password more
|
|
// secure; but it disallows guessing all the passwords with a single off-the-shelf hashcat
|
|
// run.
|
|
salt[0] ^= pw_type as u8;
|
|
|
|
let timer = ticktimer_server::Ticktimer::new().expect("couldn't connect to ticktimer");
|
|
// the bcrypt function takes the plaintext password and makes one copy to prime the blowfish bcrypt
|
|
// cipher. It is responsible for erasing this state.
|
|
let start_time = timer.elapsed_ms();
|
|
bcrypt(BCRYPT_COST, &salt, pw, &mut hashed_password); // note: this internally makes a copy of the password, and destroys it
|
|
let elapsed = timer.elapsed_ms() - start_time;
|
|
log::info!("bcrypt cost: {} time: {}ms", BCRYPT_COST, elapsed); // benchmark to figure out how to set cost parameter
|
|
|
|
// expand the 24-byte (192-bit) bcrypt result into 256 bits, so we can use it directly as XOR key
|
|
// material against 256-bit AES and curve25519 keys
|
|
let mut hasher = Sha512_256Sw::new();
|
|
hasher.update(hashed_password);
|
|
let digest = hasher.finalize();
|
|
|
|
let pcache_ptr: *mut PasswordCache = self.pass_cache.as_mut_ptr() as *mut PasswordCache;
|
|
if !verify {
|
|
unsafe {
|
|
match pw_type {
|
|
PasswordType::Boot => {
|
|
for (&src, dst) in digest.iter().zip((*pcache_ptr).hashed_boot_pw.iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
(*pcache_ptr).hashed_boot_pw_valid = 1;
|
|
}
|
|
PasswordType::Update => {
|
|
for (&src, dst) in digest.iter().zip((*pcache_ptr).hashed_update_pw.iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
(*pcache_ptr).hashed_update_pw_valid = 1;
|
|
}
|
|
}
|
|
}
|
|
true
|
|
} else {
|
|
unsafe {
|
|
match pw_type {
|
|
PasswordType::Boot => {
|
|
if (*pcache_ptr).hashed_boot_pw_valid == 1 {
|
|
for (&src, &dst) in digest.iter().zip((*pcache_ptr).hashed_boot_pw.iter()) {
|
|
if dst != src {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
PasswordType::Update => {
|
|
if (*pcache_ptr).hashed_update_pw_valid == 1 {
|
|
for (&src, &dst) in digest.iter().zip((*pcache_ptr).hashed_update_pw.iter()) {
|
|
if dst != src {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reads a 256-bit key at a given index offset
|
|
fn read_key_256(&mut self, index: u8) -> [u8; 32] {
|
|
let mut key: [u8; 32] = [0; 32];
|
|
for (addr, word) in key.chunks_mut(4).into_iter().enumerate() {
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, index as u32 + addr as u32);
|
|
let keyword = self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
for (&byte, dst) in keyword.to_be_bytes().iter().zip(word.iter_mut()) {
|
|
*dst = byte;
|
|
}
|
|
}
|
|
key
|
|
}
|
|
|
|
/// Reads a 128-bit key at a given index offset
|
|
fn read_key_128(&mut self, index: u8) -> [u8; 16] {
|
|
let mut key: [u8; 16] = [0; 16];
|
|
for (addr, word) in key.chunks_mut(4).into_iter().enumerate() {
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, index as u32 + addr as u32);
|
|
let keyword = self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
for (&byte, dst) in keyword.to_be_bytes().iter().zip(word.iter_mut()) {
|
|
*dst = byte;
|
|
}
|
|
}
|
|
key
|
|
}
|
|
|
|
/// Reads a 256-bit key at a given index offset
|
|
fn read_staged_key_256(&mut self, index: u8) -> [u8; 32] {
|
|
let mut key: [u8; 32] = [0; 32];
|
|
for (addr, word) in key.chunks_mut(4).into_iter().enumerate() {
|
|
let keyword = unsafe { self.sensitive_data.borrow().as_slice::<u32>()[index as usize + addr] };
|
|
for (&byte, dst) in keyword.to_be_bytes().iter().zip(word.iter_mut()) {
|
|
*dst = byte;
|
|
}
|
|
}
|
|
key
|
|
}
|
|
|
|
/// Returns the `salt` needed for the `bcrypt` routine.
|
|
/// This routine handles the special-case of being uninitialized: in that case, we need to get
|
|
/// salt from a staging area, and not our KEYROM. However, `setup_key_init` must be called
|
|
/// first to ensure that the staging area has a valid salt.
|
|
fn get_salt(&mut self) -> [u8; 16] {
|
|
if !self.is_initialized() || self.restore_running {
|
|
// we're not initialized, use the salt that should already be in the staging area
|
|
let mut key: [u8; 16] = [0; 16];
|
|
for (word, &keyword) in key.chunks_mut(4).into_iter()
|
|
.zip(unsafe { self.sensitive_data.borrow_mut().as_slice::<u32>() } // get the sensitive_data as a slice &mut[u32]
|
|
[KeyRomLocs::PEPPER as usize..KeyRomLocs::PEPPER as usize + 128/(size_of::<u32>()*8)].iter()) {
|
|
for (&byte, dst) in keyword.to_be_bytes().iter().zip(word.iter_mut()) {
|
|
*dst = byte;
|
|
}
|
|
}
|
|
key
|
|
} else {
|
|
self.read_key_128(KeyRomLocs::PEPPER)
|
|
}
|
|
}
|
|
|
|
/// Called by the UX layer to track which password we're currently requesting
|
|
pub fn set_ux_password_type(&mut self, cur_type: Option<PasswordType>) {
|
|
self.cur_password_type = cur_type;
|
|
}
|
|
|
|
/// Called by the UX layer to check which password request is in progress
|
|
#[allow(dead_code)]
|
|
pub fn get_ux_password_type(&self) -> Option<PasswordType> { self.cur_password_type }
|
|
|
|
pub fn is_initialized(&mut self) -> bool {
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, KeyRomLocs::CONFIG as u32);
|
|
let config = self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
if config & keyrom_config::INITIALIZED.ms(1) != 0 { true } else { false }
|
|
}
|
|
|
|
pub fn is_pcache_update_password_valid(&self) -> bool {
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
if pcache.hashed_update_pw_valid == 0 { false } else { true }
|
|
}
|
|
|
|
pub fn is_pcache_boot_password_valid(&self) -> bool {
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
if pcache.hashed_boot_pw_valid == 0 { false } else { true }
|
|
}
|
|
|
|
/// Called by the UX layer to set up a key init run. It disables suspend/resume for the duration
|
|
/// of the run, and also sets up some missing fields of KEYROM necessary to encrypt passwords.
|
|
pub fn setup_key_init(&mut self) {
|
|
self.xous_init_interlock();
|
|
// block suspend/resume ops during security-sensitive operations
|
|
self.susres.set_suspendable(false).expect("couldn't block suspend/resume");
|
|
// in this block, keyrom data is copied into RAM.
|
|
// make a copy of the KEYROM to hold the new mods, in the sensitive data area
|
|
for addr in 0..256 {
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, addr);
|
|
unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[addr as usize] =
|
|
self.keyrom.rf(utra::keyrom::DATA_DATA)
|
|
};
|
|
}
|
|
|
|
// provision the pepper
|
|
for keyword in unsafe { self.sensitive_data.borrow_mut().as_slice_mut::<u32>() }
|
|
[KeyRomLocs::PEPPER as usize..KeyRomLocs::PEPPER as usize + 128 / (size_of::<u32>() * 8)]
|
|
.iter_mut()
|
|
{
|
|
*keyword = self.trng.get_u32().expect("couldn't get random number");
|
|
}
|
|
}
|
|
|
|
pub fn setup_restore_init(&mut self, key: backups::BackupKey, rom: backups::KeyRomExport) {
|
|
self.xous_init_interlock();
|
|
// block suspend/resume ops during security-sensitive operations
|
|
self.susres.set_suspendable(false).expect("couldn't block suspend/resume");
|
|
|
|
// populate the staging area, in particular we are interested in the "pepper" so passwords work
|
|
// correctly.
|
|
unsafe { self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[..256].copy_from_slice(&rom.0) };
|
|
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
// copy the plaintext FPGA key to the pcache
|
|
pcache.fpga_key.copy_from_slice(&key.0);
|
|
pcache.fpga_key_valid = 1;
|
|
|
|
// stage the plaintext FPGA key into the keyrom area for encryption by the key_init routine.
|
|
unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u8>()
|
|
[KeyRomLocs::FPGA_KEY as usize..KeyRomLocs::FPGA_KEY as usize + 32]
|
|
.copy_from_slice(&key.0)
|
|
};
|
|
|
|
self.restore_running = true;
|
|
}
|
|
|
|
/// used to recycle a PDDB after a key init event
|
|
pub fn pddb_recycle(&mut self) {
|
|
// erase the page table, which should effectively trigger a reformat on the next boot
|
|
self.spinor.bulk_erase(xous::PDDB_LOC, 512 * 1024).expect("couldn't erase page table");
|
|
}
|
|
|
|
/// Core of the key initialization routine. Requires a `progress_modal` dialog box that has been set
|
|
/// up with the appropriate notification messages by the UX layer, and a `Slider` type action which
|
|
/// is used to report the progress of the initialization routine. We assume the `Slider` box is set
|
|
/// up to report progress on a range of 0-100%.
|
|
///
|
|
/// IMPORTANT ASSUMPTION: It is assumed that all the progress messages in the translations do not
|
|
/// end up changing the size of the dialog box. This is because such changes in size would trigger
|
|
/// a redraw message that cannot be handled by the thread (because it's busy doing this), and
|
|
/// if enough of them get issued, eventually the thread deadlocks. If you are doing translations,
|
|
/// review your messages and add \n characters on shorter messages so that the overall height
|
|
/// of the dialog box remains constant throughout the operation!
|
|
///
|
|
/// This routine dispatches the following activities:
|
|
/// - generate signing private key (encrypted with update password)
|
|
/// - generate rootkey (encrypted with boot password)
|
|
/// - generate signing public key
|
|
/// - set the init bit
|
|
/// - sign the loader
|
|
/// - sign the kernel
|
|
/// - compute the patch set for the FPGA bitstream
|
|
/// - do the patch (whatever that means - gotta deal with the AES key, HMAC etc.)
|
|
/// - verify the FPGA image hmac
|
|
/// - sign the FPGA image
|
|
/// - get ready for a reboot
|
|
///
|
|
/// Note to future self: this terrible syntax `self.sensitive_data.borrow_mut().as_slice_mut::<u32>()`
|
|
/// is embedded all over the place as a substitute for `sensitive_slice` because we need interior
|
|
/// mutability of the data within the MemoryRange (which is not mutable) to change the contents of the
|
|
/// MemoryRange (a mutable operation). We can't bind `sensitive_slice` to
|
|
/// `self.sensitive_data.borrow_mut().as_slice_mut::<u32>()` because this creates a temporary that has
|
|
/// the wrong lifetime, and thus, we have to embed that terrible piece of unmaintainable syntax all
|
|
/// over the place in the code below to solve this problem.
|
|
///
|
|
/// If `restore` is `Some`, don't generate keys, but restore from backup. The key provided to this routine
|
|
/// is *always* the correct key for the FPGA to boot from. The entry in the KeyRom will be adjusted
|
|
/// accordingly.
|
|
pub fn do_key_init(
|
|
&mut self,
|
|
rootkeys_modal: &mut Modal,
|
|
main_cid: xous::CID,
|
|
) -> Result<(), RootkeyResult> {
|
|
self.xous_init_interlock();
|
|
self.spinor.set_staging_write_protect(true).expect("couldn't protect the staging area");
|
|
|
|
let mut progress_action =
|
|
Slider::new(main_cid, Opcode::UxGutter.to_u32().unwrap(), 0, 100, 10, Some("%"), 0, true, true);
|
|
progress_action.set_is_password(true);
|
|
// now show the init wait note...
|
|
rootkeys_modal.modify(
|
|
Some(ActionType::Slider(progress_action)),
|
|
Some(t!("rootkeys.setup_wait", locales::LANG)),
|
|
false,
|
|
None,
|
|
true,
|
|
None,
|
|
);
|
|
rootkeys_modal.activate();
|
|
|
|
xous::yield_slice(); // give some time to the GAM to render
|
|
|
|
// Capture the progress bar elements in a convenience structure.
|
|
// NOTE: This is documented in the structure itself, but, it's worth repeating here:
|
|
// this routine only works because we don't resize the rootkeys_modal box as we
|
|
// advance progress. This allows us to do a local-only redraw without triggering
|
|
// a global GAM redraw operation. If we were to resize the dialog box, this would
|
|
// trigger the defacing algorithm to send back "redraw" messages into the main
|
|
// loop's queue. However, because the main loop is currently stuck running the code
|
|
// in this routine, the "redraw" messages never get serviced (even if they are
|
|
// effectively NOPs), and eventually, these messages would fill up the queue and can cause
|
|
// the system to deadlock once the queue is full.
|
|
let mut pb = ProgressBar::new(rootkeys_modal, &mut progress_action);
|
|
|
|
// kick the progress bar to indicate we've entered the routine
|
|
pb.set_percentage(1);
|
|
|
|
// get access to the pcache and generate a keypair
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
// initialize the global rollback constant to 0
|
|
unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[KeyRomLocs::GLOBAL_ROLLBACK as usize] = 0
|
|
};
|
|
let mut root_sk = [0u8; ed25519_dalek::SECRET_KEY_LENGTH];
|
|
self.trng.fill_bytes(&mut root_sk);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("root privkey: {:x?}", root_sk);
|
|
let keypair = if true {
|
|
// true for production, false for debug (uses dev keys, so we can compare results)
|
|
// we use software hashing because for short keys its faster
|
|
let mut derived_sk = [0u8; ed25519_dalek::SECRET_KEY_LENGTH];
|
|
derived_sk.copy_from_slice(&root_sk);
|
|
// hash a derived secret key based on the maximum rollback limit we anticipate less the current
|
|
// global rollback limit (which is 0) this allows us to work with a "final" key that
|
|
// generates a private signing key, which can't be predicted to future versions
|
|
// given the current key (due to the irreversibility of Sha512/256), but once derived to a more
|
|
// recent version can be willfully computed to an older version to recover e.g. old
|
|
// data encrypted with a prior key
|
|
//
|
|
// We don't use the `compute_key_rollback` function because the value of the GLOBAL_ROLLBACK in
|
|
// the KeyROM has not yet been set (it should be zero, but there is no reason for it
|
|
// to be at this point).
|
|
for _i in 0..MAX_ROLLBACK_LIMIT {
|
|
let mut hasher = Sha512_256Sw::new();
|
|
hasher.update(&derived_sk);
|
|
let digest = hasher.finalize();
|
|
derived_sk.copy_from_slice(&digest);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
if _i >= (MAX_ROLLBACK_LIMIT - 3) {
|
|
log::info!("iter {} key {:x?}", _i, derived_sk);
|
|
}
|
|
}
|
|
let sk: SigningKey = SigningKey::from_bytes(&derived_sk);
|
|
// SigningKey zeroizes on drop
|
|
sk
|
|
} else {
|
|
let sk = SigningKey::from_keypair_bytes(&[
|
|
168, 167, 118, 92, 141, 162, 215, 147, 134, 43, 8, 176, 0, 222, 188, 167, 178, 14, 137, 237,
|
|
82, 199, 133, 162, 179, 235, 161, 219, 156, 182, 42, 39, 28, 155, 234, 227, 42, 234, 200,
|
|
117, 7, 193, 128, 148, 56, 126, 255, 28, 116, 97, 66, 130, 175, 253, 129, 82, 216, 113, 53,
|
|
46, 223, 63, 88, 187,
|
|
])
|
|
.unwrap();
|
|
// SigningKey zeroizes on drop
|
|
sk
|
|
};
|
|
log::debug!("keypair pubkey: {:?}", keypair.verifying_key());
|
|
log::debug!("keypair pubkey: {:x?}", keypair.verifying_key());
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("keypair sign+verify (after anti-rollback): {:?}", keypair.to_keypair_bytes());
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("keypair sign+verify (after anti-rollback): {:x?}", keypair.to_keypair_bytes());
|
|
|
|
// encrypt the FPGA key using the update password. in an un-init system, it is provided to us in
|
|
// plaintext format e.g. in the case that we're doing a BBRAM boot (eFuse flow would give us a
|
|
// 0's key and we'd later on set it)
|
|
#[cfg(feature = "hazardous-debug")]
|
|
self.debug_print_key(KeyRomLocs::FPGA_KEY as usize, 256, "FPGA key before encryption: ");
|
|
// before we encrypt it, stash a copy in our password cache, as we'll need it later on to encrypt the
|
|
// bitstream
|
|
for (&src, dst) in self.read_key_256(KeyRomLocs::FPGA_KEY).iter().zip(pcache.fpga_key.iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
pcache.fpga_key_valid = 1;
|
|
|
|
// now encrypt it in the staging area
|
|
for (word, hashed_pass) in unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()
|
|
[KeyRomLocs::FPGA_KEY as usize..KeyRomLocs::FPGA_KEY as usize + 256 / (size_of::<u32>() * 8)]
|
|
.iter_mut()
|
|
}
|
|
.zip(pcache.hashed_update_pw.chunks(4).into_iter())
|
|
{
|
|
*word = *word ^ u32::from_be_bytes(hashed_pass.try_into().unwrap());
|
|
}
|
|
|
|
// allocate a decryption oracle for the FPGA bitstream. This will fail early if the FPGA key is wrong.
|
|
assert!(pcache.fpga_key_valid == 1);
|
|
log::debug!("making destination oracle");
|
|
let mut dst_oracle = match BitstreamOracle::new(
|
|
&pcache.fpga_key,
|
|
&pcache.fpga_key,
|
|
self.gateware(),
|
|
self.gateware_base(),
|
|
) {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
log::error!("couldn't create oracle (most likely FPGA key mismatch): {:?}", e);
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
pb.set_percentage(5);
|
|
|
|
// pub key is easy, no need to encrypt
|
|
let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = keypair.verifying_key().to_bytes();
|
|
for (src, dst) in public_key.chunks(4).into_iter().zip(unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[KeyRomLocs::SELFSIGN_PUBKEY as usize
|
|
..KeyRomLocs::SELFSIGN_PUBKEY as usize + 256 / (size_of::<u32>() * 8)]
|
|
.iter_mut()
|
|
}) {
|
|
*dst = u32::from_be_bytes(src.try_into().unwrap())
|
|
}
|
|
log::debug!("public key as computed: {:x?}", public_key);
|
|
|
|
// extract the update password key from the cache, and apply it to the private key
|
|
#[cfg(feature = "hazardous-debug")]
|
|
{
|
|
log::debug!("cached update password: {:x?}", pcache.hashed_update_pw);
|
|
}
|
|
// private key must XOR with password before storing
|
|
let mut private_key_enc: [u8; ed25519_dalek::SECRET_KEY_LENGTH] =
|
|
[0; ed25519_dalek::SECRET_KEY_LENGTH];
|
|
// I don't think this loop should make any extra copies of the secret key, but might be good to check
|
|
// in godbolt!
|
|
for (dst, (plain, key)) in
|
|
private_key_enc.iter_mut().zip(root_sk.iter() // we encrypt the root sk, not the derived sk
|
|
.zip(pcache.hashed_update_pw.iter()))
|
|
{
|
|
*dst = plain ^ key;
|
|
}
|
|
|
|
// store the private key to the keyrom staging area
|
|
for (src, dst) in private_key_enc.chunks(4).into_iter().zip(unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[KeyRomLocs::SELFSIGN_PRIVKEY as usize
|
|
..KeyRomLocs::SELFSIGN_PRIVKEY as usize + 256 / (size_of::<u32>() * 8)]
|
|
.iter_mut()
|
|
}) {
|
|
*dst = u32::from_be_bytes(src.try_into().unwrap())
|
|
}
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("private_key_enc: {:?}", private_key_enc);
|
|
|
|
pb.set_percentage(10);
|
|
|
|
// generate the "boot key". This is just a random number that eventually gets XOR'd with a PIN
|
|
// by the PDDB to derive the final PDDB .System unlock key.
|
|
let mut boot_key_enc: [u8; 32] = [0; 32];
|
|
for dst in boot_key_enc.chunks_mut(4).into_iter() {
|
|
let key_word = self.trng.get_u32().unwrap().to_be_bytes();
|
|
// just unroll this loop, it's fast and easy enough
|
|
(*dst)[0] = key_word[0];
|
|
(*dst)[1] = key_word[1];
|
|
(*dst)[2] = key_word[2];
|
|
(*dst)[3] = key_word[3];
|
|
}
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("boot_key_enc: {:?}", boot_key_enc);
|
|
|
|
// store the boot key to the keyrom staging area
|
|
for (src, dst) in boot_key_enc.chunks(4).into_iter().zip(unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()
|
|
[KeyRomLocs::USER_KEY as usize..KeyRomLocs::USER_KEY as usize + 256 / (size_of::<u32>() * 8)]
|
|
.iter_mut()
|
|
}) {
|
|
*dst = u32::from_be_bytes(src.try_into().unwrap())
|
|
}
|
|
|
|
// sign the kernel
|
|
pb.update_text(t!("rootkeys.init.signing_kernel", locales::LANG));
|
|
pb.set_percentage(15);
|
|
let (kernel_sig, kernel_len) = self.sign_kernel(&keypair);
|
|
|
|
// sign the loader
|
|
pb.update_text(t!("rootkeys.init.signing_loader", locales::LANG));
|
|
pb.rebase_subtask_percentage(20, 30);
|
|
let (loader_sig, loader_len) = self.sign_loader(&keypair, Some(&mut pb));
|
|
log::debug!("loader signature: {:x?}", loader_sig.to_bytes());
|
|
log::debug!("loader len: {} bytes", loader_len);
|
|
|
|
// set the "init" bit in the staging area
|
|
unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[KeyRomLocs::CONFIG as usize] |=
|
|
keyrom_config::INITIALIZED.ms(1)
|
|
};
|
|
|
|
#[cfg(feature = "hazardous-debug")]
|
|
{
|
|
log::debug!("Self private key: {:x?}", keypair.secret.to_bytes());
|
|
log::debug!("Self public key: {:x?}", keypair.public.to_bytes());
|
|
self.debug_staging();
|
|
}
|
|
|
|
// Because we're initializing keys for the *first* time, make a backup copy of the bitstream to
|
|
// the staging area. Note that if we're doing an update, the update target would already be
|
|
// in the staging area, so this step should be skipped.
|
|
pb.update_text(t!("rootkeys.init.backup_gateware", locales::LANG));
|
|
pb.rebase_subtask_percentage(30, 50);
|
|
self.make_gateware_backup(Some(&mut pb), false)?;
|
|
|
|
log::debug!("making source oracle");
|
|
let mut src_oracle = match BitstreamOracle::new(
|
|
&pcache.fpga_key,
|
|
&pcache.fpga_key,
|
|
self.staging(),
|
|
self.staging_base(),
|
|
) {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
log::error!("couldn't create oracle (most likely FPGA key mismatch): {:?}", e);
|
|
dst_oracle.clear();
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
// compute the keyrom patch set for the bitstream
|
|
// at this point the KEYROM as replicated in sensitive_slice should have all its assets in place
|
|
pb.update_text(t!("rootkeys.init.patching_keys", locales::LANG));
|
|
pb.rebase_subtask_percentage(50, 70);
|
|
|
|
self.gateware_copy_and_patch(&src_oracle, &dst_oracle, Some(&mut pb))?;
|
|
|
|
// make a copy of the plaintext metadata and csr records
|
|
self.spinor
|
|
.patch(
|
|
self.gateware(),
|
|
self.gateware_base(),
|
|
&self.staging()[METADATA_OFFSET..SELFSIG_OFFSET],
|
|
METADATA_OFFSET as u32,
|
|
)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
|
|
// verify that the patch worked
|
|
pb.update_text(t!("rootkeys.init.verifying_gateware", locales::LANG));
|
|
pb.rebase_subtask_percentage(70, 90);
|
|
self.verify_gateware(&dst_oracle, Some(&mut pb))?;
|
|
|
|
// sign the image, commit the signature
|
|
pb.update_text(t!("rootkeys.init.commit_signatures", locales::LANG));
|
|
self.commit_signature(loader_sig, loader_len, SignatureType::Loader)?;
|
|
log::debug!("loader {} bytes, sig: {:x?}", loader_len, loader_sig.to_bytes());
|
|
pb.set_percentage(92);
|
|
self.commit_signature(kernel_sig, kernel_len, SignatureType::Kernel)?;
|
|
pb.set_percentage(95);
|
|
|
|
// clean up the oracles as soon as we're done to avoid some borrow checker issues
|
|
src_oracle.clear();
|
|
dst_oracle.clear();
|
|
|
|
// as a sanity check, check the kernel self signature
|
|
let pubkey = VerifyingKey::from_bytes(&public_key).expect("public key was not valid");
|
|
let ret = if !self.verify_selfsign_kernel(Some(&pubkey)) {
|
|
log::error!("kernel signature failed to verify, probably should not try to reboot!");
|
|
Err(RootkeyResult::IntegrityError)
|
|
} else {
|
|
// sign the gateware
|
|
pb.set_percentage(98);
|
|
let (gateware_sig, gateware_len) = self.sign_gateware(&keypair);
|
|
log::debug!("gateware signature ({}): {:x?}", gateware_len, gateware_sig.to_bytes());
|
|
self.commit_signature(gateware_sig, gateware_len, SignatureType::Gateware)?;
|
|
Ok(())
|
|
};
|
|
|
|
// clear the write protects
|
|
self.spinor.set_staging_write_protect(false).expect("couldn't un-protect the staging area");
|
|
|
|
// finalize the progress bar on exit -- always leave at 100%
|
|
pb.set_percentage(100);
|
|
|
|
self.ticktimer.sleep_ms(500).expect("couldn't show final message");
|
|
|
|
// always purge, we're going to reboot; and if we don't, then there's shenanigans
|
|
for w in private_key_enc.iter_mut() {
|
|
// it's encrypted. but i still want it turned to zeroes.
|
|
*w = 0;
|
|
}
|
|
self.purge_password(PasswordType::Boot);
|
|
self.purge_password(PasswordType::Update);
|
|
self.purge_sensitive_data();
|
|
// ed25519 keypair zeroizes on drop
|
|
|
|
ret
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[cfg(feature = "hazardous-debug")]
|
|
pub fn printkeys(&mut self) {
|
|
// dump the keystore -- used to confirm that patching worked right. does not get compiled in when
|
|
// hazardous-debug is not enable.
|
|
for addr in 0..256 {
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, addr);
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[addr as usize] =
|
|
self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
log::info!(
|
|
"{:02x}: 0x{:08x}",
|
|
addr,
|
|
self.sensitive_data.borrow_mut().as_slice::<u32>()[addr as usize]
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn do_gateware_update(
|
|
&mut self,
|
|
rootkeys_modal: &mut Modal,
|
|
modals: &modals::Modals,
|
|
main_cid: xous::CID,
|
|
update_type: UpdateType,
|
|
) -> Result<(), RootkeyResult> {
|
|
// log::set_max_level(log::LevelFilter::Debug);
|
|
// make sure the system is sane
|
|
self.xous_init_interlock();
|
|
self.spinor.set_staging_write_protect(true).expect("couldn't protect the staging area");
|
|
|
|
// setup Ux
|
|
let mut progress_action =
|
|
Slider::new(main_cid, Opcode::UxGutter.to_u32().unwrap(), 0, 100, 10, Some("%"), 0, true, true);
|
|
progress_action.set_is_password(true);
|
|
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
let mut keypair_bytes: [u8; ed25519_dalek::KEYPAIR_LENGTH] = [0; ed25519_dalek::KEYPAIR_LENGTH];
|
|
let mut old_key = [0u8; 32];
|
|
let mut pb = if update_type == UpdateType::Restore {
|
|
// now show the init wait note...
|
|
rootkeys_modal.modify(
|
|
Some(ActionType::Slider(progress_action)),
|
|
Some(t!("rootkeys.gwup_starting", locales::LANG)),
|
|
false,
|
|
None,
|
|
true,
|
|
None,
|
|
);
|
|
rootkeys_modal.activate();
|
|
xous::yield_slice(); // give some time to the GAM to render
|
|
let mut pb = ProgressBar::new(rootkeys_modal, &mut progress_action);
|
|
pb.set_percentage(1);
|
|
|
|
// ASSUME:
|
|
// - the sensitive_data has been set up correctly
|
|
// - sensitive_data's FPGA_KEY is a *plaintext* version of the FPGA key
|
|
// - the pcache.fpga_key also contains a *plaintext* version of the FPGA key
|
|
// Note: these are handled by the `setup_restore_init()` routine.
|
|
|
|
//------ test that the restore provided password is valid for the source keyrom block
|
|
// derive signing key
|
|
if pcache.hashed_update_pw_valid == 0 {
|
|
self.purge_password(PasswordType::Update);
|
|
log::error!("no password was set going into the update routine");
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("key: {:x?}", pcache.hashed_update_pw);
|
|
log::debug!("valid: {}", pcache.hashed_update_pw_valid);
|
|
|
|
return Err(RootkeyResult::KeyError);
|
|
}
|
|
let enc_signing_key = self.read_staged_key_256(KeyRomLocs::SELFSIGN_PRIVKEY);
|
|
for (key, (&enc_key, &pw)) in keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
|
|
.iter_mut()
|
|
.zip(enc_signing_key.iter().zip(pcache.hashed_update_pw.iter()))
|
|
{
|
|
*key = enc_key ^ pw;
|
|
}
|
|
self.compute_key_rollback(&mut keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!(
|
|
"keypair privkey (after anti-rollback): {:x?}",
|
|
&keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
|
|
);
|
|
// read in the public key from the staged data
|
|
for (key, &src) in keypair_bytes[ed25519_dalek::SECRET_KEY_LENGTH..]
|
|
.iter_mut()
|
|
.zip(self.read_staged_key_256(KeyRomLocs::SELFSIGN_PUBKEY).iter())
|
|
{
|
|
*key = src;
|
|
}
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("keypair_bytes {:x?}", keypair_bytes);
|
|
// Keypair zeroizes the secret key on drop.
|
|
let keypair =
|
|
SigningKey::from_keypair_bytes(&keypair_bytes).map_err(|_| RootkeyResult::KeyError)?;
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!(
|
|
"keypair privkey (after anti-rollback + conversion): {:x?}",
|
|
keypair.secret.to_bytes()
|
|
);
|
|
|
|
// check if the keypair is valid by signing and verifying a short message
|
|
let test_data = "whiskey made me do it";
|
|
let test_sig = keypair.sign(test_data.as_bytes());
|
|
match keypair.verify(&test_data.as_bytes(), &test_sig) {
|
|
Ok(_) => (),
|
|
Err(e) => {
|
|
log::warn!("update password was not connect ({:?})", e);
|
|
self.purge_password(PasswordType::Update);
|
|
for b in keypair_bytes.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
return Err(RootkeyResult::KeyError);
|
|
}
|
|
}
|
|
|
|
//------ test that the provided encryption key can actually decrypt the boot image
|
|
// this ensures that we don't brick the FPGA in case something weird happened with a difference
|
|
// between the backup FPGA's keying state, and the destination device's keying state.
|
|
// we do this by creating an oracle that can decrypt the boot gateware using the provided key.
|
|
// if we can create the oracle, it means we were able to decrypt the first little bit of the boot
|
|
// image and we're good to go!
|
|
match BitstreamOracle::new(
|
|
&pcache.fpga_key,
|
|
&pcache.fpga_key,
|
|
self.gateware(),
|
|
self.gateware_base(),
|
|
) {
|
|
Ok(_o) => log::debug!("Provided restore key could also decrypt the boot image."),
|
|
Err(e) => {
|
|
log::error!("couldn't create oracle (most likely FPGA key mismatch): {:?}", e);
|
|
self.purge_password(PasswordType::Update);
|
|
return Err(e);
|
|
}
|
|
};
|
|
old_key.copy_from_slice(&pcache.fpga_key);
|
|
|
|
// now encrypt the FPGA key for the Keyrom in-place to the provided password
|
|
for (word, hashed_pass) in unsafe {
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[KeyRomLocs::FPGA_KEY as usize
|
|
..KeyRomLocs::FPGA_KEY as usize + 256 / (size_of::<u32>() * 8)]
|
|
.iter_mut()
|
|
}
|
|
.zip(pcache.hashed_update_pw.chunks(4).into_iter())
|
|
{
|
|
*word = *word ^ u32::from_be_bytes(hashed_pass.try_into().unwrap());
|
|
}
|
|
|
|
pb.set_percentage(3);
|
|
pb
|
|
} else {
|
|
// regular and bbram flow
|
|
// don't show the rootkey modals until after the key is computed, so we can pop up
|
|
// the BIP-39 dialog box.
|
|
|
|
// decrypt the FPGA key using the stored password
|
|
if pcache.hashed_update_pw_valid == 0 && self.is_initialized() {
|
|
self.purge_password(PasswordType::Update);
|
|
log::error!("no password was set going into the update routine");
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("key: {:x?}", pcache.hashed_update_pw);
|
|
log::debug!("valid: {}", pcache.hashed_update_pw_valid);
|
|
|
|
return Err(RootkeyResult::KeyError);
|
|
}
|
|
for (&src, dst) in self.read_key_256(KeyRomLocs::FPGA_KEY).iter().zip(pcache.fpga_key.iter_mut())
|
|
{
|
|
*dst = src;
|
|
}
|
|
log::debug!("fpga key (encrypted): {:x?}", &pcache.fpga_key);
|
|
for (fkey, &pw) in pcache.fpga_key.iter_mut().zip(pcache.hashed_update_pw.iter()) {
|
|
*fkey = *fkey ^ pw;
|
|
}
|
|
pcache.fpga_key_valid = 1;
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("fpga key (reconstituted): {:x?}", &pcache.fpga_key);
|
|
|
|
// derive signing key
|
|
let enc_signing_key = self.read_key_256(KeyRomLocs::SELFSIGN_PRIVKEY);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("encrypted root privkey: {:x?}", enc_signing_key);
|
|
for (key, (&enc_key, &pw)) in keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
|
|
.iter_mut()
|
|
.zip(enc_signing_key.iter().zip(pcache.hashed_update_pw.iter()))
|
|
{
|
|
*key = enc_key ^ pw;
|
|
}
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("decrypted root privkey: {:x?}", &keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]);
|
|
// derived_sk now holds the "Root" secret key. It needs to be hashed (MAX_ROLLBACK_LIMIT -
|
|
// GLOBAL_ROLLBACK) times to get the current signing key.
|
|
self.compute_key_rollback(&mut keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]);
|
|
// now populate the public key portion. that's just in the plain.
|
|
// note that this would have been updated in the case of an update to GLOBAL_ROLLBACK -- the
|
|
// purpose of this routine is to sign software in the current rollback count, not to
|
|
// increment the rollback count
|
|
for (key, &src) in keypair_bytes[ed25519_dalek::SECRET_KEY_LENGTH..]
|
|
.iter_mut()
|
|
.zip(self.read_key_256(KeyRomLocs::SELFSIGN_PUBKEY).iter())
|
|
{
|
|
*key = src;
|
|
}
|
|
|
|
// stage the keyrom data for patching
|
|
self.populate_sensitive_data();
|
|
if update_type == UpdateType::BbramProvision || update_type == UpdateType::EfuseProvision {
|
|
if self.is_initialized() {
|
|
// make a backup copy of the old key, so we can use it to decrypt the gateware before
|
|
// re-encrypting it
|
|
old_key.copy_from_slice(&pcache.fpga_key);
|
|
}
|
|
self.replace_fpga_key();
|
|
|
|
// we transmit the BBRAM key at this point -- because if there's going to be a failure,
|
|
// we'd rather know it now before moving on. Three copies are sent to provide some
|
|
// check on the integrity of the key.
|
|
if update_type == UpdateType::BbramProvision {
|
|
log::info!("BBKEY|: {:?}", &pcache.fpga_key);
|
|
log::info!("BBKEY|: {:?}", &pcache.fpga_key);
|
|
log::info!("BBKEY|: {:?}", &pcache.fpga_key);
|
|
log::info!("{}", crate::CONSOLE_SENTINEL);
|
|
} else if update_type == UpdateType::EfuseProvision {
|
|
// share the backup key with the user so it can be saved somewhere safe
|
|
modals
|
|
.show_bip39(Some(t!("rootkeys.backup_key", locales::LANG)), &pcache.fpga_key.to_vec())
|
|
.ok();
|
|
loop {
|
|
modals.add_list_item(t!("rootkeys.gwup.yes", locales::LANG)).expect("modals error");
|
|
modals.add_list_item(t!("rootkeys.gwup.no", locales::LANG)).expect("modals error");
|
|
log::info!("{}ROOTKEY.CONFIRM,{}", xous::BOOKEND_START, xous::BOOKEND_END);
|
|
match modals.get_radiobutton(t!("rootkeys.backup_verify", locales::LANG)) {
|
|
Ok(response) => {
|
|
if response == t!("rootkeys.gwup.yes", locales::LANG) {
|
|
match modals
|
|
.input_bip39(Some(t!("rootkeys.backup_key_enter", locales::LANG)))
|
|
{
|
|
Ok(verify) => {
|
|
log::debug!("got bip39 verification: {:x?}", verify);
|
|
if &verify == &pcache.fpga_key {
|
|
log::debug!("verify succeeded");
|
|
modals
|
|
.show_notification(
|
|
t!("rootkeys.backup_key_match", locales::LANG),
|
|
None,
|
|
)
|
|
.ok();
|
|
break;
|
|
} else {
|
|
log::debug!("verify failed");
|
|
modals
|
|
.show_bip39(
|
|
Some(t!(
|
|
"rootkeys.backup_key_mismatch",
|
|
locales::LANG
|
|
)),
|
|
&pcache.fpga_key.to_vec(),
|
|
)
|
|
.ok();
|
|
}
|
|
}
|
|
_ => {
|
|
log::debug!("bip39 verification aborted");
|
|
modals
|
|
.show_bip39(
|
|
Some(t!("rootkeys.backup_key_mismatch", locales::LANG)),
|
|
&pcache.fpga_key.to_vec(),
|
|
)
|
|
.ok();
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
_ => break,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
old_key.copy_from_slice(&pcache.fpga_key);
|
|
};
|
|
|
|
// *now* show the progress bar...
|
|
rootkeys_modal.modify(
|
|
Some(ActionType::Slider(progress_action)),
|
|
Some(t!("rootkeys.gwup_starting", locales::LANG)),
|
|
false,
|
|
None,
|
|
true,
|
|
None,
|
|
);
|
|
rootkeys_modal.activate();
|
|
xous::yield_slice(); // give some time to the GAM to render
|
|
let mut pb = ProgressBar::new(rootkeys_modal, &mut progress_action);
|
|
pb.set_percentage(3);
|
|
pb
|
|
};
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("anti-rollback privkey: {:x?}", &keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("trying to make a keypair from {:x?}", keypair_bytes);
|
|
// Keypair zeroizes on drop
|
|
let keypair: Option<SigningKey> = if ((update_type == UpdateType::BbramProvision)
|
|
|| (update_type == UpdateType::EfuseProvision))
|
|
&& !self.is_initialized()
|
|
{
|
|
// don't try to derive signing keys if we're doing BBRAM provisioning on an otherwise blank device
|
|
None
|
|
} else {
|
|
Some(SigningKey::from_keypair_bytes(&keypair_bytes).map_err(|_| RootkeyResult::KeyError)?)
|
|
};
|
|
log::debug!("keypair success");
|
|
#[cfg(feature = "hazardous-debug")]
|
|
if keypair.is_some() {
|
|
log::debug!(
|
|
"keypair privkey (after anti-rollback): {:x?}",
|
|
keypair.as_ref().unwrap().secret.to_bytes()
|
|
);
|
|
}
|
|
|
|
pb.set_percentage(4);
|
|
log::debug!("making destination oracle");
|
|
let mut dst_oracle =
|
|
match BitstreamOracle::new(&old_key, &pcache.fpga_key, self.gateware(), self.gateware_base()) {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
self.purge_password(PasswordType::Update);
|
|
log::error!("couldn't create oracle (most likely FPGA key mismatch): {:?}", e);
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
let mut next_progress =
|
|
if (update_type == UpdateType::BbramProvision) || (update_type == UpdateType::EfuseProvision) {
|
|
pb.update_text(t!("rootkeys.init.backup_gateware", locales::LANG));
|
|
pb.rebase_subtask_percentage(5, 25);
|
|
self.make_gateware_backup(Some(&mut pb), false)?;
|
|
25
|
|
} else {
|
|
10
|
|
};
|
|
log::debug!("destination oracle success");
|
|
if update_type == UpdateType::BbramProvision {
|
|
dst_oracle.set_target_key_type(FpgaKeySource::Bbram);
|
|
} else if update_type == UpdateType::EfuseProvision {
|
|
dst_oracle.set_target_key_type(FpgaKeySource::Efuse);
|
|
} else {
|
|
let keysource = dst_oracle.get_original_key_type();
|
|
dst_oracle.set_target_key_type(keysource);
|
|
}
|
|
pb.set_percentage(next_progress);
|
|
next_progress += 2;
|
|
// updates are always encrypted with the null key.
|
|
let dummy_key: [u8; 32] = [0; 32];
|
|
log::debug!("making source oracle");
|
|
let mut src_oracle =
|
|
match BitstreamOracle::new(&dummy_key, &pcache.fpga_key, self.staging(), self.staging_base()) {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
log::error!("couldn't create oracle (most likely FPGA key mismatch): {:?}", e);
|
|
dst_oracle.clear();
|
|
self.purge_password(PasswordType::Update);
|
|
return Err(e);
|
|
}
|
|
};
|
|
log::debug!("source oracle success");
|
|
|
|
log::info!("source key type: {:?}", src_oracle.get_original_key_type());
|
|
log::info!("destination key type: {:?}", dst_oracle.get_target_key_type());
|
|
|
|
pb.set_percentage(next_progress);
|
|
pb.update_text(t!("rootkeys.init.patching_keys", locales::LANG));
|
|
pb.rebase_subtask_percentage(next_progress, 60);
|
|
self.gateware_copy_and_patch(&src_oracle, &dst_oracle, Some(&mut pb))?;
|
|
|
|
// make a copy of the plaintext metadata and csr records
|
|
self.spinor
|
|
.patch(
|
|
self.gateware(),
|
|
self.gateware_base(),
|
|
&self.staging()[METADATA_OFFSET..SELFSIG_OFFSET],
|
|
METADATA_OFFSET as u32,
|
|
)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
|
|
// verify that the patch worked
|
|
pb.update_text(t!("rootkeys.init.verifying_gateware", locales::LANG));
|
|
pb.rebase_subtask_percentage(60, 75);
|
|
log::debug!("making verification oracle");
|
|
let verify_oracle = match BitstreamOracle::new(
|
|
&pcache.fpga_key,
|
|
&pcache.fpga_key,
|
|
self.gateware(),
|
|
self.gateware_base(),
|
|
) {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
log::error!("couldn't create oracle (most likely FPGA key mismatch): {:?}", e);
|
|
return Err(e);
|
|
}
|
|
};
|
|
self.verify_gateware(&verify_oracle, Some(&mut pb))?;
|
|
|
|
pb.set_percentage(76);
|
|
|
|
// commit signatures
|
|
let keypair = if let Some(kp) = keypair {
|
|
pb.update_text(t!("rootkeys.init.signing_gateware", locales::LANG));
|
|
let (gateware_sig, gateware_len) = self.sign_gateware(&kp);
|
|
log::debug!("gateware signature ({}): {:x?}", gateware_len, gateware_sig.to_bytes());
|
|
self.commit_signature(gateware_sig, gateware_len, SignatureType::Gateware)?;
|
|
|
|
// sign the kernel
|
|
pb.update_text(t!("rootkeys.init.signing_kernel", locales::LANG));
|
|
pb.set_percentage(80);
|
|
let (kernel_sig, kernel_len) = self.sign_kernel(&kp);
|
|
|
|
// sign the loader
|
|
pb.update_text(t!("rootkeys.init.signing_loader", locales::LANG));
|
|
pb.rebase_subtask_percentage(85, 92);
|
|
let (loader_sig, loader_len) = self.sign_loader(&kp, Some(&mut pb));
|
|
|
|
// commit the signatures
|
|
pb.update_text(t!("rootkeys.init.commit_signatures", locales::LANG));
|
|
self.commit_signature(loader_sig, loader_len, SignatureType::Loader)?;
|
|
log::debug!("loader {} bytes, sig: {:x?}", loader_len, loader_sig.to_bytes());
|
|
pb.set_percentage(88);
|
|
self.commit_signature(kernel_sig, kernel_len, SignatureType::Kernel)?;
|
|
pb.set_percentage(89);
|
|
|
|
// pass the kp back into the original variable. keypair does not implement copy...for good
|
|
// reasons.
|
|
Some(kp)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// clean up the oracles
|
|
pb.set_percentage(90);
|
|
src_oracle.clear();
|
|
dst_oracle.clear();
|
|
// make a backup copy of the public key before we purge it. Pubkey is...public, so that's fine!
|
|
let pubkey = match update_type {
|
|
UpdateType::Restore => {
|
|
log::info!("Restore process is verifying using staged public key");
|
|
VerifyingKey::from_bytes(&self.read_staged_key_256(KeyRomLocs::SELFSIGN_PUBKEY))
|
|
.expect("public key was not valid")
|
|
}
|
|
_ => VerifyingKey::from_bytes(&self.read_key_256(KeyRomLocs::SELFSIGN_PUBKEY))
|
|
.expect("public key was not valid"),
|
|
};
|
|
self.purge_sensitive_data();
|
|
self.spinor.set_staging_write_protect(false).expect("couldn't un-protect the staging area");
|
|
for b in keypair_bytes.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
for b in old_key.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
// ed25519 keypair zeroizes on drop
|
|
|
|
// check signatures
|
|
if keypair.is_some() {
|
|
pb.set_percentage(92);
|
|
if !self.verify_gateware_self_signature(Some(&pubkey)) {
|
|
return Err(RootkeyResult::IntegrityError);
|
|
}
|
|
// as a sanity check, check the kernel self signature
|
|
if !self.verify_selfsign_kernel(Some(&pubkey)) {
|
|
log::error!("kernel signature failed to verify, probably should not try to reboot!");
|
|
return Err(RootkeyResult::IntegrityError);
|
|
}
|
|
}
|
|
|
|
if update_type == UpdateType::BbramProvision {
|
|
pb.set_percentage(95);
|
|
self.ticktimer.sleep_ms(500).unwrap();
|
|
// this will kick off the programming
|
|
log::info!("BURN_NOW");
|
|
log::info!("{}", crate::CONSOLE_SENTINEL);
|
|
|
|
// the key burning routine should finish before this timeout happens, and the system should have
|
|
// been rebooted
|
|
self.ticktimer.sleep_ms(10_000).unwrap();
|
|
|
|
pb.update_text(t!("rootkeys.bbram.failed_restore", locales::LANG));
|
|
pb.set_percentage(0);
|
|
pb.rebase_subtask_percentage(0, 100);
|
|
self.make_gateware_backup(Some(&mut pb), true)?;
|
|
} else if update_type == UpdateType::EfuseProvision {
|
|
pb.update_text(t!("rootkeys.efuse_burning", locales::LANG));
|
|
self.ticktimer.sleep_ms(300).ok();
|
|
pb.set_percentage(93);
|
|
self.ticktimer.sleep_ms(300).ok();
|
|
#[cfg(feature = "hazardous-debug")]
|
|
match self.jtag.efuse_fetch() {
|
|
Ok(rec) => {
|
|
log::info!("Efuse record before burn: {:?}", rec);
|
|
}
|
|
Err(e) => {
|
|
log::error!("failed to fetch jtag record: {:?}", e);
|
|
return Err(RootkeyResult::IntegrityError);
|
|
}
|
|
}
|
|
log::info!("{}EFUSE.BURN,{}", xous::BOOKEND_START, xous::BOOKEND_END);
|
|
// burn the key here to eFuses
|
|
match self.jtag.efuse_key_burn(pcache.fpga_key) {
|
|
Ok(result) => {
|
|
if !result {
|
|
pb.update_text(t!("rootkeys.efuse_burn_fail", locales::LANG));
|
|
self.ticktimer.sleep_ms(2000).ok();
|
|
return Err(RootkeyResult::KeyError);
|
|
} else {
|
|
log::info!("{}EFUSE.BURN_OK,{}", xous::BOOKEND_START, xous::BOOKEND_END);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
pb.update_text(&format!(
|
|
"{}\n{:?}",
|
|
t!("rootkeys.efuse_internal_error", locales::LANG),
|
|
e
|
|
));
|
|
self.ticktimer.sleep_ms(2000).ok();
|
|
return Err(RootkeyResult::StateError);
|
|
}
|
|
}
|
|
log::info!("{}EFUSE.BURN_DONE,{}", xous::BOOKEND_START, xous::BOOKEND_END);
|
|
|
|
#[cfg(feature = "hazardous-debug")]
|
|
match self.jtag.efuse_fetch() {
|
|
Ok(rec) => {
|
|
log::info!("Efuse record readback: {:?}", rec);
|
|
}
|
|
Err(e) => {
|
|
log::error!("failed to fetch jtag record: {:?}", e);
|
|
return Err(RootkeyResult::IntegrityError);
|
|
}
|
|
}
|
|
|
|
// seal the device from key readout, force encrypted boot
|
|
pb.update_text(t!("rootkeys.efuse_sealing", locales::LANG));
|
|
pb.set_percentage(96);
|
|
log::info!("{}EFUSE.SEAL,{}", xous::BOOKEND_START, xous::BOOKEND_END);
|
|
match self.jtag.seal_device() {
|
|
Ok(result) => {
|
|
if !result {
|
|
pb.update_text(t!("rootkeys.efuse_seal_fail", locales::LANG));
|
|
self.ticktimer.sleep_ms(2000).ok();
|
|
return Err(RootkeyResult::FlashError);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
pb.update_text(&format!(
|
|
"{}\n{:?}",
|
|
t!("rootkeys.efuse_internal_error", locales::LANG),
|
|
e
|
|
));
|
|
self.ticktimer.sleep_ms(2000).ok();
|
|
return Err(RootkeyResult::StateError);
|
|
}
|
|
}
|
|
log::info!("{}EFUSE.SEAL_OK,{}", xous::BOOKEND_START, xous::BOOKEND_END);
|
|
|
|
#[cfg(feature = "hazardous-debug")]
|
|
match self.jtag.efuse_fetch() {
|
|
Ok(rec) => {
|
|
log::info!("Efuse record after seal: {:?}", rec);
|
|
}
|
|
Err(e) => {
|
|
log::error!("failed to fetch jtag record: {:?}", e);
|
|
return Err(RootkeyResult::IntegrityError);
|
|
}
|
|
}
|
|
}
|
|
pb.set_percentage(100);
|
|
|
|
// check if we're to purge the password on completion
|
|
if self.update_password_policy == PasswordRetentionPolicy::AlwaysPurge {
|
|
self.purge_password(PasswordType::Update);
|
|
}
|
|
// log::set_max_level(log::LevelFilter::Info);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn do_sign_xous(
|
|
&mut self,
|
|
rootkeys_modal: &mut Modal,
|
|
main_cid: xous::CID,
|
|
) -> Result<(), RootkeyResult> {
|
|
// make sure the system is sane
|
|
self.xous_init_interlock();
|
|
|
|
// setup Ux
|
|
let mut progress_action =
|
|
Slider::new(main_cid, Opcode::UxGutter.to_u32().unwrap(), 0, 100, 10, Some("%"), 0, true, true);
|
|
progress_action.set_is_password(true);
|
|
// now show the init wait note...
|
|
rootkeys_modal.modify(
|
|
Some(ActionType::Slider(progress_action)),
|
|
Some(t!("rootkeys.gwup_starting", locales::LANG)),
|
|
false,
|
|
None,
|
|
true,
|
|
None,
|
|
);
|
|
rootkeys_modal.activate();
|
|
xous::yield_slice(); // give some time to the GAM to render
|
|
let mut pb = ProgressBar::new(rootkeys_modal, &mut progress_action);
|
|
pb.set_percentage(1);
|
|
|
|
// derive signing key
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
if pcache.hashed_update_pw_valid == 0 {
|
|
self.purge_password(PasswordType::Update);
|
|
log::error!("no password was set going into the update routine");
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("key: {:x?}", pcache.hashed_update_pw);
|
|
log::debug!("valid: {}", pcache.hashed_update_pw_valid);
|
|
|
|
return Err(RootkeyResult::KeyError);
|
|
}
|
|
let mut keypair_bytes: [u8; ed25519_dalek::KEYPAIR_LENGTH] = [0; ed25519_dalek::KEYPAIR_LENGTH];
|
|
let enc_signing_key = self.read_key_256(KeyRomLocs::SELFSIGN_PRIVKEY);
|
|
for (key, (&enc_key, &pw)) in keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
|
|
.iter_mut()
|
|
.zip(enc_signing_key.iter().zip(pcache.hashed_update_pw.iter()))
|
|
{
|
|
*key = enc_key ^ pw;
|
|
}
|
|
self.compute_key_rollback(&mut keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!(
|
|
"keypair privkey (after anti-rollback): {:x?}",
|
|
&keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
|
|
);
|
|
for (key, &src) in keypair_bytes[ed25519_dalek::SECRET_KEY_LENGTH..]
|
|
.iter_mut()
|
|
.zip(self.read_key_256(KeyRomLocs::SELFSIGN_PUBKEY).iter())
|
|
{
|
|
*key = src;
|
|
}
|
|
// Keypair zeroizes the secret key on drop.
|
|
let keypair = SigningKey::from_keypair_bytes(&keypair_bytes).map_err(|_| RootkeyResult::KeyError)?;
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("keypair privkey (after anti-rollback + conversion): {:x?}", keypair.secret.to_bytes());
|
|
|
|
// check if the keypair is valid by signing and verifying a short message
|
|
let test_data = "whiskey made me do it";
|
|
let test_sig = keypair.sign(test_data.as_bytes());
|
|
match keypair.verify(&test_data.as_bytes(), &test_sig) {
|
|
Ok(_) => (),
|
|
Err(e) => {
|
|
log::warn!("update password was not connect ({:?})", e);
|
|
self.purge_password(PasswordType::Update);
|
|
for b in keypair_bytes.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
return Err(RootkeyResult::KeyError);
|
|
}
|
|
}
|
|
|
|
// Question: do we want to re-verify the kernel and loader's devkey sign immediately before
|
|
// re-signing them? Nominally, they are checked on boot, but there is an opportunity for
|
|
// a TOCTOU by not re-verifying them.
|
|
|
|
// sign the kernel
|
|
pb.update_text(t!("rootkeys.init.signing_kernel", locales::LANG));
|
|
pb.set_percentage(35);
|
|
let (kernel_sig, kernel_len) = self.sign_kernel(&keypair);
|
|
|
|
// sign the loader
|
|
pb.update_text(t!("rootkeys.init.signing_loader", locales::LANG));
|
|
pb.rebase_subtask_percentage(35, 85);
|
|
let (loader_sig, loader_len) = self.sign_loader(&keypair, Some(&mut pb));
|
|
log::info!("loader signature: {:x?}", loader_sig.to_bytes());
|
|
log::info!("loader len: {} bytes", loader_len);
|
|
|
|
// commit the signatures
|
|
pb.update_text(t!("rootkeys.init.commit_signatures", locales::LANG));
|
|
self.commit_signature(loader_sig, loader_len, SignatureType::Loader)?;
|
|
log::debug!("loader {} bytes, sig: {:x?}", loader_len, loader_sig.to_bytes());
|
|
pb.set_percentage(90);
|
|
self.commit_signature(kernel_sig, kernel_len, SignatureType::Kernel)?;
|
|
pb.set_percentage(92);
|
|
|
|
// as a sanity check, check the kernel self signature
|
|
let ret = if !self.verify_selfsign_kernel(None) {
|
|
log::error!("kernel signature failed to verify, probably should not try to reboot!");
|
|
Err(RootkeyResult::IntegrityError)
|
|
} else {
|
|
Ok(())
|
|
};
|
|
|
|
// check if we're to purge the password on completion
|
|
if self.update_password_policy == PasswordRetentionPolicy::AlwaysPurge {
|
|
self.purge_password(PasswordType::Update);
|
|
}
|
|
// purge the temporaries that we can
|
|
for b in keypair_bytes.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
// ed25519 keypair zeroizes on drop
|
|
|
|
pb.set_percentage(100);
|
|
self.ticktimer.sleep_ms(250).expect("couldn't show final message");
|
|
|
|
ret
|
|
}
|
|
|
|
pub fn test(&mut self, rootkeys_modal: &mut Modal, main_cid: xous::CID) -> Result<(), RootkeyResult> {
|
|
let mut progress_action =
|
|
Slider::new(main_cid, Opcode::UxGutter.to_u32().unwrap(), 0, 100, 10, Some("%"), 0, true, true);
|
|
progress_action.set_is_password(true);
|
|
// now show the init wait note...
|
|
rootkeys_modal.modify(
|
|
Some(ActionType::Slider(progress_action)),
|
|
Some(t!("rootkeys.setup_wait", locales::LANG)),
|
|
false,
|
|
None,
|
|
true,
|
|
None,
|
|
);
|
|
rootkeys_modal.activate();
|
|
|
|
xous::yield_slice(); // give some time to the GAM to render
|
|
// capture the progress bar elements in a convenience structure
|
|
let mut pb = ProgressBar::new(rootkeys_modal, &mut progress_action);
|
|
|
|
// kick the progress bar to indicate we've entered the routine
|
|
for i in 1..100 {
|
|
pb.set_percentage(i);
|
|
self.ticktimer.sleep_ms(50).unwrap();
|
|
}
|
|
self.ticktimer.sleep_ms(1000).unwrap();
|
|
// the stop emoji, when sent to the slider action bar in progress mode, will cause it to close and
|
|
// relinquish focus
|
|
rootkeys_modal.key_event(['🛑', '\u{0000}', '\u{0000}', '\u{0000}']);
|
|
|
|
#[cfg(feature = "hazardous-debug")]
|
|
{
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
|
|
// setup the local cache
|
|
for addr in 0..256 {
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, addr);
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[addr as usize] =
|
|
self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
log::info!(
|
|
"{:02x}: 0x{:08x}",
|
|
addr,
|
|
self.sensitive_data.borrow_mut().as_slice::<u32>()[addr as usize]
|
|
);
|
|
}
|
|
|
|
for (word, key) in self.sensitive_data.borrow().as_slice::<u32>()
|
|
[KeyRomLocs::FPGA_KEY as usize..KeyRomLocs::FPGA_KEY as usize + 256 / (size_of::<u32>() * 8)]
|
|
.iter()
|
|
.zip(pcache.fpga_key.chunks_mut(4).into_iter())
|
|
{
|
|
for (&s, d) in word.to_be_bytes().iter().zip(key.iter_mut()) {
|
|
*d = s;
|
|
}
|
|
}
|
|
pcache.fpga_key_valid = 1;
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[KeyRomLocs::CONFIG as usize] |=
|
|
keyrom_config::INITIALIZED.ms(1);
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[0x30] = 0xc0de_600d;
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[0x31] = 0x1234_5678;
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[0x32] = 0x8000_0000;
|
|
self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[0x33] = 0x5555_3333;
|
|
|
|
// one time only
|
|
/*
|
|
match self.make_gateware_backup(30, 50, progress_modal, progress_action, false) {
|
|
Err(e) => {
|
|
log::error!("got spinor error: {:?}", e);
|
|
panic!("got spinor error, halting");
|
|
}
|
|
_ => ()
|
|
};*/
|
|
|
|
// make the oracles
|
|
assert!(pcache.fpga_key_valid == 1);
|
|
// copy from staging
|
|
let src_oracle =
|
|
BitstreamOracle::new(&pcache.fpga_key, &pcache.fpga_key, self.staging(), self.staging_base())
|
|
.unwrap();
|
|
// write to gateware
|
|
let dst_oracle = BitstreamOracle::new(
|
|
&pcache.fpga_key,
|
|
&pcache.fpga_key,
|
|
self.gateware(),
|
|
self.gateware_base(),
|
|
)
|
|
.unwrap();
|
|
|
|
// TEST
|
|
if self.gateware_copy_and_patch(&src_oracle, &dst_oracle, None).is_err() {
|
|
log::error!("error occured in patch_keys.");
|
|
return Err(RootkeyResult::FlashError);
|
|
}
|
|
|
|
if self.verify_gateware(&dst_oracle, None).is_err() {
|
|
log::error!("error occurred in gateware verification");
|
|
return Err(RootkeyResult::IntegrityError);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// blind-copy a staged gateware to the boot location. This can only be done if not root keys
|
|
/// have been initialized. This routine exists just to make the UX flow consistent between
|
|
/// unprovisioned and provisioned devices.
|
|
pub(crate) fn do_gateware_provision_uninitialized(
|
|
&mut self,
|
|
rootkeys_modal: &mut Modal,
|
|
main_cid: xous::CID,
|
|
) -> Result<(), RootkeyResult> {
|
|
if self.is_initialized() {
|
|
return Err(RootkeyResult::StateError);
|
|
}
|
|
let mut progress_action =
|
|
Slider::new(main_cid, Opcode::UxGutter.to_u32().unwrap(), 0, 100, 10, Some("%"), 0, true, true);
|
|
progress_action.set_is_password(true);
|
|
rootkeys_modal.modify(
|
|
Some(ActionType::Slider(progress_action)),
|
|
Some(t!("rootkeys.gwup_starting", locales::LANG)),
|
|
false,
|
|
None,
|
|
true,
|
|
None,
|
|
);
|
|
rootkeys_modal.activate();
|
|
xous::yield_slice(); // give some time to the GAM to render
|
|
let mut pb = ProgressBar::new(rootkeys_modal, &mut progress_action);
|
|
pb.set_percentage(1);
|
|
self.ticktimer.sleep_ms(250).expect("couldn't show final message");
|
|
self.make_gateware_backup(Some(&mut pb), true)?;
|
|
pb.set_percentage(100);
|
|
self.ticktimer.sleep_ms(250).expect("couldn't show final message");
|
|
Ok(())
|
|
}
|
|
|
|
/// copy data from a source region to a destination region, re-encrypting and patching as we go along.
|
|
/// the region/region_base should be specified for the destination oracle
|
|
///
|
|
/// KEYROM patching data must previously have been staged in the sensitive area.
|
|
/// failure to do so would result in the erasure of all secret data.
|
|
/// ASSUME: CSR appendix does not change during the copy (it is not copied/updated)
|
|
fn gateware_copy_and_patch(
|
|
&self,
|
|
src_oracle: &BitstreamOracle,
|
|
dst_oracle: &BitstreamOracle,
|
|
mut maybe_pb: Option<&mut ProgressBar>,
|
|
) -> Result<(), RootkeyResult> {
|
|
log::debug!(
|
|
"sanity checks: src_offset {}, dst_offset {}, src_len {}, dst_len {}",
|
|
src_oracle.ciphertext_offset(),
|
|
dst_oracle.ciphertext_offset(),
|
|
src_oracle.ciphertext_len(),
|
|
dst_oracle.ciphertext_len()
|
|
);
|
|
|
|
// start with a naive implementation that simple goes through and re-encrypts and patches everything.
|
|
// later on, we could skip patching everything up to the first frame that needs patching;
|
|
// this should optimize up to 15% of the time required to do a patch, although, because the patching
|
|
// routine is "smart" and does not issue writes for unchanged data, this could actually be a much
|
|
// smaller performance gain.
|
|
|
|
let mut pt_sector: [u8; spinor::SPINOR_ERASE_SIZE as usize] = [0; spinor::SPINOR_ERASE_SIZE as usize];
|
|
let mut ct_sector: [u8; spinor::SPINOR_ERASE_SIZE as usize] = [0; spinor::SPINOR_ERASE_SIZE as usize];
|
|
let mut flipper: [u8; spinor::SPINOR_ERASE_SIZE as usize] = [0; spinor::SPINOR_ERASE_SIZE as usize];
|
|
|
|
let mut hasher = Sha256::new();
|
|
let hash_stop = src_oracle.ciphertext_len() - 320 - 160;
|
|
|
|
// handle the special case of the first sector
|
|
src_oracle.decrypt(0, &mut pt_sector);
|
|
|
|
// initialize the hmac code for later use
|
|
let mut hmac_code: [u8; 32] = [0; 32];
|
|
for (dst, (&hm1, &mask)) in
|
|
hmac_code.iter_mut().zip(pt_sector[0..32].iter().zip(pt_sector[32..64].iter()))
|
|
{
|
|
*dst = hm1 ^ mask;
|
|
}
|
|
log::debug!("recovered hmac: {:x?}", hmac_code);
|
|
log::debug!("hmac constant: {:x?}", &pt_sector[32..64]);
|
|
let mut bytes_hashed = spinor::SPINOR_ERASE_SIZE as usize - src_oracle.ciphertext_offset();
|
|
|
|
// encrypt and patch the data to disk
|
|
dst_oracle.encrypt_sector(
|
|
-(dst_oracle.ciphertext_offset() as i32),
|
|
&mut pt_sector[..bytes_hashed],
|
|
&mut ct_sector, // full array, for space for plaintext header
|
|
);
|
|
|
|
// hash the first sector; the pt_sector could have been patched by encrypt_sector() to change the key
|
|
// soucre, so it must be done *after* we call encrypt_sector()
|
|
bitflip(&pt_sector[..bytes_hashed], &mut flipper[..bytes_hashed]);
|
|
hasher.update(&flipper[..bytes_hashed]);
|
|
|
|
log::debug!("sector 0 patch len: {}", bytes_hashed);
|
|
log::debug!("sector 0 header: {:x?}", &ct_sector[..dst_oracle.ciphertext_offset()]);
|
|
self.spinor
|
|
.patch(dst_oracle.bitstream(), dst_oracle.base(), &ct_sector, 0)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
|
|
// now we can patch the rest of the sectors as a loop
|
|
let mut from = spinor::SPINOR_ERASE_SIZE - src_oracle.ciphertext_offset() as u32;
|
|
let mut dummy_consume = 0;
|
|
if let Some(ref mut pb) = maybe_pb {
|
|
pb.rebase_subtask_work(
|
|
0,
|
|
src_oracle.ciphertext()[from as usize..]
|
|
.chunks(spinor::SPINOR_ERASE_SIZE as usize)
|
|
.into_iter()
|
|
.count() as u32,
|
|
);
|
|
}
|
|
for _ in
|
|
src_oracle.ciphertext()[from as usize..].chunks(spinor::SPINOR_ERASE_SIZE as usize).into_iter()
|
|
{
|
|
let decrypt_len = src_oracle.decrypt(from as usize, &mut pt_sector);
|
|
|
|
// wrap the patch call with a range check, because the patch lookup search is pretty expensive
|
|
if self.patch_in_range(src_oracle, from, from + spinor::SPINOR_ERASE_SIZE as u32) {
|
|
dummy_consume ^= self.patch_sector(src_oracle, from, &mut pt_sector, unsafe {
|
|
&self.sensitive_data.borrow_mut().as_slice_mut::<u32>()[0..256]
|
|
});
|
|
}
|
|
|
|
let hash_len =
|
|
if bytes_hashed + decrypt_len < hash_stop { decrypt_len } else { hash_stop - bytes_hashed };
|
|
|
|
// encrypt before hashing, because a bit that selects the key type can be patched by the encryptor
|
|
dst_oracle.encrypt_sector(
|
|
from as i32,
|
|
&mut pt_sector[..decrypt_len],
|
|
&mut ct_sector[..decrypt_len],
|
|
);
|
|
|
|
if hash_len > 0 {
|
|
bitflip(&pt_sector[..hash_len], &mut flipper[..hash_len]);
|
|
hasher.update(&flipper[..hash_len]);
|
|
bytes_hashed += hash_len;
|
|
if hash_len != decrypt_len {
|
|
log::debug!("final short block len: {}", hash_len);
|
|
}
|
|
}
|
|
|
|
self.spinor
|
|
.patch(
|
|
dst_oracle.bitstream(),
|
|
dst_oracle.base(),
|
|
&ct_sector[..decrypt_len],
|
|
from + dst_oracle.ciphertext_offset() as u32,
|
|
)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
|
|
from += decrypt_len as u32;
|
|
if let Some(ref mut pb) = maybe_pb {
|
|
pb.increment_work(1);
|
|
}
|
|
}
|
|
// consume the dummy value, to ensure it's not optimized out by the compiler
|
|
log::debug!("copy_and_patch dummy consume: 0x{:x}", dummy_consume);
|
|
|
|
// at this point, we've patched & encrypted all the sectors, but we've
|
|
// encrypted the wrong hash at the end, so the HMAC won't work out. let's patch that.
|
|
let h1_digest: [u8; 32] = hasher.finalize().try_into().unwrap();
|
|
log::debug!("bytes hashed: {}, computed h1 hash: {:x?}", bytes_hashed, h1_digest);
|
|
|
|
let mut hasher2 = Sha256::new();
|
|
let footer_mask: [u8; 32] = [0x3A; 32];
|
|
let mut masked_footer: [u8; 32] = [0; 32];
|
|
for (dst, (&hm2, &mask)) in masked_footer.iter_mut().zip(hmac_code.iter().zip(footer_mask.iter())) {
|
|
*dst = hm2 ^ mask;
|
|
}
|
|
log::debug!("masked_footer: {:x?}", masked_footer);
|
|
log::debug!("footer_mask: {:x?}", footer_mask);
|
|
let mut masked_footer_flipped: [u8; 32] = [0; 32];
|
|
bitflip(&masked_footer, &mut masked_footer_flipped);
|
|
let mut footer_mask_flipped: [u8; 32] = [0; 32];
|
|
bitflip(&footer_mask, &mut footer_mask_flipped);
|
|
hasher2.update(masked_footer_flipped);
|
|
hasher2.update(footer_mask_flipped);
|
|
hasher2.update(h1_digest);
|
|
let h2_digest: [u8; 32] = hasher2.finalize().try_into().unwrap();
|
|
log::debug!("h2 hash: {:x?}", h2_digest);
|
|
|
|
// now encrypt and patch this final hashed value into the expected area
|
|
let ct_end = dst_oracle.ciphertext_len();
|
|
|
|
// the math below will have some problems if the block happens to end up exactly on an erase block
|
|
// boundary. but it doesn't, so I'm going to leave that case unaddressed...
|
|
let ct_last_block_loc =
|
|
(ct_end & !(spinor::SPINOR_ERASE_SIZE as usize - 1)) - dst_oracle.ciphertext_offset();
|
|
let pt_sector_len = ct_end - ct_last_block_loc;
|
|
|
|
log::trace!(
|
|
"ct_end: {}, ct_last_block_loc {}, pt_sector_len {}",
|
|
ct_end,
|
|
ct_last_block_loc,
|
|
pt_sector_len
|
|
);
|
|
src_oracle.decrypt(ct_last_block_loc, &mut pt_sector[..pt_sector_len]);
|
|
|
|
// patch in the hash at the very end
|
|
let mut h2_digest_flipped: [u8; 32] = [0; 32];
|
|
bitflip(&h2_digest, &mut h2_digest_flipped);
|
|
for (&src, dst) in
|
|
h2_digest_flipped.iter().zip(pt_sector[pt_sector_len - 32..pt_sector_len].iter_mut())
|
|
{
|
|
*dst = src;
|
|
}
|
|
log::debug!("last bytes patched: {:x?}", &pt_sector[pt_sector_len - 256..pt_sector_len]);
|
|
|
|
dst_oracle.encrypt_sector(
|
|
ct_last_block_loc as i32,
|
|
&mut pt_sector[..pt_sector_len],
|
|
&mut ct_sector[..pt_sector_len],
|
|
);
|
|
log::trace!("hash patching from 0x{:x} len {}", ct_last_block_loc, pt_sector_len);
|
|
self.spinor
|
|
.patch(
|
|
dst_oracle.bitstream(),
|
|
dst_oracle.base(),
|
|
&ct_sector[..pt_sector_len],
|
|
ct_last_block_loc as u32 + dst_oracle.ciphertext_offset() as u32,
|
|
)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn patch_in_range(&self, oracle: &BitstreamOracle, range_ct_start: u32, range_ct_end: u32) -> bool {
|
|
let (start_frame, _) = oracle.ciphertext_offset_to_frame(range_ct_start as usize);
|
|
let (end_frame, _) = oracle.ciphertext_offset_to_frame(range_ct_end as usize);
|
|
|
|
if end_frame < patch::PATCH_FRAMES[0] as usize
|
|
|| start_frame > *patch::PATCH_FRAMES.last().unwrap() as usize
|
|
{
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
// data_ct_start is the ciphertext offset of the region we're searching for a patch in
|
|
fn patch_sector(
|
|
&self,
|
|
oracle: &BitstreamOracle,
|
|
data_ct_start: u32,
|
|
data: &mut [u8],
|
|
keyrom: &[u32],
|
|
) -> u32 {
|
|
assert!(keyrom.len() == 256, "KEYROM data is not whole");
|
|
|
|
let mut dummy_consume = 0;
|
|
for (offset, word) in data.chunks_mut(4).into_iter().enumerate() {
|
|
let ct_offset = data_ct_start as usize + offset * 4;
|
|
let (frame, frame_offset) = oracle.ciphertext_offset_to_frame(ct_offset);
|
|
|
|
// the outer should_patch() call is a small optimization that replaces an O(N) search through
|
|
// a frame array with a pair of limit comparisons.
|
|
if patch::should_patch(frame as u32) {
|
|
match patch::patch_frame(frame as u32, frame_offset as u32, keyrom) {
|
|
Some((patch, patch_inv)) => {
|
|
let mut flip: [u8; 4] = [0; 4];
|
|
bitflip(&patch.to_be_bytes(), &mut flip);
|
|
log::trace!(
|
|
"patching {} frame 0x{:x} offset {} 0x{:x} -> 0x{:x}",
|
|
ct_offset,
|
|
frame,
|
|
frame_offset,
|
|
u32::from_be_bytes(word[0..4].try_into().unwrap()),
|
|
u32::from_be_bytes(flip)
|
|
);
|
|
for (&s, d) in flip.iter().zip(word.iter_mut()) {
|
|
*d = s;
|
|
}
|
|
dummy_consume ^= patch_inv; // consume the dummy value (to keep constant time properties)
|
|
}
|
|
None => {}
|
|
}
|
|
}
|
|
}
|
|
dummy_consume
|
|
}
|
|
|
|
fn verify_gateware(
|
|
&self,
|
|
oracle: &BitstreamOracle,
|
|
mut maybe_pb: Option<&mut ProgressBar>,
|
|
) -> Result<(), RootkeyResult> {
|
|
let mut hmac_area = [0; 64];
|
|
oracle.decrypt(0, &mut hmac_area);
|
|
let mut hmac_code: [u8; 32] = [0; 32];
|
|
for (dst, (&hm1, &mask)) in
|
|
hmac_code.iter_mut().zip(hmac_area[0..32].iter().zip(hmac_area[32..64].iter()))
|
|
{
|
|
*dst = hm1 ^ mask;
|
|
}
|
|
log::debug!("hmac code: {:x?}", hmac_code);
|
|
|
|
log::debug!("verifying gateware");
|
|
let mut hasher = Sha256::new();
|
|
// magic number alert:
|
|
// 160 = reserved space for the 2nd hash
|
|
// 320 = some padding that is built into the file format, for whatever reason xilinx picked
|
|
let tot_len = oracle.ciphertext_len() - 320 - 160;
|
|
// slow but steady. we can optimize later.
|
|
// for now, very tiny optimization - use blocksize * 2 because that happens to be divisible by the
|
|
// bitstream parameters
|
|
let mut decrypt = [0; AES_BLOCKSIZE * 2];
|
|
let mut flipped = [0; AES_BLOCKSIZE * 2];
|
|
if let Some(ref mut pb) = maybe_pb {
|
|
pb.rebase_subtask_work(0, tot_len as u32);
|
|
}
|
|
for index in (0..tot_len).step_by(AES_BLOCKSIZE * 2) {
|
|
oracle.decrypt(index, &mut decrypt);
|
|
bitflip(&decrypt, &mut flipped);
|
|
hasher.update(&flipped);
|
|
|
|
if let Some(ref mut pb) = maybe_pb {
|
|
pb.increment_work((AES_BLOCKSIZE * 2) as u32);
|
|
}
|
|
}
|
|
let h1_digest: [u8; 32] = hasher.finalize().try_into().unwrap();
|
|
log::debug!("computed hash of {} bytes: {:x?}", tot_len, h1_digest);
|
|
|
|
let mut hasher2 = Sha256::new();
|
|
let footer_mask: [u8; 32] = [0x3A; 32];
|
|
let mut masked_footer: [u8; 32] = [0; 32];
|
|
for (dst, (&hm2, &mask)) in masked_footer.iter_mut().zip(hmac_code.iter().zip(footer_mask.iter())) {
|
|
*dst = hm2 ^ mask;
|
|
}
|
|
log::debug!("masked_footer: {:x?}", masked_footer);
|
|
log::debug!("footer_mask: {:x?}", footer_mask);
|
|
let mut masked_footer_flipped: [u8; 32] = [0; 32];
|
|
bitflip(&masked_footer, &mut masked_footer_flipped);
|
|
let mut footer_mask_flipped: [u8; 32] = [0; 32];
|
|
bitflip(&footer_mask, &mut footer_mask_flipped);
|
|
hasher2.update(masked_footer_flipped);
|
|
hasher2.update(footer_mask_flipped);
|
|
hasher2.update(h1_digest);
|
|
let h2_digest: [u8; 32] = hasher2.finalize().try_into().unwrap();
|
|
|
|
log::debug!("h2 hash: {:x?}", h2_digest);
|
|
let mut ref_digest_flipped: [u8; 32] = [0; 32];
|
|
oracle.decrypt(oracle.ciphertext_len() - 32, &mut ref_digest_flipped);
|
|
let mut ref_digest: [u8; 32] = [0; 32];
|
|
log::debug!("ref digest (flipped): {:x?}", ref_digest_flipped);
|
|
bitflip(&ref_digest_flipped, &mut ref_digest);
|
|
log::debug!("ref digest : {:x?}", ref_digest);
|
|
|
|
let mut matching = true;
|
|
for (&l, &r) in ref_digest.iter().zip(h2_digest.iter()) {
|
|
if l != r {
|
|
matching = false;
|
|
}
|
|
}
|
|
|
|
if matching {
|
|
log::debug!("gateware verified");
|
|
Ok(())
|
|
} else {
|
|
log::error!("gateware failed to verify");
|
|
Err(RootkeyResult::IntegrityError)
|
|
}
|
|
}
|
|
|
|
fn make_gateware_backup(
|
|
&self,
|
|
mut maybe_pb: Option<&mut ProgressBar>,
|
|
do_restore: bool,
|
|
) -> Result<(), RootkeyResult> {
|
|
let gateware_dest = if !do_restore { self.staging() } else { self.gateware() };
|
|
let mut gateware_dest_base = if !do_restore { self.staging_base() } else { self.gateware_base() };
|
|
let gateware_src = if !do_restore { self.gateware() } else { self.staging() };
|
|
|
|
const PATCH_CHUNK: usize = 65536; // this controls the granularity of the erase operation
|
|
let mut prog_ctr = 0;
|
|
if let Some(ref mut pb) = maybe_pb {
|
|
pb.rebase_subtask_work(0, xous::SOC_STAGING_GW_LEN);
|
|
}
|
|
|
|
for (dst, src) in gateware_dest.chunks(PATCH_CHUNK).into_iter().zip(gateware_src.chunks(PATCH_CHUNK))
|
|
{
|
|
log::debug!("writing {} backup bytes to offset 0x{:08x}", src.len(), prog_ctr);
|
|
self.spinor.patch(dst, gateware_dest_base, src, 0).map_err(|_| RootkeyResult::FlashError)?;
|
|
gateware_dest_base += PATCH_CHUNK as u32;
|
|
|
|
prog_ctr += PATCH_CHUNK as u32;
|
|
if let Some(ref mut pb) = maybe_pb {
|
|
pb.increment_work(PATCH_CHUNK as u32);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "hazardous-debug")]
|
|
fn debug_staging(&self) {
|
|
self.debug_print_key(KeyRomLocs::FPGA_KEY as usize, 256, "FPGA key: ");
|
|
self.debug_print_key(KeyRomLocs::SELFSIGN_PRIVKEY as usize, 256, "Self private key: ");
|
|
self.debug_print_key(KeyRomLocs::SELFSIGN_PUBKEY as usize, 256, "Self public key: ");
|
|
self.debug_print_key(KeyRomLocs::DEVELOPER_PUBKEY as usize, 256, "Dev public key: ");
|
|
self.debug_print_key(KeyRomLocs::THIRDPARTY_PUBKEY as usize, 256, "3rd party public key: ");
|
|
self.debug_print_key(KeyRomLocs::USER_KEY as usize, 256, "Boot key: ");
|
|
self.debug_print_key(KeyRomLocs::PEPPER as usize, 128, "Pepper: ");
|
|
self.debug_print_key(KeyRomLocs::CONFIG as usize, 32, "Config (as BE): ");
|
|
self.debug_print_key(KeyRomLocs::GLOBAL_ROLLBACK as usize, 32, "Global rollback state: ");
|
|
}
|
|
|
|
#[cfg(feature = "hazardous-debug")]
|
|
fn debug_print_key(&self, offset: usize, num_bits: usize, name: &str) {
|
|
use core::fmt::Write;
|
|
let mut debugstr = xous_ipc::String::<4096>::new();
|
|
write!(debugstr, "{}", name).unwrap();
|
|
for word in self.sensitive_data.borrow_mut().as_slice::<u32>()
|
|
[offset..offset as usize + num_bits / (size_of::<u32>() * 8)]
|
|
.iter()
|
|
{
|
|
for byte in word.to_be_bytes().iter() {
|
|
write!(debugstr, "{:02x}", byte).unwrap();
|
|
}
|
|
}
|
|
log::info!("{}", debugstr);
|
|
}
|
|
|
|
/// Unfortunately we have to re-implement the signature details manually here.
|
|
/// these are based on copying the implementation out of the ed25519-dalek crate.
|
|
/// why, may you ask, do we do such a dangerous thing?
|
|
/// it's because the loader's memory space is shared between this crate, and the graphics crate
|
|
/// the font glyph data is part of the loader, it was put there because:
|
|
/// - we need fonts early in boot to show data
|
|
/// - like the loader, it's a large static data set that rarely changes
|
|
/// - this helps keep kernel updates faster and lighter, at the trade-off of some inconvenience of
|
|
/// loader updates
|
|
/// - because we have virtual memory, we must "borrow" copies of the font data for signing
|
|
/// - because we have limited physical memory, it's not a great idea to simply allocate a huge block of
|
|
/// RAM and copy the loader + font data there and sign it; it means in low memory conditions we can't
|
|
/// do signatures
|
|
///
|
|
/// The ideal solution for us is to pre-hash the entire loader region, and then have it signed.
|
|
/// the ed25519-dalek crate does support this, but, it's also not a "standard" hash and sign, because
|
|
/// it adds some good ideas to the signature. For entirely self-signed regions, we can use the
|
|
/// ed25519-dalek's version of the signature, but for interoperability we have to stick with the
|
|
/// traditional signature. Unfortunately, the traditional signature implementation only supports
|
|
/// passing the entire region as a slice, and not as a pre-hash. This is because the hash has to be
|
|
/// primed with a nonce that's derived from the secret key. So, we re-implement this, so we can
|
|
/// interleave the hash as required to allow us to process the font data in page-sized chunks that
|
|
/// don't use a huge amount of RAM.
|
|
///
|
|
/// Some more color on "interoperability reasons": originally, the pre-hash method of signing wasn't
|
|
/// hammered out when the APIs were created. So, the verification method baked into the SoC's ROM uses
|
|
/// a standard, non-prehash signature. While it would be trivial to upgrade *this* API to use a
|
|
/// now-standardized pre-hash signature, it would be risky to upgrade the SoC ROM (a measurable risk
|
|
/// of users bricking devices). So, we opt instead to carefully re-implement the signature check here,
|
|
/// rather than subject users to a risk of bricking.
|
|
///
|
|
/// However, it might be worth noting that for *future versions of hardware* we should migrate the
|
|
/// loader signature to a pre-hash version, so we have more flexibility on how we compute hashes and
|
|
/// we can stay with 100% spec-compliant APIs.
|
|
#[allow(non_snake_case)]
|
|
pub fn sign_loader(
|
|
&self,
|
|
signing_key: &SigningKey,
|
|
maybe_pb: Option<&mut ProgressBar>,
|
|
) -> (Signature, u32) {
|
|
let maybe_pb = maybe_pb.map(|pb| {
|
|
pb.rebase_subtask_work(0, 2);
|
|
pb
|
|
});
|
|
let loader_len = xous::LOADER_CODE_LEN
|
|
- SIGBLOCK_SIZE
|
|
+ graphics_server::fontmap::FONT_TOTAL_LEN as u32
|
|
// these also need to be updated in graphics-server/src/main.rs @ Some(Opcode::BulkReadfonts)
|
|
+ 16 // for the minimum compatible semver
|
|
+ 16 // for the current semver
|
|
+ 8; // two u32 words are appended to the end, which repeat the "version" and "length" fields encoded in the signature block
|
|
|
|
// compute the nonce. This is a small hash, so use a software hasher.
|
|
let mut nonce_hasher = Sha512Sw::new();
|
|
nonce_hasher.update(signing_key.as_bytes());
|
|
let hazmat = nonce_hasher.finalize();
|
|
let hash_prefix = &hazmat.as_slice()[32..];
|
|
|
|
// this is a huge hash, so, get a hardware hasher, even if it means waiting for it
|
|
let mut hasher = Sha512Hw::new();
|
|
// prime the hash with the nonce derived from the upper bits of the secret key's hash.
|
|
hasher.update(hash_prefix);
|
|
|
|
{
|
|
// this is the equivalent of hasher.update(&message)
|
|
let loader_region = self.loader_code();
|
|
// the loader data starts one page in; the first page is reserved for the signature itself
|
|
hasher.update(&loader_region[SIGBLOCK_SIZE as usize..]);
|
|
|
|
// now get the font plane data
|
|
self.gfx.bulk_read_restart(); // reset the bulk read pointers on the gfx side
|
|
let bulkread = BulkRead::default();
|
|
let mut buf = xous_ipc::Buffer::into_buf(bulkread)
|
|
.expect("couldn't transform bulkread into aligned buffer");
|
|
// this form of loop was chosen to avoid the multiple re-initializations and copies that would be
|
|
// entailed in our usual idiom for pasing buffers around. instead, we create a single
|
|
// buffer, and re-use it for every iteration of the loop.
|
|
loop {
|
|
buf.lend_mut(self.gfx.conn(), self.gfx.bulk_read_fontmap_op())
|
|
.expect("couldn't do bulkread from gfx");
|
|
let br = buf.as_flat::<BulkRead, _>().unwrap();
|
|
hasher.update(&br.buf[..br.len as usize]);
|
|
if br.len != bulkread.buf.len() as u32 {
|
|
log::trace!("non-full block len: {}", br.len);
|
|
}
|
|
if br.len < bulkread.buf.len() as u32 {
|
|
// read until we get a buffer that's not fully filled
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
let maybe_pb = maybe_pb.map(|pb| {
|
|
pb.increment_work(1);
|
|
pb
|
|
});
|
|
|
|
let r = Scalar::from_hash(hasher);
|
|
let R: CompressedEdwardsY = EdwardsPoint::mul_base(&r).compress();
|
|
|
|
let mut hasher = Sha512Hw::new();
|
|
hasher.update(R.as_bytes());
|
|
hasher.update(signing_key.verifying_key().as_bytes());
|
|
|
|
{
|
|
// this is the equivalent of hasher.update(&message)
|
|
let loader_region = self.loader_code();
|
|
// the loader data starts one page in; the first page is reserved for the signature itself
|
|
hasher.update(&loader_region[SIGBLOCK_SIZE as usize..]);
|
|
|
|
// now get the font plane data
|
|
self.gfx.bulk_read_restart(); // reset the bulk read pointers on the gfx side
|
|
let bulkread = BulkRead::default();
|
|
let mut buf = xous_ipc::Buffer::into_buf(bulkread)
|
|
.expect("couldn't transform bulkread into aligned buffer");
|
|
// this form of loop was chosen to avoid the multiple re-initializations and copies that would be
|
|
// entailed in our usual idiom for pasing buffers around. instead, we create a single
|
|
// buffer, and re-use it for every iteration of the loop.
|
|
loop {
|
|
buf.lend_mut(self.gfx.conn(), self.gfx.bulk_read_fontmap_op())
|
|
.expect("couldn't do bulkread from gfx");
|
|
let br = buf.as_flat::<BulkRead, _>().unwrap();
|
|
hasher.update(&br.buf[..br.len as usize]);
|
|
if br.len != bulkread.buf.len() as u32 {
|
|
log::trace!("non-full block len: {}", br.len);
|
|
}
|
|
if br.len < bulkread.buf.len() as u32 {
|
|
// read until we get a buffer that's not fully filled
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if let Some(pb) = maybe_pb {
|
|
pb.increment_work(1);
|
|
}
|
|
|
|
let k = Scalar::from_hash(hasher);
|
|
let s = &(&k * &signing_key.to_scalar()) + &r;
|
|
|
|
let mut signature_bytes: [u8; 64] = [0u8; 64];
|
|
|
|
signature_bytes[..32].copy_from_slice(&R.as_bytes()[..]);
|
|
signature_bytes[32..].copy_from_slice(&s.as_bytes()[..]);
|
|
|
|
(Signature::from_bytes(&signature_bytes), loader_len)
|
|
}
|
|
|
|
pub fn sign_kernel(&self, signing_key: &SigningKey) -> (Signature, u32) {
|
|
let kernel_sig_region = self.kernel_sig();
|
|
let kernel_end_region = self.kernel_end();
|
|
assert!(kernel_end_region.len() % 4096 == 0); // this must be aligned to a page boundary.
|
|
let end_mask = kernel_end_region.len() - 1;
|
|
|
|
// First, find the advertised length in the unchecked header, then, check it against the length in the
|
|
// signed region of the kernel
|
|
let sig_region = &kernel_sig_region[..core::mem::size_of::<SignatureInFlash>()];
|
|
let sig_rec: &SignatureInFlash =
|
|
unsafe { (sig_region.as_ptr() as *const SignatureInFlash).as_ref().unwrap() }; // this pointer better not be null, we just created it!
|
|
|
|
let kernel_len = sig_rec.signed_len as usize; // this length is an unchecked guideline
|
|
let protected_len = u32::from_le_bytes(
|
|
kernel_end_region[(SIGBLOCK_SIZE as usize + kernel_len as usize - 4) & end_mask
|
|
..(SIGBLOCK_SIZE as usize + kernel_len as usize) & end_mask]
|
|
.try_into()
|
|
.unwrap(),
|
|
);
|
|
|
|
// we now have a checked length derived from a region of the kernel that is signed, confirm that it
|
|
// matches the advertised length
|
|
log::info!(
|
|
"kernel total signed len 0x{:x}, kernel len inside signed region 0x{:x}",
|
|
kernel_len,
|
|
protected_len
|
|
);
|
|
// throw a panic -- this check should have passed during boot.
|
|
assert!(
|
|
(kernel_len) - 4 == protected_len as usize,
|
|
"The advertised kernel length does not match the signed length"
|
|
);
|
|
|
|
// force the records to match our measured values
|
|
let mut len_data = [0u8; 8];
|
|
for (&src, dst) in SIG_KERNEL_VERSION.to_le_bytes().iter().zip(len_data[..4].iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
for (&src, dst) in (kernel_len as u32 - 4).to_le_bytes().iter().zip(len_data[4..].iter_mut()) {
|
|
*dst = src;
|
|
}
|
|
log::info!(
|
|
"kernel len area before: {:x?}",
|
|
&(self.kernel_end()[(SIGBLOCK_SIZE as usize + kernel_len - 8) & end_mask
|
|
..(SIGBLOCK_SIZE as usize + kernel_len) & end_mask])
|
|
);
|
|
self.spinor
|
|
.patch(
|
|
self.kernel_end(),
|
|
self.kernel_end_base(),
|
|
&len_data,
|
|
(SIGBLOCK_SIZE + kernel_len as u32 - 8) & end_mask as u32,
|
|
)
|
|
.expect("couldn't patch length area");
|
|
log::info!(
|
|
"kernel len area after: {:x?}",
|
|
&(self.kernel_end()[(SIGBLOCK_SIZE as usize + kernel_len - 8) & end_mask
|
|
..(SIGBLOCK_SIZE as usize + kernel_len) & end_mask])
|
|
);
|
|
(
|
|
signing_key.try_sign_digest(self.kernel_hash()).expect("Error computing pre-hash signature"),
|
|
kernel_len as u32,
|
|
)
|
|
}
|
|
|
|
/// the public key must already be in the cache -- this version is used by the init routine, before the
|
|
/// keys are written
|
|
pub fn verify_selfsign_kernel(&mut self, maybe_pubkey: Option<&VerifyingKey>) -> bool {
|
|
let local_pk = VerifyingKey::from_bytes(&self.read_key_256(KeyRomLocs::SELFSIGN_PUBKEY))
|
|
.expect("public key was not valid");
|
|
let pubkey = if let Some(pk) = maybe_pubkey { pk } else { &local_pk };
|
|
log::debug!("pubkey as reconstituted: {:x?}", pubkey);
|
|
|
|
let kernel_sig_region = self.kernel_sig();
|
|
let sig_region = &kernel_sig_region[..core::mem::size_of::<SignatureInFlash>()];
|
|
let sig_rec: &SignatureInFlash =
|
|
unsafe { (sig_region.as_ptr() as *const SignatureInFlash).as_ref().unwrap() }; // this pointer better not be null, we just created it!
|
|
let sig = Signature::from_bytes(&sig_rec.signature);
|
|
|
|
let kern_len = sig_rec.signed_len as usize;
|
|
log::debug!("recorded kernel len: {} bytes", kern_len);
|
|
log::debug!("verifying with signature {:x?}", sig_rec.signature);
|
|
log::debug!("verifying with pubkey {:x?}", pubkey.to_bytes());
|
|
|
|
let hash = self.kernel_hash();
|
|
match pubkey.verify_prehashed(hash, None, &sig) {
|
|
Ok(()) => true,
|
|
Err(e) => {
|
|
log::error!("error verifying signature: {:?}", e);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn sign_gateware(&self, signing_key: &SigningKey) -> (Signature, u32) {
|
|
let gateware_region = self.gateware();
|
|
|
|
(signing_key.sign(&gateware_region[..SELFSIG_OFFSET]), SELFSIG_OFFSET as u32)
|
|
}
|
|
|
|
/// This is a fast check on the gateware meant to be called on boot just to confirm that we're using a
|
|
/// self-signed gateware
|
|
pub fn verify_gateware_self_signature(&mut self, maybe_pubkey: Option<&VerifyingKey>) -> bool {
|
|
let local_pk = VerifyingKey::from_bytes(&self.read_key_256(KeyRomLocs::SELFSIGN_PUBKEY))
|
|
.expect("public key was not valid");
|
|
let pubkey = if let Some(pk) = maybe_pubkey { pk } else { &local_pk };
|
|
// read the signature directly out of the keyrom
|
|
let gateware_region = self.gateware();
|
|
|
|
let mut sig_region: [u8; core::mem::size_of::<SignatureInFlash>()] =
|
|
[0; core::mem::size_of::<SignatureInFlash>()];
|
|
for (&src, dst) in gateware_region
|
|
[SELFSIG_OFFSET..SELFSIG_OFFSET + core::mem::size_of::<SignatureInFlash>()]
|
|
.iter()
|
|
.zip(sig_region.iter_mut())
|
|
{
|
|
*dst = src;
|
|
}
|
|
let sig_rec: &SignatureInFlash =
|
|
unsafe { (sig_region.as_ptr() as *const SignatureInFlash).as_ref().unwrap() }; // this pointer better not be null, we just created it!
|
|
let sig = Signature::from_bytes(&sig_rec.signature);
|
|
log::debug!("sig_rec ({}): {:x?}", sig_rec.signed_len, sig_rec.signature);
|
|
log::debug!("sig: {:x?}", sig.to_bytes());
|
|
log::debug!("pubkey: {:x?}", pubkey.to_bytes());
|
|
|
|
// note that we use the *prehash* version here, this has a different signature than a straightforward
|
|
// ed25519
|
|
match pubkey.verify_strict(&gateware_region[..SELFSIG_OFFSET], &sig) {
|
|
Ok(_) => {
|
|
log::info!("gateware verified!");
|
|
true
|
|
}
|
|
Err(e) => {
|
|
log::warn!("gateware did not verify! {:?}", e);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This function does a comprehensive check of all the possible signature types in a specified gateware
|
|
/// region
|
|
pub fn check_gateware_signature(&mut self, region_enum: GatewareRegion) -> SignatureResult {
|
|
let mut sig_region: [u8; core::mem::size_of::<SignatureInFlash>()] =
|
|
[0; core::mem::size_of::<SignatureInFlash>()];
|
|
{
|
|
let region = match region_enum {
|
|
GatewareRegion::Boot => self.gateware(),
|
|
GatewareRegion::Staging => self.staging(),
|
|
};
|
|
for (&src, dst) in region
|
|
[SELFSIG_OFFSET..SELFSIG_OFFSET + core::mem::size_of::<SignatureInFlash>()]
|
|
.iter()
|
|
.zip(sig_region.iter_mut())
|
|
{
|
|
*dst = src;
|
|
}
|
|
}
|
|
let sig_rec: &SignatureInFlash =
|
|
unsafe { (sig_region.as_ptr() as *const SignatureInFlash).as_ref().unwrap() };
|
|
let sig = Signature::from_bytes(&sig_rec.signature);
|
|
let mut sigtype = SignatureResult::SelfSignOk;
|
|
// check against all the known signature types in detail
|
|
loop {
|
|
let pubkey_bytes = match sigtype {
|
|
SignatureResult::SelfSignOk => self.read_key_256(KeyRomLocs::SELFSIGN_PUBKEY),
|
|
SignatureResult::ThirdPartyOk => self.read_key_256(KeyRomLocs::THIRDPARTY_PUBKEY),
|
|
SignatureResult::DevKeyOk => self.read_key_256(KeyRomLocs::DEVELOPER_PUBKEY),
|
|
_ => return SignatureResult::InvalidSignatureType,
|
|
};
|
|
// check for uninitialized signature records
|
|
let mut all_zero = true;
|
|
for &b in pubkey_bytes.iter() {
|
|
if b != 0 {
|
|
all_zero = false;
|
|
break;
|
|
}
|
|
}
|
|
if all_zero {
|
|
match sigtype {
|
|
SignatureResult::SelfSignOk => sigtype = SignatureResult::ThirdPartyOk,
|
|
SignatureResult::ThirdPartyOk => sigtype = SignatureResult::DevKeyOk,
|
|
_ => return SignatureResult::Invalid,
|
|
}
|
|
continue;
|
|
}
|
|
let pubkey = match VerifyingKey::from_bytes(&pubkey_bytes) {
|
|
Ok(pubkey) => pubkey,
|
|
Err(_) => return SignatureResult::InvalidPubKey,
|
|
};
|
|
|
|
let region = match region_enum {
|
|
GatewareRegion::Boot => self.gateware(),
|
|
GatewareRegion::Staging => self.staging(),
|
|
};
|
|
match pubkey.verify_strict(®ion[..SELFSIG_OFFSET], &sig) {
|
|
Ok(_) => break,
|
|
Err(_e) => {
|
|
match sigtype {
|
|
SignatureResult::SelfSignOk => sigtype = SignatureResult::ThirdPartyOk,
|
|
SignatureResult::ThirdPartyOk => sigtype = SignatureResult::DevKeyOk,
|
|
_ => return SignatureResult::Invalid,
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
sigtype
|
|
}
|
|
|
|
pub fn fetch_gw_metadata(&self, region_enum: GatewareRegion) -> MetadataInFlash {
|
|
let region = match region_enum {
|
|
GatewareRegion::Boot => self.gateware(),
|
|
GatewareRegion::Staging => self.staging(),
|
|
};
|
|
let md_ptr: *const MetadataInFlash = (®ion
|
|
[METADATA_OFFSET..METADATA_OFFSET + core::mem::size_of::<MetadataInFlash>()])
|
|
.as_ptr() as *const MetadataInFlash;
|
|
|
|
unsafe { *md_ptr }.clone()
|
|
}
|
|
|
|
pub fn commit_signature(
|
|
&self,
|
|
sig: Signature,
|
|
len: u32,
|
|
sig_type: SignatureType,
|
|
) -> Result<(), RootkeyResult> {
|
|
let mut sig_region: [u8; core::mem::size_of::<SignatureInFlash>()] =
|
|
[0; core::mem::size_of::<SignatureInFlash>()];
|
|
// map a structure onto the signature region, so we can do something sane when writing stuff to it
|
|
let signature: &mut SignatureInFlash =
|
|
unsafe { (sig_region.as_mut_ptr() as *mut SignatureInFlash).as_mut().unwrap() }; // this pointer better not be null, we just created it!
|
|
|
|
signature.signed_len = len;
|
|
signature.signature = sig.to_bytes();
|
|
log::debug!("sig: {:x?}", sig.to_bytes());
|
|
log::debug!("signature region to patch: {:x?}", sig_region);
|
|
|
|
match sig_type {
|
|
SignatureType::Loader => {
|
|
signature.version = SIG_LOADER_VERSION;
|
|
log::info!("loader sig area before: {:x?}", &(self.loader_code()[..0x80]));
|
|
self.spinor
|
|
.patch(self.loader_code(), self.loader_base(), &sig_region, 0)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
log::info!("loader sig area after: {:x?}", &(self.loader_code()[..0x80]));
|
|
}
|
|
SignatureType::Kernel => {
|
|
signature.version = SIG_KERNEL_VERSION;
|
|
log::info!("kernel sig area before: {:x?}", &(self.kernel_sig()[..0x80]));
|
|
self.spinor
|
|
.patch(self.kernel_sig(), self.kernel_sig_base(), &sig_region, 0)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
log::info!("kernel sig area after: {:x?}", &(self.kernel_sig()[..0x80]));
|
|
}
|
|
SignatureType::Gateware => {
|
|
signature.version = SIG_LOADER_VERSION;
|
|
log::info!(
|
|
"gateware sig area before: {:x?}",
|
|
&(self.gateware()
|
|
[SELFSIG_OFFSET..SELFSIG_OFFSET + core::mem::size_of::<SignatureInFlash>()])
|
|
);
|
|
self.spinor
|
|
.patch(self.gateware(), self.gateware_base(), &sig_region, (SELFSIG_OFFSET) as u32)
|
|
.map_err(|_| RootkeyResult::FlashError)?;
|
|
log::info!(
|
|
"gateware sig area after: {:x?}",
|
|
&(self.gateware()
|
|
[SELFSIG_OFFSET..SELFSIG_OFFSET + core::mem::size_of::<SignatureInFlash>()])
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Called by the UX layer at the epilogue of the initialization run. Allows suspend/resume to resume,
|
|
/// and zero-izes any sensitive data that was created in the process.
|
|
pub fn finish_key_init(&mut self) {
|
|
// purge the password cache, if the policy calls for it
|
|
match self.boot_password_policy {
|
|
PasswordRetentionPolicy::AlwaysPurge => {
|
|
self.purge_password(PasswordType::Boot);
|
|
}
|
|
_ => (),
|
|
}
|
|
match self.update_password_policy {
|
|
PasswordRetentionPolicy::AlwaysPurge => {
|
|
self.purge_password(PasswordType::Update);
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
// now purge the keyrom copy and other temporaries
|
|
self.purge_sensitive_data();
|
|
// reset this flag to false in case this was called at the end of a restore op; harmless if it's
|
|
// already false.
|
|
self.restore_running = false;
|
|
|
|
// re-allow suspend/resume ops
|
|
self.susres.set_suspendable(true).expect("couldn't re-allow suspend/resume");
|
|
}
|
|
|
|
pub fn staged_semver(&self) -> SemVer {
|
|
let staging_meta = self.fetch_gw_metadata(GatewareRegion::Staging);
|
|
if staging_meta.magic == 0x6174656d {
|
|
let tag_str =
|
|
std::str::from_utf8(&staging_meta.tag_str[..(staging_meta.tag_len as usize).min(64)])
|
|
.unwrap_or("v0.0.0-1");
|
|
SemVer::from_str(tag_str).unwrap_or(SemVer { maj: 0, min: 0, rev: 0, extra: 2, commit: None })
|
|
} else if staging_meta.magic == 0xFFFF_FFFF {
|
|
log::debug!("metadata blank");
|
|
SemVer { maj: 0xFFFF, min: 0xFFFF, rev: 0xFFFF, extra: 0xFFFF, commit: None }
|
|
} else {
|
|
log::debug!("metadata corrupted: {:?}", staging_meta);
|
|
SemVer { maj: 0, min: 0, rev: 0, extra: 3, commit: None }
|
|
}
|
|
}
|
|
|
|
/// Attempt to apply an update with the following assumptions:
|
|
/// 1. No root keys exist.
|
|
/// 2. The staged update is newer than the current update.
|
|
///
|
|
/// If any of these assumptions fail, return false. Do not crash or panic.
|
|
///
|
|
/// The main utility of this function is to simplify the out of the box experience
|
|
/// before root keys have been added.
|
|
pub fn try_nokey_soc_update(&mut self, rootkeys_modal: &mut Modal, main_cid: xous::CID) -> bool {
|
|
if self.is_initialized() {
|
|
log::warn!("No-touch update attempted, but keys are initialized. Aborting.");
|
|
return false;
|
|
}
|
|
let staged_sv = self.staged_semver();
|
|
|
|
// the semver check *should* be done already, but we check the gateware as written
|
|
// to FLASH and not what's reported from the `llio`. The condition at which these are
|
|
// inconsistent is someone has applied the update but did not do the "cold boot" to
|
|
// force a reload of the SoC. In this case, do not keep applying the same update over
|
|
// and over again. We also do this because we skip all UX interaction in this flow,
|
|
// unlike the UxBlindUpdate call, which has an explicit approval step in it.
|
|
let soc_meta = self.fetch_gw_metadata(GatewareRegion::Boot);
|
|
let tag_str =
|
|
std::str::from_utf8(&soc_meta.tag_str[..soc_meta.tag_len as usize]).unwrap_or("v0.0.0-1");
|
|
let soc_sv =
|
|
SemVer::from_str(tag_str).unwrap_or(SemVer { maj: 0, min: 0, rev: 0, extra: 0, commit: None });
|
|
if staged_sv > soc_sv {
|
|
let mut progress_action = Slider::new(
|
|
main_cid,
|
|
Opcode::UxGutter.to_u32().unwrap(),
|
|
0,
|
|
100,
|
|
10,
|
|
Some("%"),
|
|
0,
|
|
true,
|
|
true,
|
|
);
|
|
progress_action.set_is_password(true);
|
|
rootkeys_modal.modify(
|
|
Some(ActionType::Slider(progress_action)),
|
|
Some(t!("rootkeys.gwup_starting", locales::LANG)),
|
|
false,
|
|
None,
|
|
true,
|
|
None,
|
|
);
|
|
rootkeys_modal.activate();
|
|
xous::yield_slice(); // give some time to the GAM to render
|
|
let mut pb = ProgressBar::new(rootkeys_modal, &mut progress_action);
|
|
pb.set_percentage(1);
|
|
self.ticktimer.sleep_ms(250).expect("couldn't show final message");
|
|
let ret = self.make_gateware_backup(Some(&mut pb), true).is_ok();
|
|
pb.set_percentage(100);
|
|
self.ticktimer.sleep_ms(250).expect("couldn't show final message");
|
|
// the stop emoji, when sent to the slider action bar in progress mode, will cause it to close and
|
|
// relinquish focus
|
|
rootkeys_modal.key_event(['🛑', '\u{0000}', '\u{0000}', '\u{0000}']);
|
|
ret
|
|
} else {
|
|
log::warn!(
|
|
"No-touch update attempted, but the staged version is not newer than the existing version. Aborting."
|
|
);
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn should_prompt_for_update(&mut self) -> bool {
|
|
let soc_region = self.staging();
|
|
let status = u32::from_le_bytes(soc_region[soc_region.len() - 4..].try_into().unwrap());
|
|
log::info!("prompt for update: {:x?}", status);
|
|
if status == 0xFFFF_FFFF { true } else { false }
|
|
}
|
|
|
|
/// the update prompt is reset every time you stage a new update
|
|
pub fn set_prompt_for_update(&mut self, state: bool) {
|
|
let patch_data = if state { [0xffu8; 4] } else { [0x0u8; 4] };
|
|
self.spinor
|
|
.patch(self.staging(), self.staging_base(), &patch_data, self.staging().len() as u32 - 4)
|
|
.expect("couldn't patch update prompt");
|
|
}
|
|
|
|
pub fn is_dont_ask_init_set(&mut self) -> bool {
|
|
let soc_region = self.gateware();
|
|
let status = u32::from_le_bytes(soc_region[soc_region.len() - 4..].try_into().unwrap());
|
|
log::info!("prompt for root key init: {:x?}", status);
|
|
// just check the first byte, although a full word is written for the flag
|
|
status != 0xffff_ffff
|
|
}
|
|
|
|
/// this is reset every time the gateware is updated. That's rather intentional, if someone
|
|
/// *is* updating their gateware and they haven't initialized root keys...maybe they should?
|
|
pub fn set_dont_ask_init(&mut self) {
|
|
self.spinor
|
|
.patch(self.gateware(), self.gateware_base(), &[0u8; 4], self.gateware().len() as u32 - 4)
|
|
.expect("couldn't erase backup region");
|
|
}
|
|
|
|
pub fn reset_dont_ask_init(&mut self) {
|
|
self.spinor
|
|
.patch(self.gateware(), self.gateware_base(), &[0xffu8; 4], self.gateware().len() as u32 - 4)
|
|
.expect("couldn't erase backup region");
|
|
}
|
|
|
|
pub fn read_backup_header(&mut self) -> Option<BackupHeader> {
|
|
let region = self.kernel_backup();
|
|
let backup = ®ion[..size_of::<BackupHeader>()];
|
|
let mut header = BackupHeader::default();
|
|
header.as_mut().copy_from_slice(backup);
|
|
if (header.version & BACKUP_VERSION_MASK) == (BACKUP_VERSION & BACKUP_VERSION_MASK) {
|
|
Some(header)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn get_backup_key(&mut self) -> Option<(backups::BackupKey, backups::KeyRomExport)> {
|
|
// make sure the system is sane
|
|
self.xous_init_interlock();
|
|
|
|
// derive signing key
|
|
let pcache: &mut PasswordCache =
|
|
unsafe { &mut *(self.pass_cache.as_mut_ptr() as *mut PasswordCache) };
|
|
if pcache.hashed_update_pw_valid == 0 {
|
|
self.purge_password(PasswordType::Update);
|
|
log::error!("no password was set going into the update routine");
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("key: {:x?}", pcache.hashed_update_pw);
|
|
log::debug!("valid: {}", pcache.hashed_update_pw_valid);
|
|
|
|
return None;
|
|
}
|
|
let mut keypair_bytes: [u8; ed25519_dalek::KEYPAIR_LENGTH] = [0; ed25519_dalek::KEYPAIR_LENGTH];
|
|
let enc_signing_key = self.read_key_256(KeyRomLocs::SELFSIGN_PRIVKEY);
|
|
for (key, (&enc_key, &pw)) in keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
|
|
.iter_mut()
|
|
.zip(enc_signing_key.iter().zip(pcache.hashed_update_pw.iter()))
|
|
{
|
|
*key = enc_key ^ pw;
|
|
}
|
|
self.compute_key_rollback(&mut keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]);
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!(
|
|
"keypair privkey (after anti-rollback): {:x?}",
|
|
&keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
|
|
);
|
|
for (key, &src) in keypair_bytes[ed25519_dalek::SECRET_KEY_LENGTH..]
|
|
.iter_mut()
|
|
.zip(self.read_key_256(KeyRomLocs::SELFSIGN_PUBKEY).iter())
|
|
{
|
|
*key = src;
|
|
}
|
|
// Keypair zeroizes the secret key on drop.
|
|
let keypair = SigningKey::from_keypair_bytes(&keypair_bytes).ok()?;
|
|
#[cfg(feature = "hazardous-debug")]
|
|
log::debug!("keypair privkey (after anti-rollback + conversion): {:x?}", keypair.secret.to_bytes());
|
|
|
|
// check if the keypair is valid by signing and verifying a short message
|
|
let test_data = "whiskey made me do it";
|
|
let test_sig = keypair.sign(test_data.as_bytes());
|
|
match keypair.verify(&test_data.as_bytes(), &test_sig) {
|
|
Ok(_) => {
|
|
for (&src, dst) in
|
|
self.read_key_256(KeyRomLocs::FPGA_KEY).iter().zip(pcache.fpga_key.iter_mut())
|
|
{
|
|
*dst = src;
|
|
}
|
|
log::debug!("fpga key (encrypted): {:x?}", &pcache.fpga_key);
|
|
for (fkey, &pw) in pcache.fpga_key.iter_mut().zip(pcache.hashed_update_pw.iter()) {
|
|
*fkey = *fkey ^ pw;
|
|
}
|
|
pcache.fpga_key_valid = 1;
|
|
// copy the plaintext FPGA key into the backup key structure, which implements the zeroize
|
|
// trait.
|
|
let mut bkey = backups::BackupKey::default();
|
|
bkey.0.copy_from_slice(&pcache.fpga_key);
|
|
// copy the key rom into the backup keyrom structure, which implements the zeroize trait.
|
|
let mut backup_rom = backups::KeyRomExport::default();
|
|
for i in 0..256 {
|
|
self.keyrom.wfo(utra::keyrom::ADDRESS_ADDRESS, i);
|
|
backup_rom.0[i as usize] = self.keyrom.rf(utra::keyrom::DATA_DATA);
|
|
}
|
|
// we're done with the password now, clear all the temps
|
|
self.purge_password(PasswordType::Update);
|
|
for b in keypair_bytes.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
self.purge_sensitive_data();
|
|
Some((bkey, backup_rom))
|
|
}
|
|
Err(e) => {
|
|
log::warn!("update password was not connect ({:?})", e);
|
|
self.purge_password(PasswordType::Update);
|
|
for b in keypair_bytes.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn write_backup(
|
|
&mut self,
|
|
mut header: BackupHeader,
|
|
backup_ct: backups::BackupDataCt,
|
|
checksums: Option<Checksums>,
|
|
) -> Result<(), xous::Error> {
|
|
header.op = BackupOp::Backup; // set the "we're backing up" flag
|
|
|
|
// condense the data into a single block, to reduce read/write cycles on the block
|
|
const HASH_LEN: usize = 32;
|
|
let mut block = [0u8; size_of::<BackupHeader>() + size_of::<backups::BackupDataCt>() + HASH_LEN];
|
|
block[..size_of::<BackupHeader>()].copy_from_slice(header.as_ref());
|
|
block[size_of::<BackupHeader>()..size_of::<BackupHeader>() + size_of::<backups::BackupDataCt>()]
|
|
.copy_from_slice(backup_ct.as_ref());
|
|
|
|
// compute a hash of the data, for fast verification of the backup header integrity to detect media
|
|
// errors, etc.
|
|
let mut hasher = Sha512_256Sw::new();
|
|
hasher.update(&block[..size_of::<BackupHeader>() + size_of::<backups::BackupDataCt>()]);
|
|
let digest = hasher.finalize();
|
|
assert!(digest.len() == HASH_LEN, "Wrong length hash selected! Check your code.");
|
|
block[size_of::<BackupHeader>() + size_of::<backups::BackupDataCt>()..]
|
|
.copy_from_slice(digest.as_slice());
|
|
|
|
// stage the artifacts into a full page header
|
|
const PAGE_LEN: usize = 4096;
|
|
let mut page = [0xFFu8; PAGE_LEN];
|
|
// place the metadata at the top of the block
|
|
page[..block.len()].copy_from_slice(&block);
|
|
if let Some(cs) = checksums {
|
|
use std::ops::Deref;
|
|
let cs_slice = cs.deref();
|
|
// stage the checksums, if any, aligned to the end of the block
|
|
assert!(
|
|
block.len() + cs_slice.len() < PAGE_LEN,
|
|
"Error: checksum block has outgrown the available space"
|
|
);
|
|
page[PAGE_LEN - cs_slice.len()..].copy_from_slice(cs_slice);
|
|
}
|
|
self.spinor
|
|
.patch(self.kernel_backup(), self.kernel_backup_base(), &page, 0)
|
|
.map_err(|_| xous::Error::InternalError)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn write_restore_dna(
|
|
&mut self,
|
|
mut header: BackupHeader,
|
|
backup_ct: backups::BackupDataCt,
|
|
) -> Result<(), xous::Error> {
|
|
header.op = BackupOp::RestoreDna; // set the "restore DNA" flag
|
|
|
|
// condense the data into a single block, to reduce read/write cycles on the block
|
|
let mut block = [0u8; size_of::<BackupHeader>() + size_of::<backups::BackupDataCt>()];
|
|
block[..size_of::<BackupHeader>()].copy_from_slice(header.as_ref());
|
|
block[size_of::<BackupHeader>()..].copy_from_slice(backup_ct.as_ref());
|
|
self.spinor
|
|
.patch(self.kernel_backup(), self.kernel_backup_base(), &block, 0)
|
|
.map_err(|_| xous::Error::InternalError)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn read_backup(&mut self) -> Result<(BackupHeader, backups::BackupDataCt), xous::Error> {
|
|
let mut header = BackupHeader::default();
|
|
let mut ct = backups::BackupDataCt::default();
|
|
header.as_mut().copy_from_slice(&self.kernel_backup()[..size_of::<BackupHeader>()]);
|
|
ct.as_mut().copy_from_slice(
|
|
&self.kernel_backup()
|
|
[size_of::<BackupHeader>()..size_of::<BackupHeader>() + size_of::<backups::BackupDataCt>()],
|
|
);
|
|
Ok((header, ct))
|
|
}
|
|
|
|
pub fn erase_backup(&mut self) {
|
|
let blank = [0xffu8; size_of::<BackupHeader>() + size_of::<backups::BackupDataCt>()];
|
|
self.spinor
|
|
.patch(self.kernel_backup(), self.kernel_backup_base(), &blank, 0)
|
|
.expect("couldn't erase backup region");
|
|
}
|
|
}
|