mirror of
https://github.com/saymrwulf/xous-core.git
synced 2026-11-10 06:36:32 +00:00
492 lines
17 KiB
Rust
492 lines
17 KiB
Rust
mod rkyv_enum;
|
|
pub use rkyv_enum::*;
|
|
|
|
use core::ops::{Deref, DerefMut};
|
|
use core::mem::size_of;
|
|
|
|
pub(crate) const SERVER_NAME_KEYS: &str = "_Root key server and update manager_";
|
|
#[allow(dead_code)]
|
|
pub(crate) const SIG_VERSION: u32 = 1;
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Debug)]
|
|
pub(crate) enum Opcode {
|
|
/// use to check if we've been initialized
|
|
KeysInitialized = 0,
|
|
/// check that the digital signature on the gateware
|
|
CheckGatewareSignature = 1,
|
|
/// check if the efuse has been locked down
|
|
IsEfuseSecured = 2,
|
|
/// quick check to see if the JTAG can read its IDCODE
|
|
IsJtagWorking = 3,
|
|
/// initiate an AES oracle operation
|
|
AesOracle = 4,
|
|
/// initiate key wrapper operation
|
|
AesKwp = 5,
|
|
/// create new FPGA keys; provisioning requires a slave device to be connected that can run the JTAG sequence
|
|
BbramProvision = 6,
|
|
/// clear a cached password
|
|
ClearPasswordCacheEntry = 7,
|
|
|
|
TestUx = 8,
|
|
|
|
/// attempt to initialize keys on a brand new system. Does nothing if the keys are already provisioned.
|
|
UxTryInitKeys = 9,
|
|
UxInitUpdateFirstPasswordReturn = 10,
|
|
UxInitUpdatePasswordReturn = 11,
|
|
|
|
/// provision a gateware update with our secret data
|
|
UxUpdateGateware = 12,
|
|
UxUpdateGwPasswordReturn = 13,
|
|
UxUpdateGwRun = 14,
|
|
|
|
/// self-sign kernel/loader
|
|
UxSelfSignXous = 15,
|
|
UxSignXousPasswordReturn = 16,
|
|
UxSignXousRun = 17,
|
|
|
|
/// Ux AES calls
|
|
UxAesEnsurePassword = 18,
|
|
UxAesPasswordPolicy = 19,
|
|
UxAesEnsureReturn = 20,
|
|
|
|
/// Ux BBRAM flow
|
|
UxBbramCheckReturn = 21,
|
|
UxBbramPasswordReturn = 22,
|
|
UxBbramRun = 23,
|
|
|
|
// General Ux calls
|
|
UxGutter = 24, // NOP for UX calls that require a destination
|
|
#[cfg(feature = "policy-menu")]
|
|
UxGetPolicy = 25,
|
|
#[cfg(feature = "policy-menu")]
|
|
UxPolicyReturn = 26,
|
|
UxTryReboot = 27,
|
|
UxDoReboot = 28,
|
|
|
|
/// UX opcodes
|
|
ModalRedraw = 29,
|
|
ModalKeys = 30,
|
|
ModalDrop = 31,
|
|
|
|
/// Suspend/resume callback
|
|
SuspendResume = 32,
|
|
|
|
Quit = 33,
|
|
|
|
/// makes a "blind copy" of the staged gateware. This will destroy root keys,
|
|
/// but the call will not succeed if the image was provisioned. This has
|
|
/// an explicit user approval step.
|
|
UxBlindCopy = 34,
|
|
/// report the semver of the staged gateware
|
|
StagedSemver = 35,
|
|
/// A zero-touch version of UxBlindCopy.
|
|
/// Run an update of a staged gateware that is newer. Will silently fail if
|
|
/// root keys exist, or if the staged gateware is invalid.
|
|
TryNoKeySocUpdate = 36,
|
|
/// Query if the "don't bother me again for an update" flag is set
|
|
ShouldPromptForUpdate = 37,
|
|
/// Set the "don't bother me again for an update"
|
|
SetPromptForUpdate = 38,
|
|
/// Create a backup block (including UX flow that discloses our AES key!)
|
|
CreateBackup = 39,
|
|
UxCreateBackupPwReturn = 40,
|
|
/// Query if a backup exists to be restored
|
|
ShouldRestore = 41,
|
|
/// Perform the restore operation (including UX to acquire the AES key)
|
|
DoRestore = 42,
|
|
UxDoRestorePwReturn = 43,
|
|
/// A check to see if the zero-key was used on boot
|
|
IsZeroKey = 44,
|
|
/// Erase the backup block
|
|
EraseBackupBlock = 45,
|
|
/// Checks to see if "don't ask me about updates" is set
|
|
IsDontAskSet = 46,
|
|
/// Resets the dont ask bit. Mainly for use by the OQC testing routine
|
|
ResetDontAsk = 47,
|
|
|
|
/// Initiates the efuse burn process. Any user abort for the flow must be done
|
|
/// prior to this call; the flow within rootkeys is intended to be non-abortable.
|
|
#[cfg(feature = "efuse")]
|
|
BurnEfuse = 48,
|
|
#[cfg(feature = "efuse")]
|
|
EfuseRun = 49,
|
|
#[cfg(feature = "efuse")]
|
|
EfusePasswordReturn = 50,
|
|
}
|
|
|
|
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive, PartialEq, Eq)]
|
|
pub enum PasswordRetentionPolicy {
|
|
AlwaysKeep = 0,
|
|
EraseOnSuspend = 1,
|
|
AlwaysPurge = 2,
|
|
}
|
|
|
|
/// Enumerate the possible password types dealt with by this manager.
|
|
/// Note that the discriminant of the enum is used to every-so-slightly change the salt going into bcrypt
|
|
/// I don't think it hurts; more importantly, it also prevents an off-the-shelf "hashcat" run from
|
|
/// being used to brute force both passwords in a single go, as the salt has to be (slightly)
|
|
/// recomputed for each type of password.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum PasswordType {
|
|
Boot = 1,
|
|
Update = 2,
|
|
}
|
|
#[cfg_attr(not(any(feature="precursor", feature="renode")), allow(dead_code))]
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum RootkeyResult {
|
|
AlignmentError = 0,
|
|
KeyError = 1,
|
|
IntegrityError = 2,
|
|
FlashError = 3,
|
|
/// enclave is in the wrong state to do the requested operation
|
|
StateError = 4,
|
|
}
|
|
|
|
/// AES operation definitions
|
|
pub use cipher::{BlockCipher, consts::U16};
|
|
use keyboard::KeyMap;
|
|
use zeroize::Zeroize;
|
|
|
|
/// 128-bit AES block
|
|
#[allow(dead_code)]
|
|
pub type Block = cipher::generic_array::GenericArray<u8, cipher::consts::U16>;
|
|
/// 16 x 128-bit AES blocks to be processed in bulk
|
|
#[allow(dead_code)]
|
|
pub type ParBlocks = cipher::generic_array::GenericArray<Block, cipher::consts::U16>;
|
|
|
|
pub const PAR_BLOCKS: usize = 16;
|
|
/// Selects which key to use for the decryption/encryption oracle.
|
|
/// currently only one type is available, the User key, but dozens more
|
|
/// could be accommodated.
|
|
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive, PartialEq, Eq, Copy, Clone)]
|
|
pub enum AesRootkeyType {
|
|
User0 = 0x28,
|
|
NoneSpecified = 0xff,
|
|
}
|
|
|
|
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Zeroize)]
|
|
#[zeroize(drop)]
|
|
pub enum AesOpType {
|
|
Encrypt = 0,
|
|
Decrypt = 1,
|
|
}
|
|
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Zeroize)]
|
|
#[zeroize(drop)]
|
|
pub struct AesOp {
|
|
/// the caller can try to request "any" index, but it's checked inside the oracle first.
|
|
pub key_index: u8,
|
|
pub block: AesBlockType,
|
|
pub aes_op: AesOpType,
|
|
}
|
|
impl AesOp {
|
|
pub fn clear(&mut self) {
|
|
match self.block {
|
|
AesBlockType::SingleBlock(mut blk) => {
|
|
for b in blk.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
}
|
|
AesBlockType::ParBlock(mut blks) => {
|
|
for blk in blks.iter_mut() {
|
|
for b in blk.iter_mut() {
|
|
*b = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Eq, PartialEq)]
|
|
pub enum KeyWrapOp {
|
|
Wrap = 0,
|
|
Unwrap = 1,
|
|
}
|
|
|
|
use std::error::Error;
|
|
impl Error for KeywrapError {}
|
|
|
|
use std::fmt;
|
|
|
|
impl fmt::Display for KeywrapError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
|
match self {
|
|
KeywrapError::InvalidDataSize => f.write_str("Invalid data size"),
|
|
KeywrapError::InvalidKekSize => f.write_str("Invalid key size"),
|
|
KeywrapError::InvalidOutputSize => f.write_str("Invalid output size"),
|
|
KeywrapError::IntegrityCheckFailed => f.write_str("Authentication failed"),
|
|
KeywrapError::UpgradeToNew((_k, _wk)) => f.write_str("Legacy migration detected! New wrapped key transmitted to caller"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) const MAX_WRAP_DATA: usize = 2048;
|
|
/// Note regression in v0.9.9: we had to return an array type in the KeywrapError enum that
|
|
/// has a signature for an array that is 40 bytes long, which is bigger than Rust's devire
|
|
/// can deal with. So, unfortunately, the result of this does *not* get zeroized on drop :(
|
|
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
|
// #[zeroize(drop)]
|
|
pub (crate) struct KeyWrapper {
|
|
pub data: [u8; MAX_WRAP_DATA + 8],
|
|
// used to specify the length of the data used in the fixed-length array above
|
|
pub len: u32,
|
|
pub key_index: u8,
|
|
pub op: KeyWrapOp,
|
|
pub result: Option<KeywrapError>,
|
|
// used by the unwrap side
|
|
pub expected_len: u32,
|
|
}
|
|
|
|
// the BackupHeader type is serialized into u8 before going through rkyv.
|
|
// a bit inefficient but convenient, because we need an Option<> of the
|
|
// BackupHeader and not the header itself.
|
|
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
|
pub (crate) struct BackupHeaderIpc {
|
|
pub data: Option<[u8; core::mem::size_of::<BackupHeader>()]>,
|
|
pub checksums: Option<Checksums>,
|
|
|
|
}
|
|
impl Default for BackupHeaderIpc {
|
|
fn default() -> Self {
|
|
BackupHeaderIpc {
|
|
data: None::<[u8; core::mem::size_of::<BackupHeader>()]>,
|
|
checksums: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const BACKUP_VERSION: u32 = 0x00_01_00_01;
|
|
#[cfg(any(feature="precursor", feature="renode"))]
|
|
pub const BACKUP_VERSION_MASK: u32 = 0xFF_FF_00_00; // mask off bits that are cross-compatible
|
|
|
|
#[repr(u32)]
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
#[allow(dead_code)]
|
|
pub enum BackupOp {
|
|
/// This is the value that's kept inside the BackupDataPt
|
|
Archive = 0,
|
|
/// backup and restore can be manipulated by the OS without updating the ciphertext
|
|
Backup = 1,
|
|
Restore = 2,
|
|
RestoreDna = 3,
|
|
}
|
|
|
|
#[repr(u32)]
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
#[allow(dead_code)]
|
|
pub enum BackupLanguage {
|
|
En = 0,
|
|
EnTts = 1,
|
|
Ja = 2,
|
|
Zh = 3,
|
|
}
|
|
impl Default for BackupLanguage {
|
|
fn default() -> Self {
|
|
match xous::LANG {
|
|
"en" => BackupLanguage::En,
|
|
"en-tts" => BackupLanguage::EnTts,
|
|
"ja" => BackupLanguage::Ja,
|
|
"zh" => BackupLanguage::Zh,
|
|
_ => BackupLanguage::En,
|
|
}
|
|
}
|
|
}
|
|
impl From::<BackupLanguage> for [u8; 4] {
|
|
fn from(l: BackupLanguage) -> [u8; 4] {
|
|
(l as u32).to_le_bytes()
|
|
}
|
|
}
|
|
#[repr(u32)]
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
#[allow(dead_code)]
|
|
/// We keep a separate version of this for serialization/deserialization because
|
|
/// we need to handle "unknown/undefined" layouts in a different way from the keyboard
|
|
/// crate. Plus this helps avoid a dependency on the keyboard crate as well.
|
|
pub enum BackupKeyboardLayout {
|
|
Qwerty = 0,
|
|
Dvorak = 1,
|
|
Qwertz = 2,
|
|
Azerty = 3,
|
|
Braille = 4,
|
|
Hangul = 5,
|
|
// codes above 16384 are reserved for user layouts
|
|
}
|
|
impl From::<KeyMap> for BackupKeyboardLayout {
|
|
fn from(map: KeyMap) -> BackupKeyboardLayout {
|
|
match map {
|
|
KeyMap::Qwerty => BackupKeyboardLayout::Qwerty,
|
|
KeyMap::Azerty => BackupKeyboardLayout::Azerty,
|
|
KeyMap::Dvorak => BackupKeyboardLayout::Dvorak,
|
|
KeyMap::Qwertz => BackupKeyboardLayout::Qwertz,
|
|
KeyMap::Braille => BackupKeyboardLayout::Braille,
|
|
KeyMap::Undefined => BackupKeyboardLayout::Qwerty,
|
|
}
|
|
}
|
|
}
|
|
impl From<BackupKeyboardLayout> for KeyMap {
|
|
fn from(layout: BackupKeyboardLayout) -> KeyMap {
|
|
match layout {
|
|
BackupKeyboardLayout::Qwerty => KeyMap::Qwerty,
|
|
BackupKeyboardLayout::Braille => KeyMap::Braille,
|
|
BackupKeyboardLayout::Dvorak => KeyMap::Dvorak,
|
|
BackupKeyboardLayout::Qwertz => KeyMap::Qwertz,
|
|
BackupKeyboardLayout::Azerty => KeyMap::Azerty,
|
|
BackupKeyboardLayout::Hangul => KeyMap::Undefined,
|
|
}
|
|
}
|
|
}
|
|
impl Default for BackupKeyboardLayout {
|
|
fn default() -> Self {
|
|
BackupKeyboardLayout::Qwerty
|
|
}
|
|
}
|
|
impl From::<BackupKeyboardLayout> for [u8; 4] {
|
|
fn from(l: BackupKeyboardLayout) -> [u8; 4] {
|
|
(l as u32).to_le_bytes()
|
|
}
|
|
}
|
|
impl From::<[u8; 4]> for BackupKeyboardLayout {
|
|
fn from(b: [u8; 4]) -> BackupKeyboardLayout {
|
|
let code = u32::from_le_bytes(b);
|
|
match code {
|
|
0 => BackupKeyboardLayout::Qwerty,
|
|
1 => BackupKeyboardLayout::Dvorak,
|
|
2 => BackupKeyboardLayout::Qwertz,
|
|
3 => BackupKeyboardLayout::Azerty,
|
|
4 => BackupKeyboardLayout::Braille,
|
|
5 => BackupKeyboardLayout::Hangul,
|
|
_ => BackupKeyboardLayout::Qwerty,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const HEADER_TOTAL_SIZE: u32 = 4096;
|
|
/// Size of a checksummed block in pages. 0x100 = 256 pages,
|
|
/// or 1 MiB for a checksummed block. This is specified in 4kiB pages
|
|
/// because it really doesn't make sense to checksum anything smaller
|
|
/// than that, and it allows us to grow the size of a single checksummed
|
|
/// block to well over 4GiB.
|
|
pub const CHECKSUM_BLOCKLEN_PAGE: u32 = 0x100;
|
|
/// The total number of checksums required to cover the length of the PDDB
|
|
/// divided by the length of a checksummed page. This number does not count
|
|
/// the single additional checksum that is included to check the integrity of the
|
|
/// plaintext + ciphertext header itself.
|
|
///
|
|
/// Total number of checksums has to divide evenly into the size of the PDDB region
|
|
pub const TOTAL_CHECKSUMS: u32 = xous::PDDB_LEN / (CHECKSUM_BLOCKLEN_PAGE * 4096);
|
|
|
|
#[repr(C, align(8))]
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct BackupHeader {
|
|
pub version: u32,
|
|
// the `ver`s are all serialized SemVers. To be done by the caller.
|
|
pub xous_ver: [u8; 16],
|
|
pub soc_ver: [u8; 16],
|
|
pub ec_ver: [u8; 16],
|
|
pub wf200_ver: [u8; 16],
|
|
pub timestamp: u64,
|
|
pub language: [u8; 4],
|
|
pub kbd_layout: [u8; 4],
|
|
pub dna: [u8; 8],
|
|
/// Length of a checksummed region, in 4kiB pages
|
|
pub checksum_len_page: [u8; 4],
|
|
/// Number of checksummed pages
|
|
///
|
|
/// The location of the first header is computed as follows:
|
|
/// `header_start_address + header_total_size - (total_checksums) * 16`
|
|
/// The backup data starts at `header_start_address + header_total_size`
|
|
///
|
|
/// In other words, checksums are aligned to the highest address in the header
|
|
/// while the header plaintext+ciphertext data is aligned to the lowest address
|
|
/// in the header region.
|
|
pub total_checksums: [u8; 4],
|
|
/// Total size of the header in bytes, including unused space
|
|
pub header_total_size: [u8; 4],
|
|
pub _reserved: [u8; 36],
|
|
pub op: BackupOp,
|
|
}
|
|
impl Default for BackupHeader {
|
|
fn default() -> Self {
|
|
assert!(xous::PDDB_LEN & ((CHECKSUM_BLOCKLEN_PAGE * 0x1000) - 1) == 0, "PDDB_LEN is not an integer multiple of CHECKSUM_LEN_PAGE");
|
|
BackupHeader {
|
|
version: BACKUP_VERSION,
|
|
xous_ver: [0u8; 16],
|
|
soc_ver: [0u8; 16],
|
|
ec_ver: [0u8; 16],
|
|
wf200_ver: [0u8; 16],
|
|
timestamp: 0,
|
|
language: BackupLanguage::default().into(), // this is "correct by default"
|
|
kbd_layout: BackupKeyboardLayout::default().into(), // this has to be adjusted
|
|
dna: [0u8; 8],
|
|
checksum_len_page: CHECKSUM_BLOCKLEN_PAGE.to_le_bytes(),
|
|
total_checksums: TOTAL_CHECKSUMS.to_le_bytes(),
|
|
header_total_size: HEADER_TOTAL_SIZE.to_le_bytes(),
|
|
_reserved: [0u8; 36],
|
|
op: BackupOp::Archive,
|
|
}
|
|
}
|
|
}
|
|
impl Deref for BackupHeader {
|
|
type Target = [u8];
|
|
fn deref(&self) -> &[u8] {
|
|
unsafe {
|
|
core::slice::from_raw_parts(self as *const BackupHeader as *const u8, size_of::<BackupHeader>())
|
|
as &[u8]
|
|
}
|
|
}
|
|
}
|
|
impl DerefMut for BackupHeader {
|
|
fn deref_mut(&mut self) -> &mut [u8] {
|
|
unsafe {
|
|
core::slice::from_raw_parts_mut(self as *mut BackupHeader as *mut u8, size_of::<BackupHeader>())
|
|
as &mut [u8]
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The Checksums structure is an array of 16-byte (128-bit) checksums that
|
|
/// are applied to backed up data. There is one checksum per block region
|
|
/// (currently set to 1MiB).
|
|
///
|
|
/// The actual checksum is a SHA512 of the region, but with only the first
|
|
/// 128 bits stored. The goal of the checksum isn't a cryptographic tamper
|
|
/// proofing -- this is already handled by the underlying AEAD's that are
|
|
/// applied to the PDDB. The utility of the checksum is to detect media
|
|
/// or transmission errors in storage, without having to fully decrypt
|
|
/// the entire PDDB to look for corruption. We use SHA512 and truncate it to
|
|
/// 128 bits not because it's the optimal hash, but because it's what we
|
|
/// have a hardware accelerator for. We don't truncate to 256 bits because
|
|
/// we're trying to pack as many checksums into a 4k header, and going to
|
|
/// 128 bits is still "strong" for a checksum and allows us to get the region
|
|
/// size down to 1MiB, which is a reasonably sized region for a retry-download
|
|
/// in case a checksum error is found.
|
|
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
|
#[repr(C, align(8))]
|
|
pub struct Checksums {
|
|
pub checksums: [[u8; 16]; crate::api::TOTAL_CHECKSUMS as usize],
|
|
}
|
|
impl Default for Checksums {
|
|
fn default() -> Self {
|
|
Checksums {
|
|
checksums: [[0u8; 16]; crate::api::TOTAL_CHECKSUMS as usize],
|
|
}
|
|
}
|
|
}
|
|
impl Deref for Checksums {
|
|
type Target = [u8];
|
|
fn deref(&self) -> &[u8] {
|
|
unsafe {
|
|
core::slice::from_raw_parts(self as *const Checksums as *const u8, size_of::<Checksums>())
|
|
as &[u8]
|
|
}
|
|
}
|
|
}
|
|
impl DerefMut for Checksums {
|
|
fn deref_mut(&mut self) -> &mut [u8] {
|
|
unsafe {
|
|
core::slice::from_raw_parts_mut(self as *mut Checksums as *mut u8, size_of::<Checksums>())
|
|
as &mut [u8]
|
|
}
|
|
}
|
|
}
|