mirror of
https://github.com/BillyOutlast/oboromi.git
synced 2026-07-01 19:54:43 -04:00
fix(rsa): correct TEST_PRIVATE_EXP_D byte 121 (F4→F3)
Byte 121 of the hardcoded private exponent was 0xF4 instead of 0xF3, breaking the RSA invariant e·d ≡ 1 (mod φ(n)). All sign-then-verify operations failed with InvalidPadding. Verified: e·d mod φ(n) = 1 via Python pow(e, -1, phi). 42/42 RSA tests pass. 4/4 bootrom_cpu_e2e tests pass.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Append -latomic after all objects to resolve Unicorn Engine 2.x
|
||||
# __atomic_compare_exchange_16 / __atomic_load_16 / __atomic_store_16
|
||||
# on x86-64 Linux (GNU ld needs -latomic after the objects that reference it).
|
||||
# Scoped to Linux only: macOS has no libatomic and would fail.
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-args=-latomic"]
|
||||
@@ -63,3 +63,5 @@ vendor/
|
||||
coverage/
|
||||
.cache/
|
||||
tmp/
|
||||
.agents/
|
||||
skills-lock.json
|
||||
|
||||
@@ -148,6 +148,8 @@ fn generate<W: std::fmt::Write>(w: &mut W, data: &str) -> Result<(), Box<dyn std
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src/nn/auto.defs");
|
||||
// Unicorn (QEMU) uses 128-bit atomics (__atomic_store_16) on x86_64 Linux
|
||||
println!("cargo:rustc-link-lib=atomic");
|
||||
|
||||
let data = std::fs::read_to_string("src/nn/auto.defs").unwrap_or_default();
|
||||
let mut w = String::new();
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::mmio::gic::{GicDistributor, GicV3};
|
||||
use crate::mmio::MmioDevice;
|
||||
use crate::security::bootrom::{BootRom, BootResult, BootError};
|
||||
use crate::security::efuse::EfuseArray;
|
||||
use crate::security::rsa::RsaPublicKey;
|
||||
use std::cell::RefCell;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
@@ -177,4 +178,22 @@ impl CpuManager {
|
||||
.ok_or(BootError::NoCpu)?;
|
||||
bootrom.boot(core, firmware)
|
||||
}
|
||||
|
||||
/// Run the BootROM on core 0 with a custom RSA public key.
|
||||
///
|
||||
/// This is the test-facing variant that accepts a custom `RsaPublicKey`
|
||||
/// (e.g., from `generate_test_keypair()`). The caller signs firmware with
|
||||
/// the matching private key so BootROM can verify it.
|
||||
pub fn boot_rom_with_key(
|
||||
&mut self,
|
||||
efuse: &EfuseArray,
|
||||
firmware: &[u8],
|
||||
rsa_pub: &RsaPublicKey,
|
||||
) -> Result<BootResult, BootError> {
|
||||
let bootrom = BootRom::with_rsa_key(efuse, rsa_pub.n_bytes(), rsa_pub.e_u32());
|
||||
let core = self
|
||||
.get_core_mut(0)
|
||||
.ok_or(BootError::NoCpu)?;
|
||||
bootrom.boot(core, firmware)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +423,31 @@ impl HipcRouter {
|
||||
Ok(handler(data))
|
||||
}
|
||||
|
||||
/// Convenience: parse raw HIPC bytes and dispatch in one call.
|
||||
///
|
||||
/// Extracts `method_id` from the first u32 of the message payload and
|
||||
/// passes the remaining bytes to the registered handler. Returns
|
||||
/// `MalformedMessage` if the input cannot be parsed.
|
||||
pub fn dispatch_message(
|
||||
&self,
|
||||
data: &[u8],
|
||||
service_name: &str,
|
||||
) -> Result<HipcResponse, DispatchError> {
|
||||
let msg = HipcMessage::parse(data, service_name).map_err(|e| {
|
||||
warn!(
|
||||
"HipcRouter::dispatch_message: parse failed for '{}': {}",
|
||||
service_name, e
|
||||
);
|
||||
DispatchError::MalformedMessage
|
||||
})?;
|
||||
let payload = if msg.raw_data.len() >= 4 {
|
||||
&msg.raw_data[4..]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
self.dispatch(&msg.service_name, msg.method_id, payload)
|
||||
}
|
||||
|
||||
pub fn registered_services(&self) -> Vec<String> {
|
||||
self.services.keys().cloned().collect()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::sys;
|
||||
use crate::nn::hipc::HipcRouter;
|
||||
|
||||
pub mod hipc;
|
||||
pub mod sm;
|
||||
|
||||
macro_rules! define_service {
|
||||
($($name:ident),* $(,)?) => {
|
||||
@@ -223,4 +224,8 @@ pub fn start_host_services(state: &mut sys::State) {
|
||||
for (_name, run_fn) in entries.iter() {
|
||||
run_fn(state);
|
||||
}
|
||||
|
||||
// Wire sm service handlers into the HIPC router.
|
||||
state.hipc_router.register("sm", 0, sm::handler_register_service);
|
||||
state.hipc_router.register("sm", 1, sm::handler_get_service_handle);
|
||||
}
|
||||
|
||||
@@ -410,6 +410,31 @@ pub fn aes_cbc_decrypt(key: &Aes128Key, iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
// ── CTR mode ─────────────────────────────────────────────────────
|
||||
|
||||
/// AES-CTR encrypt/decrypt (XOR with keystream).
|
||||
///
|
||||
/// CTR mode is the same for encrypt and decrypt: XOR plaintext/ciphertext
|
||||
/// with the AES-encrypted counter block. This function can be used for both.
|
||||
///
|
||||
/// The IV is used as the initial counter. For block i, the counter is
|
||||
/// `iv ^ block_index` (big-endian block index XORed into the low 8 bytes).
|
||||
pub fn aes_ctr_xor(key: &Aes128Key, iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
|
||||
let block_count = (data.len() + 15) / 16;
|
||||
let mut out = Vec::with_capacity(data.len());
|
||||
for block_idx in 0..block_count {
|
||||
let mut ctr = *iv;
|
||||
let idx_bytes = (block_idx as u64).to_be_bytes();
|
||||
for i in 0..8 { ctr[i] ^= idx_bytes[i]; }
|
||||
let keystream = aes_encrypt_block(key, &ctr);
|
||||
let data_offset = block_idx * 16;
|
||||
let remaining = data.len() - data_offset;
|
||||
let take = remaining.min(16);
|
||||
for i in 0..take { out.push(data[data_offset + i] ^ keystream[i]); }
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -9,16 +9,16 @@ use std::time::Instant;
|
||||
|
||||
use log::{error, info, warn};
|
||||
|
||||
use super::aes::{Aes128Key, aes_encrypt_block};
|
||||
use super::aes::{Aes128Key, aes_ctr_xor};
|
||||
use super::efuse::EfuseArray;
|
||||
use super::key_derivation::KeyDerivation;
|
||||
use super::rsa::{RsaPublicKey, RsaVerifyError, sha256};
|
||||
use super::rsa::{RsaPublicKey, RsaVerifyError};
|
||||
|
||||
pub const PACKAGE2_LOAD_ADDR: u64 = 0x4001_0000;
|
||||
const SIG_SIZE: usize = 256;
|
||||
const PK11_HEADER_SIZE: usize = 256;
|
||||
pub const SIG_SIZE: usize = 256;
|
||||
pub const PK11_HEADER_SIZE: usize = 256;
|
||||
pub const MIN_FIRMWARE_SIZE: usize = SIG_SIZE + PK11_HEADER_SIZE + 1;
|
||||
const PK11_MAGIC: u32 = 0x504B_3131;
|
||||
pub const PK11_MAGIC: u32 = 0x504B_3131;
|
||||
|
||||
// Community-reference T210 RSA-2048 modulus (Atmosphère / fusee-gelee)
|
||||
const T210_RSA_MODULUS: [u8; 256] = [
|
||||
@@ -41,24 +41,6 @@ const T210_RSA_MODULUS: [u8; 256] = [
|
||||
];
|
||||
const T210_RSA_EXPONENT: u32 = 65537;
|
||||
|
||||
// ── AES-CTR (inlined — aes.rs doesn't export CTR mode) ────────────
|
||||
|
||||
fn aes_ctr_xor(key: &Aes128Key, iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
|
||||
let block_count = (data.len() + 15) / 16;
|
||||
let mut out = Vec::with_capacity(data.len());
|
||||
for block_idx in 0..block_count {
|
||||
let mut ctr = *iv;
|
||||
let idx_bytes = (block_idx as u64).to_be_bytes();
|
||||
for i in 0..8 { ctr[i] ^= idx_bytes[i]; }
|
||||
let keystream = aes_encrypt_block(key, &ctr);
|
||||
let data_offset = block_idx * 16;
|
||||
let remaining = data.len() - data_offset;
|
||||
let take = remaining.min(16);
|
||||
for i in 0..take { out.push(data[data_offset + i] ^ keystream[i]); }
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── PK11 header ───────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -70,6 +52,16 @@ pub struct Pk11Header {
|
||||
}
|
||||
|
||||
impl Pk11Header {
|
||||
/// Serialize this PK11 header to a 256-byte array.
|
||||
pub fn serialize(&self) -> [u8; 256] {
|
||||
let mut raw = [0u8; 256];
|
||||
raw[0..4].copy_from_slice(&self.magic.to_le_bytes());
|
||||
raw[4..8].copy_from_slice(&self.version.to_le_bytes());
|
||||
raw[8..16].copy_from_slice(&self.package2_size.to_le_bytes());
|
||||
raw[16..32].copy_from_slice(&self.ctr_iv);
|
||||
raw
|
||||
}
|
||||
|
||||
pub fn parse(raw: &[u8; 256]) -> Result<Self, BootError> {
|
||||
let magic = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
|
||||
if magic != PK11_MAGIC {
|
||||
@@ -275,6 +267,7 @@ impl fmt::Debug for BootRom {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::aes::aes_ctr_xor;
|
||||
|
||||
#[test] fn pk11_parse_valid() {
|
||||
let mut raw = [0u8; 256];
|
||||
|
||||
+671
-29
@@ -354,8 +354,8 @@ impl BigUint {
|
||||
}
|
||||
|
||||
let mut rem_limb = 0u32;
|
||||
if word_shift > 0 && word_shift <= self.limbs.len() {
|
||||
rem_limb = self.limbs[word_shift - 1] >> (32 - bit_shift.min(32));
|
||||
if word_shift > 0 && word_shift <= self.limbs.len() && bit_shift > 0 {
|
||||
rem_limb = self.limbs[word_shift - 1] >> (32 - bit_shift);
|
||||
} else if word_shift == 0 && bit_shift > 0 {
|
||||
rem_limb = self.limbs[0] & ((1u32 << bit_shift) - 1);
|
||||
}
|
||||
@@ -394,56 +394,43 @@ impl fmt::Debug for BigUint {
|
||||
|
||||
/// Compute `base^exponent mod modulus` using Barrett reduction.
|
||||
///
|
||||
/// All inputs are unsigned (big-endian byte vectors). exponent is the
|
||||
/// public exponent (typically 65537 = 0x010001).
|
||||
/// Standard left-to-right binary exponentiation (square-and-multiply).
|
||||
/// Barrett μ is precomputed once and reused for all reductions.
|
||||
fn mod_pow(base: &BigUint, exponent: &BigUint, modulus: &BigUint) -> BigUint {
|
||||
if modulus.is_one() {
|
||||
return BigUint { limbs: vec![0] };
|
||||
}
|
||||
|
||||
// Barrett precomputation: mu = floor(4^k / n) where k = ceil(len(n)/2)
|
||||
// Actually we'll use a simpler left-to-right binary exponentiation
|
||||
// with repeated modulo via a basic division.
|
||||
let k = (modulus.bit_len() + 7) / 8;
|
||||
let k_bits = k * 8;
|
||||
let mu = barrett_mu(modulus, k_bits);
|
||||
|
||||
let mut result = BigUint { limbs: vec![1] };
|
||||
let mut b = base.clone();
|
||||
|
||||
// Left-to-right binary exponentiation
|
||||
// Left-to-right binary exponentiation: square result each step,
|
||||
// multiply by base when the bit is set. Base stays constant.
|
||||
for i in (0..exponent.bit_len()).rev() {
|
||||
result = barrett_mod(&result.mul(&result), modulus);
|
||||
result = barrett_mod_with_mu(&result.mul(&result), modulus, k_bits, &mu);
|
||||
|
||||
let word_idx = i / 32;
|
||||
let bit_idx = i % 32;
|
||||
let bit = (exponent.limbs[word_idx] >> bit_idx) & 1;
|
||||
if bit == 1 {
|
||||
result = barrett_mod(&result.mul(&b), modulus);
|
||||
result = barrett_mod_with_mu(&result.mul(base), modulus, k_bits, &mu);
|
||||
}
|
||||
b = barrett_mod(&b.mul(&b), modulus);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Barrett reduction: compute `x mod n`.
|
||||
///
|
||||
/// Precomputes μ = floor(4^(2k) / n) where k = ceil(log256(n)).
|
||||
/// Then for x < n^2: x mod n = x - floor(x * μ / 4^(2k)) * n,
|
||||
/// with at most one corrective subtraction.
|
||||
fn barrett_mod(x: &BigUint, n: &BigUint) -> BigUint {
|
||||
// Quick path: if x < n, return x
|
||||
/// Barrett reduction with precomputed μ: compute `x mod n`.
|
||||
fn barrett_mod_with_mu(x: &BigUint, n: &BigUint, k_bits: usize, mu: &BigUint) -> BigUint {
|
||||
if !x.ge(n) {
|
||||
return x.clone();
|
||||
}
|
||||
|
||||
let k = (n.bit_len() + 7) / 8; // ceil(log256(n))
|
||||
let k_bits = k * 8;
|
||||
|
||||
// μ = floor(2^(2*k_bits) / n)
|
||||
let mu = barrett_mu(n, k_bits);
|
||||
|
||||
// q_hat = floor(x * μ / 2^(2*k_bits))
|
||||
// = (x * μ) >> (2*k_bits)
|
||||
let (q_hat, _) = x.mul(&mu).shr_bits(2 * k_bits);
|
||||
let (q_hat, _) = x.mul(mu).shr_bits(2 * k_bits);
|
||||
|
||||
// r_hat = x - q_hat * n
|
||||
let qn = q_hat.mul(n);
|
||||
@@ -451,8 +438,6 @@ fn barrett_mod(x: &BigUint, n: &BigUint) -> BigUint {
|
||||
if r.ge(&qn) {
|
||||
r.sub_assign(&qn);
|
||||
} else {
|
||||
// If q_hat overshoots, just return x mod n the slow way
|
||||
// This shouldn't happen for Barrett with proper mu
|
||||
return slow_mod(x, n);
|
||||
}
|
||||
|
||||
@@ -464,6 +449,17 @@ fn barrett_mod(x: &BigUint, n: &BigUint) -> BigUint {
|
||||
r
|
||||
}
|
||||
|
||||
/// Barrett reduction: compute `x mod n`.
|
||||
///
|
||||
/// Convenience wrapper. Prefer `barrett_mod_with_mu` for repeated
|
||||
/// reductions with the same modulus.
|
||||
fn barrett_mod(x: &BigUint, n: &BigUint) -> BigUint {
|
||||
let k = (n.bit_len() + 7) / 8;
|
||||
let k_bits = k * 8;
|
||||
let mu = barrett_mu(n, k_bits);
|
||||
barrett_mod_with_mu(x, n, k_bits, &mu)
|
||||
}
|
||||
|
||||
/// Compute μ = floor(2^(2*k_bits) / n) for Barrett reduction.
|
||||
fn barrett_mu(n: &BigUint, k_bits: usize) -> BigUint {
|
||||
// μ = floor(2^(2*k_bits) / n)
|
||||
@@ -558,6 +554,199 @@ fn slow_mod(x: &BigUint, n: &BigUint) -> BigUint {
|
||||
r
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Modular inverse (extended Euclidean algorithm)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Compute `a^{-1} mod n` using the extended Euclidean algorithm.
|
||||
///
|
||||
/// Returns `None` if `gcd(a, n) != 1` (i.e. no inverse exists).
|
||||
fn mod_inverse(a: &BigUint, n: &BigUint) -> Option<BigUint> {
|
||||
// Extended Euclidean: find x such that a*x + n*y = gcd(a,n)
|
||||
// If gcd = 1, then x = a^{-1} mod n.
|
||||
|
||||
if n.is_one() || a.is_zero() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// We'll work on BigUint copies
|
||||
let mut r0 = a.clone();
|
||||
let mut r1 = n.clone();
|
||||
let mut s0 = BigUint { limbs: vec![1] };
|
||||
let mut s1 = BigUint { limbs: vec![0] };
|
||||
|
||||
while !r1.is_zero() {
|
||||
// q = r0 / r1
|
||||
let q = div_floor(&r0, &r1);
|
||||
|
||||
// r2 = r0 - q * r1
|
||||
let qr1 = q.mul(&r1);
|
||||
let mut r2 = if r0.ge(&qr1) {
|
||||
r0.clone()
|
||||
} else {
|
||||
// shouldn't happen since q = floor(r0/r1)
|
||||
return None;
|
||||
};
|
||||
r2.sub_assign(&qr1);
|
||||
|
||||
// s2 = s0 - q * s1 mod n
|
||||
let qs1 = q.mul(&s1);
|
||||
let mut s2: BigUint;
|
||||
if s0.ge(&qs1) {
|
||||
s2 = s0.clone();
|
||||
s2.sub_assign(&qs1);
|
||||
} else {
|
||||
// s0 - q*s1 could be negative; add multiples of n until positive
|
||||
s2 = s0.clone();
|
||||
// compute s0 + ceil(qs1/n)*n - qs1, but simpler: while !s2.ge(&qs1) { s2.add_shifted_mut(&n.limbs, 0); }
|
||||
// Actually, let's do it mod n directly:
|
||||
// s2 ≡ s0 - q*s1 (mod n)
|
||||
// = (s0 mod n) - (q*s1 mod n) mod n
|
||||
let qs1_mod = barrett_mod(&qs1, n);
|
||||
let s0_mod = barrett_mod(&s0, n);
|
||||
if s0_mod.ge(&qs1_mod) {
|
||||
s2 = s0_mod;
|
||||
s2.sub_assign(&qs1_mod);
|
||||
} else {
|
||||
s2 = {
|
||||
let mut tmp = n.clone();
|
||||
// tmp = n - (qs1_mod - s0_mod)
|
||||
let mut diff = qs1_mod.clone();
|
||||
diff.sub_assign(&s0_mod);
|
||||
tmp.sub_assign(&diff);
|
||||
tmp
|
||||
};
|
||||
}
|
||||
}
|
||||
// Ensure s2 < n
|
||||
while s2.ge(n) {
|
||||
s2.sub_assign(n);
|
||||
}
|
||||
|
||||
r0 = r1;
|
||||
r1 = r2;
|
||||
s0 = s1;
|
||||
s1 = s2;
|
||||
}
|
||||
|
||||
// r0 = gcd(a, n), s0 = a^{-1} mod n if gcd = 1
|
||||
if !r0.is_one() {
|
||||
return None; // no inverse
|
||||
}
|
||||
|
||||
Some(s0)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Miller-Rabin primality test (deterministic for test reproducibility)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Miller-Rabin probabilistic primality test with the given witness bases.
|
||||
///
|
||||
/// Returns `true` if `n` is probably prime. Uses the specified bases;
|
||||
/// for numbers < 2^64, bases [2, 3, 5, 7, 11] suffice. For larger
|
||||
/// numbers this is probabilistic but good enough for test keygen.
|
||||
fn miller_rabin(n: &BigUint, bases: &[u32]) -> bool {
|
||||
if n.bit_len() < 2 {
|
||||
return false;
|
||||
}
|
||||
// Check if n is even
|
||||
if (n.limbs[0] & 1) == 0 {
|
||||
return *n == BigUint { limbs: vec![2] };
|
||||
}
|
||||
// Check small divisors
|
||||
let small_primes: [u32; 10] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
|
||||
for &p in &small_primes {
|
||||
let p_big = BigUint { limbs: vec![p] };
|
||||
if n.ge(&p_big) {
|
||||
let (_, rem) = div_mod(n, &p_big);
|
||||
if rem.is_zero() {
|
||||
return *n == p_big;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write n - 1 = 2^s * d
|
||||
let one = BigUint { limbs: vec![1] };
|
||||
let two = BigUint { limbs: vec![2] };
|
||||
let mut n_minus_1 = n.clone();
|
||||
n_minus_1.sub_assign(&one);
|
||||
|
||||
let mut d = n_minus_1.clone();
|
||||
let mut s = 0usize;
|
||||
while (d.limbs[0] & 1) == 0 {
|
||||
let (q, _) = d.shr_bits(1);
|
||||
d = q;
|
||||
s += 1;
|
||||
}
|
||||
|
||||
for &base in bases {
|
||||
let a = BigUint { limbs: vec![base] };
|
||||
if a.ge(n) {
|
||||
continue;
|
||||
}
|
||||
let mut x = mod_pow(&a, &d, n);
|
||||
if x.is_one() || x == n_minus_1 {
|
||||
continue;
|
||||
}
|
||||
let mut composite = true;
|
||||
for _ in 1..s {
|
||||
x = barrett_mod(&x.mul(&x), n);
|
||||
if x == n_minus_1 {
|
||||
composite = false;
|
||||
break;
|
||||
}
|
||||
if x.is_one() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if composite {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Integer division returning (quotient, remainder): a = q*b + r.
|
||||
fn div_mod(a: &BigUint, b: &BigUint) -> (BigUint, BigUint) {
|
||||
let q = div_floor(a, b);
|
||||
let qb = q.mul(b);
|
||||
let mut r = a.clone();
|
||||
r.sub_assign(&qb);
|
||||
(q, r)
|
||||
}
|
||||
|
||||
/// Generate a deterministic 1024-bit probable prime from a seed.
|
||||
///
|
||||
/// Starts checking numbers at `seed`, increments by 2 until a probable
|
||||
/// prime is found. Uses Miller-Rabin with bases [2, 3, 5, 7, 11].
|
||||
fn find_prime(seed: &BigUint) -> BigUint {
|
||||
let two = BigUint { limbs: vec![2] };
|
||||
let mut candidate = seed.clone();
|
||||
// Ensure candidate is odd
|
||||
if (candidate.limbs[0] & 1) == 0 {
|
||||
candidate.limbs[0] |= 1;
|
||||
}
|
||||
loop {
|
||||
if miller_rabin(&candidate, &[2, 3, 5, 7, 11]) {
|
||||
return candidate;
|
||||
}
|
||||
// candidate += 2
|
||||
let mut carry = 2u64;
|
||||
for limb in &mut candidate.limbs {
|
||||
let sum = (*limb as u64) + carry;
|
||||
*limb = sum as u32;
|
||||
carry = sum >> 32;
|
||||
if carry == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if carry != 0 {
|
||||
candidate.limbs.push(carry as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// PKCS#1 v1.5 signature verification (RFC 8017 §8.2.2)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -644,6 +833,21 @@ impl RsaPublicKey {
|
||||
m.to_be_bytes_padded(256)
|
||||
}
|
||||
|
||||
/// Return a reference to the modulus bytes (256 bytes, big-endian).
|
||||
pub fn n_bytes(&self) -> &[u8; 256] {
|
||||
&self.n
|
||||
}
|
||||
|
||||
/// Return the public exponent as a u32 (e.g., 65537).
|
||||
pub fn e_u32(&self) -> u32 {
|
||||
// e is stored as big-endian bytes; reconstruct u32
|
||||
let mut val: u32 = 0;
|
||||
for &b in &self.e {
|
||||
val = (val << 8) | (b as u32);
|
||||
}
|
||||
val
|
||||
}
|
||||
|
||||
/// Verify a PKCS#1 v1.5 SHA-256 signature.
|
||||
///
|
||||
/// Returns `Ok(())` if `signature` is a valid RSA-2048 PKCS#1 v1.5
|
||||
@@ -673,6 +877,159 @@ impl fmt::Debug for RsaPublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RSA-2048 private key and signing (for test key generation)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/// RSA-2048 private key (n, d).
|
||||
///
|
||||
/// Holds the modulus `n` and private exponent `d`. Used to sign test
|
||||
/// firmware so the BootROM can verify it.
|
||||
pub struct RsaPrivateKey {
|
||||
/// RSA modulus (matching the public key).
|
||||
n: [u8; 256],
|
||||
/// Private exponent `d = e^{-1} mod λ(n)`.
|
||||
d: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RsaPrivateKey {
|
||||
/// RSA-2048 raw private-key operation: `m^d mod n`.
|
||||
///
|
||||
/// Returns the big-endian encoded signed message representative (256 bytes).
|
||||
fn rsasp1(&self, m: &[u8; 256]) -> Vec<u8> {
|
||||
let m_big = BigUint::from_be_bytes(m);
|
||||
let n_big = BigUint::from_be_bytes(&self.n);
|
||||
let d_big = BigUint::from_be_bytes(&self.d);
|
||||
|
||||
let s = mod_pow(&m_big, &d_big, &n_big);
|
||||
s.to_be_bytes_padded(256)
|
||||
}
|
||||
|
||||
/// Sign a message using PKCS#1 v1.5 SHA-256 (RSASSA-PKCS1-v1_5-SIGN).
|
||||
///
|
||||
/// 1. Compute SHA-256(message)
|
||||
/// 2. EMSA-PKCS1-v1_5-ENCODE the hash → EM (256 bytes)
|
||||
/// 3. RSASP1: s = EM^d mod n
|
||||
///
|
||||
/// Returns the 256-byte signature (big-endian).
|
||||
pub fn sign(&self, message: &[u8]) -> [u8; 256] {
|
||||
let digest = sha256(message);
|
||||
let em = emsa_pkcs1_v15_encode(&digest, 256);
|
||||
let em_arr: [u8; 256] = em.try_into().expect("EM must be 256 bytes");
|
||||
let sig_bytes = self.rsasp1(&em_arr);
|
||||
let mut sig = [0u8; 256];
|
||||
sig.copy_from_slice(&sig_bytes);
|
||||
sig
|
||||
}
|
||||
|
||||
/// Get the corresponding public key (e = 65537).
|
||||
pub fn public_key(&self) -> RsaPublicKey {
|
||||
RsaPublicKey::new_with_e_u32(&self.n, 65537)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RsaPrivateKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RsaPrivateKey")
|
||||
.field("n_bits", &2048usize)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-computed 1024-bit prime p for test key generation.
|
||||
///
|
||||
/// Generated from seed 2^1023 + 1, found after 577 Miller-Rabin steps.
|
||||
/// Hardcoded to avoid expensive runtime primality testing in pure Rust.
|
||||
const TEST_PRIME_P: [u8; 128] = [
|
||||
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x83,
|
||||
];
|
||||
|
||||
/// Pre-computed 1024-bit prime q for test key generation.
|
||||
///
|
||||
/// Generated from seed p + 200000 (offset from p to ensure p ≠ q),
|
||||
/// found after 209 Miller-Rabin steps.
|
||||
const TEST_PRIME_Q: [u8; 128] = [
|
||||
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x13, 0x65,
|
||||
];
|
||||
|
||||
/// Pre-computed 2048-bit RSA modulus n = p * q for test keys.
|
||||
/// Generated from p = next_prime(2^1023 + 1), q = next_prime(p + 200000),
|
||||
/// with a fixed random seed (12345) for reproducible Miller-Rabin.
|
||||
const TEST_MODULUS: [u8; 256] = [
|
||||
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8B, 0xF4,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xE0, 0x80, 0xAF,
|
||||
];
|
||||
|
||||
/// Pre-computed private exponent d = e^{-1} mod λ(pq) for our test keypair.
|
||||
/// e = 65537.
|
||||
const TEST_PRIVATE_EXP_D: [u8; 256] = [
|
||||
0x32, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C,
|
||||
0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C,
|
||||
0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C,
|
||||
0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C,
|
||||
0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C,
|
||||
0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C,
|
||||
0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C,
|
||||
0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF3, 0x4D, 0x0C, 0xB2, 0xF4, 0x88, 0x43,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9,
|
||||
0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xC6, 0x46, 0x39, 0xB9, 0xD1, 0x4F, 0xE8, 0xF1,
|
||||
];
|
||||
|
||||
/// Generate a deterministic test RSA-2048 keypair.
|
||||
///
|
||||
/// Uses hardcoded pre-computed primes p, q and private exponent d.
|
||||
/// e = 65537. This ensures reproducible test vectors and instant
|
||||
/// key generation without expensive runtime primality testing.
|
||||
///
|
||||
/// The primes were generated externally from deterministic seeds
|
||||
/// (2^1023 + 1 for p, 2^1023 + 200001 for q) and verified with
|
||||
/// Python's built-in Miller-Rabin.
|
||||
pub fn generate_test_keypair() -> (RsaPublicKey, RsaPrivateKey) {
|
||||
let e_u32: u32 = 65537;
|
||||
|
||||
let public = RsaPublicKey::new_with_e_u32(&TEST_MODULUS, e_u32);
|
||||
let private = RsaPrivateKey {
|
||||
n: TEST_MODULUS,
|
||||
d: TEST_PRIVATE_EXP_D.to_vec(),
|
||||
};
|
||||
|
||||
(public, private)
|
||||
}
|
||||
|
||||
/// Verify that EM (encoded message representative, 256 bytes) matches
|
||||
/// PKCS#1 v1.5 signature format for SHA-256 digest.
|
||||
fn verify_em(em: &[u8], digest: &[u8; 32]) -> Result<(), RsaVerifyError> {
|
||||
@@ -934,6 +1291,162 @@ mod tests {
|
||||
assert_eq!(r.to_be_bytes(), vec![4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_pow_vs_python_2e_mod_n() {
|
||||
let base = BigUint { limbs: vec![2] };
|
||||
let exp = BigUint { limbs: vec![65537] };
|
||||
let n = BigUint::from_be_bytes(&TEST_MODULUS);
|
||||
|
||||
let r = mod_pow(&base, &exp, &n);
|
||||
let r_bytes = r.to_be_bytes_padded(256);
|
||||
|
||||
// Python: pow(2, 65537, n) — recomputed for corrected TEST_MODULUS
|
||||
let expected: [u8; 256] = [
|
||||
0x00, 0x2F, 0x36, 0xB2, 0x4A, 0x32, 0x75, 0xB3,
|
||||
0x0C, 0xD4, 0xFF, 0xFE, 0x6C, 0x78, 0xA9, 0xC8,
|
||||
0x61, 0xB3, 0x51, 0x5A, 0xB9, 0x3C, 0xB1, 0xF7,
|
||||
0x8E, 0x33, 0x1F, 0x44, 0xEA, 0x8C, 0x45, 0x49,
|
||||
0x6A, 0x12, 0xA0, 0x5A, 0x36, 0x17, 0x65, 0x02,
|
||||
0xE4, 0x8E, 0x20, 0x2A, 0xA5, 0xDE, 0x44, 0x35,
|
||||
0x03, 0x51, 0x05, 0x2F, 0x37, 0xEE, 0xC6, 0xCC,
|
||||
0xB9, 0xE8, 0x75, 0x5F, 0x52, 0x17, 0xA3, 0x27,
|
||||
0xC2, 0x76, 0xAA, 0xBF, 0xA5, 0x7F, 0x9F, 0x67,
|
||||
0x8F, 0xBB, 0x87, 0x94, 0xCE, 0xBE, 0xC0, 0x47,
|
||||
0x42, 0x3D, 0x32, 0x02, 0x04, 0xFE, 0xE8, 0x33,
|
||||
0x85, 0xD9, 0x3D, 0x0D, 0x9C, 0x43, 0x74, 0xD8,
|
||||
0x79, 0x1D, 0xA2, 0x25, 0x7B, 0x56, 0x1B, 0x59,
|
||||
0xE2, 0x34, 0x20, 0xCB, 0xAD, 0x47, 0x8D, 0xB4,
|
||||
0xE7, 0x57, 0xB3, 0x40, 0x32, 0xC9, 0x65, 0xF0,
|
||||
0x9B, 0x7C, 0x9A, 0xE1, 0xF0, 0x8D, 0x34, 0x10,
|
||||
0x2A, 0x07, 0x8C, 0xC9, 0x83, 0x52, 0x0D, 0xA5,
|
||||
0xC9, 0xFD, 0xF1, 0xC6, 0xC8, 0xCC, 0x06, 0x21,
|
||||
0x98, 0x10, 0x18, 0xA3, 0x79, 0xAD, 0xDF, 0xCD,
|
||||
0x21, 0x4C, 0x27, 0xDC, 0x6D, 0xB9, 0x34, 0x73,
|
||||
0x24, 0x12, 0xEE, 0x04, 0x17, 0x19, 0x89, 0x6F,
|
||||
0x41, 0x2D, 0x7D, 0x13, 0x96, 0xA2, 0xEB, 0xDA,
|
||||
0x92, 0x56, 0xCF, 0x0C, 0xE0, 0x11, 0x5B, 0xF0,
|
||||
0x5C, 0x19, 0x0D, 0x53, 0xF9, 0x05, 0x09, 0x88,
|
||||
0x93, 0xD6, 0x7E, 0x6B, 0x98, 0xFC, 0x90, 0x1B,
|
||||
0x75, 0x41, 0x4B, 0x60, 0x19, 0xD2, 0x63, 0x61,
|
||||
0x8A, 0xFE, 0x47, 0x83, 0x44, 0x26, 0x56, 0x33,
|
||||
0x89, 0x62, 0x5D, 0x80, 0x48, 0x6A, 0xF8, 0x17,
|
||||
0x28, 0x56, 0x42, 0xBC, 0x44, 0x3A, 0x1D, 0xD1,
|
||||
0xBE, 0x88, 0xB3, 0xCD, 0xB6, 0xCB, 0x98, 0x53,
|
||||
0xA0, 0x3E, 0x77, 0x04, 0xA9, 0x7C, 0x13, 0x1D,
|
||||
0x06, 0x5D, 0x92, 0xC0, 0x9A, 0x23, 0xBA, 0xD7,
|
||||
];
|
||||
assert_eq!(r_bytes, expected, "2^65537 mod n must match Python");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_pow_vs_python_2e_0xffff() {
|
||||
// Verify 2^0xFFFF mod n matches Python (medium exponent)
|
||||
let base = BigUint { limbs: vec![2] };
|
||||
let exp = BigUint { limbs: vec![0xFFFF] };
|
||||
let n = BigUint::from_be_bytes(&TEST_MODULUS);
|
||||
|
||||
let r = mod_pow(&base, &exp, &n);
|
||||
let r_bytes = r.to_be_bytes_padded(256);
|
||||
|
||||
let expected: [u8; 256] = [
|
||||
0x30, 0x0B, 0xCD, 0xAC, 0x92, 0x8C, 0x9D, 0x6C,
|
||||
0xC3, 0x35, 0x3F, 0xFF, 0x9B, 0x1E, 0x2A, 0x72,
|
||||
0x18, 0x6C, 0xD4, 0x56, 0xAE, 0x4F, 0x2C, 0x7D,
|
||||
0xE3, 0x8C, 0xC7, 0xD1, 0x3A, 0xA3, 0x11, 0x52,
|
||||
0x5A, 0x84, 0xA8, 0x16, 0x8D, 0x85, 0xD9, 0x40,
|
||||
0xB9, 0x23, 0x88, 0x0A, 0xA9, 0x77, 0x91, 0x0D,
|
||||
0x40, 0xD4, 0x41, 0x4B, 0xCD, 0xFB, 0xB1, 0xB3,
|
||||
0x2E, 0x7A, 0x1D, 0x57, 0xD4, 0x85, 0xE8, 0xC9,
|
||||
0xF0, 0x9D, 0xAA, 0xAF, 0xE9, 0x5F, 0xE7, 0xD9,
|
||||
0xE3, 0xEE, 0xE1, 0xE5, 0x33, 0xAF, 0xB0, 0x11,
|
||||
0xD0, 0x8F, 0x4C, 0x80, 0x81, 0x3F, 0xBA, 0x0C,
|
||||
0xE1, 0x76, 0x4F, 0x43, 0x67, 0x10, 0xDD, 0x36,
|
||||
0x1E, 0x47, 0x68, 0x89, 0x5E, 0xD5, 0x86, 0xD6,
|
||||
0x78, 0x8D, 0x08, 0x32, 0xEB, 0x51, 0xE3, 0x6D,
|
||||
0x39, 0xD5, 0xEC, 0xD0, 0x0C, 0xB2, 0x59, 0x7C,
|
||||
0x26, 0xDF, 0x26, 0xB8, 0x7C, 0x24, 0x75, 0xFB,
|
||||
0x0A, 0x81, 0xE3, 0x32, 0x60, 0xD4, 0x83, 0x69,
|
||||
0x72, 0x7F, 0x7C, 0x71, 0xB2, 0x33, 0x01, 0x88,
|
||||
0x66, 0x04, 0x06, 0x28, 0xDE, 0x6B, 0x77, 0xF3,
|
||||
0x48, 0x53, 0x09, 0xF7, 0x1B, 0x6E, 0x4D, 0x1C,
|
||||
0xC9, 0x04, 0xBB, 0x81, 0x05, 0xC6, 0x62, 0x5B,
|
||||
0xD0, 0x4B, 0x5F, 0x44, 0xE5, 0xA8, 0xBA, 0xF6,
|
||||
0xA4, 0x95, 0xB3, 0xC3, 0x38, 0x04, 0x56, 0xFC,
|
||||
0x17, 0x06, 0x43, 0x54, 0xFE, 0x41, 0x42, 0x62,
|
||||
0x24, 0xF5, 0x9F, 0x9A, 0xE6, 0x3F, 0x24, 0x06,
|
||||
0xDD, 0x50, 0x52, 0xD8, 0x06, 0x74, 0x98, 0xD8,
|
||||
0x62, 0xBF, 0x91, 0xE0, 0xD1, 0x09, 0x95, 0x8C,
|
||||
0xE2, 0x58, 0x97, 0x60, 0x12, 0x1A, 0xBE, 0x05,
|
||||
0xCA, 0x15, 0x90, 0xAF, 0x11, 0x0E, 0x87, 0x74,
|
||||
0x6F, 0xA2, 0x2C, 0xF3, 0x6D, 0xB2, 0xE6, 0x14,
|
||||
0xE8, 0x0F, 0x9D, 0xC1, 0x2A, 0x5F, 0x04, 0xC7,
|
||||
0x41, 0x97, 0x64, 0xB0, 0x30, 0xF1, 0x4F, 0x39,
|
||||
];
|
||||
assert_eq!(r_bytes, expected, "2^0xFFFF mod n must match Python");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_pow_vs_python_2d_mod_n() {
|
||||
// Verify 2^d mod n matches Python for the test key's private exponent.
|
||||
let base = BigUint { limbs: vec![2] };
|
||||
let d_big = BigUint::from_be_bytes(&TEST_PRIVATE_EXP_D);
|
||||
let n = BigUint::from_be_bytes(&TEST_MODULUS);
|
||||
|
||||
let r = mod_pow(&base, &d_big, &n);
|
||||
let r_bytes = r.to_be_bytes_padded(256);
|
||||
|
||||
// Python: pow(2, d, n) — recomputed for corrected TEST_PRIVATE_EXP_D
|
||||
let expected: [u8; 256] = [
|
||||
0x3C, 0xC7, 0x20, 0xF0, 0xDA, 0xAA, 0x83, 0x8E,
|
||||
0x1E, 0xC6, 0xAE, 0x55, 0x83, 0x61, 0x83, 0x0C,
|
||||
0xB1, 0x27, 0xB6, 0xE2, 0x5B, 0xD4, 0x1C, 0x66,
|
||||
0x23, 0x09, 0x11, 0x42, 0x2C, 0xFC, 0xB3, 0x01,
|
||||
0x93, 0x6A, 0x62, 0xE8, 0x6F, 0xC2, 0x7B, 0xD3,
|
||||
0x2A, 0x58, 0x4A, 0xD4, 0x14, 0x90, 0x11, 0x45,
|
||||
0xFE, 0xFC, 0x8A, 0x9A, 0x5C, 0xFF, 0x0E, 0xAB,
|
||||
0xD2, 0x18, 0x1D, 0xA2, 0x01, 0x28, 0xD8, 0x02,
|
||||
0xC4, 0xAF, 0xB3, 0x1F, 0xE7, 0xED, 0x48, 0x4B,
|
||||
0x9F, 0x2E, 0x0A, 0x73, 0x4F, 0x1F, 0x94, 0xB9,
|
||||
0xC9, 0x66, 0xE9, 0x4C, 0x13, 0x1E, 0x9D, 0xC1,
|
||||
0xD9, 0xD7, 0x3D, 0xEE, 0x9A, 0x79, 0x1A, 0xD5,
|
||||
0xD0, 0xBE, 0x01, 0xAC, 0x36, 0xBB, 0x1F, 0xC5,
|
||||
0xB8, 0x26, 0x1E, 0x9F, 0x00, 0x7A, 0xE7, 0x34,
|
||||
0x25, 0x00, 0x75, 0x29, 0xDA, 0x3B, 0x28, 0x02,
|
||||
0x0D, 0x16, 0x96, 0x33, 0x32, 0xC5, 0x26, 0xCF,
|
||||
0x4E, 0x62, 0x20, 0x34, 0x9A, 0x87, 0x08, 0xFA,
|
||||
0x53, 0x5D, 0x16, 0xDA, 0xE8, 0x54, 0x8A, 0x83,
|
||||
0xA7, 0xEF, 0x2D, 0x3F, 0xE8, 0xE8, 0x1A, 0xC3,
|
||||
0x02, 0x48, 0x9D, 0x45, 0x92, 0x52, 0x62, 0x62,
|
||||
0x4C, 0x1F, 0x6F, 0xB0, 0x19, 0x77, 0x7D, 0xA6,
|
||||
0xA7, 0x4F, 0xEE, 0xEB, 0xDF, 0xF2, 0x82, 0x5A,
|
||||
0x50, 0x54, 0x2F, 0xD9, 0xA7, 0xDE, 0xE4, 0x09,
|
||||
0x72, 0x76, 0x37, 0x91, 0xBD, 0xDC, 0x90, 0xEE,
|
||||
0xFC, 0xBE, 0x36, 0x63, 0x1E, 0xF4, 0x91, 0x95,
|
||||
0x27, 0xA5, 0x39, 0x40, 0xD2, 0x8B, 0x75, 0x1C,
|
||||
0xDE, 0xBF, 0xFB, 0x47, 0x3E, 0xBD, 0x7A, 0xB2,
|
||||
0xBC, 0x30, 0x32, 0xFC, 0x58, 0x6F, 0xCF, 0x5A,
|
||||
0xB2, 0xC7, 0x07, 0x66, 0xD4, 0x84, 0x7A, 0x80,
|
||||
0x12, 0x08, 0xDF, 0x50, 0x85, 0x94, 0xDD, 0xD4,
|
||||
0x14, 0x89, 0x8A, 0x62, 0x69, 0x14, 0x6D, 0x44,
|
||||
0xE6, 0xD1, 0x9F, 0x3D, 0xE1, 0x06, 0xF5, 0x75,
|
||||
];
|
||||
assert_eq!(r_bytes, expected, "2^d mod n must match Python");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_rsa_roundtrip_test_key() {
|
||||
// Raw (m^d)^e mod n == m, using mod_pow directly
|
||||
let n = BigUint::from_be_bytes(&TEST_MODULUS);
|
||||
let d_big = BigUint::from_be_bytes(&TEST_PRIVATE_EXP_D);
|
||||
let e_big = BigUint { limbs: vec![65537] };
|
||||
let m = BigUint { limbs: vec![12345] };
|
||||
|
||||
let s = mod_pow(&m, &d_big, &n);
|
||||
let recovered = mod_pow(&s, &e_big, &n);
|
||||
assert_eq!(recovered.to_be_bytes(), m.to_be_bytes(),
|
||||
"raw RSA roundtrip m^d^e must recover m");
|
||||
}
|
||||
|
||||
// ── PKCS#1 v1.5 encoding ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
@@ -988,6 +1501,135 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── RSA keypair generation and sign-then-verify ───────────────
|
||||
|
||||
#[test]
|
||||
fn generate_test_keypair_works() {
|
||||
let (public, _private) = generate_test_keypair();
|
||||
|
||||
// Keygen must produce a valid 2048-bit modulus
|
||||
let n_bytes = {
|
||||
// Use the raw RSAEP operation: encrypt a known message,
|
||||
// then RSASP1 decrypts it back — we test via sign/verify below
|
||||
&public.n
|
||||
};
|
||||
assert_eq!(n_bytes.len(), 256);
|
||||
// Modulus must not be zero
|
||||
assert!(n_bytes.iter().any(|&b| b != 0), "modulus must be non-zero");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_then_verify_roundtrip() {
|
||||
let (public, private) = generate_test_keypair();
|
||||
|
||||
let msg = b"BootROM firmware package1 signed payload";
|
||||
let signature = private.sign(msg);
|
||||
|
||||
// Public key must accept the signature
|
||||
public
|
||||
.verify(&signature, msg)
|
||||
.expect("sign-then-verify roundtrip should pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_then_verify_multiple_messages() {
|
||||
let (public, private) = generate_test_keypair();
|
||||
|
||||
let messages: &[&[u8]] = &[
|
||||
b"",
|
||||
b"Hello, world!",
|
||||
b"BootROM stage 1 loader",
|
||||
b"abcdefghijklmnopqrstuvwxyz0123456789",
|
||||
];
|
||||
|
||||
for &msg in messages {
|
||||
let sig = private.sign(msg);
|
||||
public
|
||||
.verify(&sig, msg)
|
||||
.expect("valid signature must verify for all messages");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_then_verify_tampered_signature_fails() {
|
||||
let (public, private) = generate_test_keypair();
|
||||
|
||||
let msg = b"firmware payload";
|
||||
let mut sig = private.sign(msg);
|
||||
|
||||
// Flip a bit in the signature
|
||||
sig[128] ^= 0x01;
|
||||
|
||||
let result = public.verify(&sig, msg);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"tampered signature must not verify"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_then_verify_tampered_message_fails() {
|
||||
let (public, private) = generate_test_keypair();
|
||||
|
||||
let msg = b"original firmware";
|
||||
let sig = private.sign(msg);
|
||||
let tampered = b"modified firmware";
|
||||
|
||||
let result = public.verify(&sig, tampered);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"signature over different message must not verify"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_then_verify_deterministic_keypair() {
|
||||
// Two calls to generate_test_keypair() must produce the same keys
|
||||
let (pk1, sk1) = generate_test_keypair();
|
||||
let (pk2, sk2) = generate_test_keypair();
|
||||
|
||||
assert_eq!(pk1.n, pk2.n, "public modulus must be deterministic");
|
||||
assert_eq!(sk1.n, sk2.n, "private modulus must match");
|
||||
assert_eq!(sk1.d, sk2.d, "private exponent must be deterministic");
|
||||
|
||||
// Also verify they interoperate
|
||||
let msg = b"determinism check";
|
||||
let sig1 = sk1.sign(msg);
|
||||
pk2.verify(&sig1, msg)
|
||||
.expect("cross-keypair verify must pass when deterministic");
|
||||
}
|
||||
|
||||
// ── Modular inverse tests ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn mod_inverse_basic() {
|
||||
// 3^{-1} mod 7 = 5 (since 3*5 = 15 ≡ 1 mod 7)
|
||||
let a = BigUint { limbs: vec![3] };
|
||||
let n = BigUint { limbs: vec![7] };
|
||||
let inv = mod_inverse(&a, &n).expect("3 has inverse mod 7");
|
||||
assert_eq!(inv.to_be_bytes(), vec![5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_inverse_no_inverse() {
|
||||
// 2^{-1} mod 4 doesn't exist (gcd(2,4) = 2)
|
||||
let a = BigUint { limbs: vec![2] };
|
||||
let n = BigUint { limbs: vec![4] };
|
||||
assert!(mod_inverse(&a, &n).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_inverse_large() {
|
||||
// 65537^{-1} mod phi where phi = 60: 65537 mod 60 = 17, 17^{-1} mod 60 = 53
|
||||
let a = BigUint { limbs: vec![65537] };
|
||||
let n = BigUint { limbs: vec![60] };
|
||||
let inv = mod_inverse(&a, &n).expect("65537 has inverse mod 60");
|
||||
// 65537 * 53 mod 60 = 17 * 53 mod 60 = 901 mod 60 = 1
|
||||
let prod = a.mul(&inv);
|
||||
let r = barrett_mod(&prod, &n);
|
||||
assert!(r.is_one(), "product must be 1 mod n, got {:?}", r);
|
||||
}
|
||||
|
||||
// ── Negative tests (Q7) ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::nn;
|
||||
use crate::gpu;
|
||||
use crate::nn::hipc::HipcRouter;
|
||||
use crate::cpu::cpu_manager::CpuManager;
|
||||
use crate::security::bootrom::{BootResult, BootError};
|
||||
use crate::security::efuse::EfuseArray;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Services {
|
||||
@@ -164,20 +167,49 @@ pub struct Services {
|
||||
pub vic: Option<nn::vic::State>,
|
||||
pub wlan: Option<nn::wlan::State>,
|
||||
pub xcd: Option<nn::xcd::State>,
|
||||
pub sm: Option<nn::sm::State>,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub services: Services,
|
||||
pub gpu_state: gpu::State,
|
||||
pub hipc_router: HipcRouter,
|
||||
pub cpu_manager: CpuManager,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
let mut cpu_manager = CpuManager::new();
|
||||
// Register GICv3 on all cores so interrupts work from boot.
|
||||
cpu_manager.register_gic();
|
||||
|
||||
Self {
|
||||
services: Services::default(),
|
||||
gpu_state: gpu::State::default(),
|
||||
hipc_router: HipcRouter::new(),
|
||||
cpu_manager,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: run the BootROM chain on core 0.
|
||||
///
|
||||
/// Derives keys from `efuse`, creates a `BootRom`, and runs `boot()`
|
||||
/// on core 0. This is the main entry point for tests that want to
|
||||
/// validate the full secure boot chain.
|
||||
pub fn boot_rom(&mut self, efuse: &EfuseArray, firmware: &[u8]) -> Result<BootResult, BootError> {
|
||||
self.cpu_manager.boot_rom(efuse, firmware)
|
||||
}
|
||||
|
||||
/// Convenience: run the BootROM chain on core 0 with a custom RSA key.
|
||||
///
|
||||
/// Uses the provided `RsaPublicKey` instead of the hardcoded T210 key.
|
||||
/// This lets tests sign firmware with a test keypair and verify it.
|
||||
pub fn boot_rom_with_key(
|
||||
&mut self,
|
||||
efuse: &EfuseArray,
|
||||
firmware: &[u8],
|
||||
rsa_pub: &crate::security::rsa::RsaPublicKey,
|
||||
) -> Result<BootResult, BootError> {
|
||||
self.cpu_manager.boot_rom_with_key(efuse, firmware, rsa_pub)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//! BootROM → CPU end-to-end integration tests.
|
||||
//!
|
||||
//! Exercises the full boot chain: firmware construction → BootROM validation →
|
||||
//! Package2 placement → end-state verification. Uses the real UnicornCPU
|
||||
//! backing CpuManager, with a custom test RSA keypair so we can both sign
|
||||
//! and verify without depending on the hardcoded T210 key.
|
||||
|
||||
use crate::security::bootrom::{BootPhase, BootError, PACKAGE2_LOAD_ADDR};
|
||||
use crate::security::efuse::EfuseArray;
|
||||
use crate::security::rsa::generate_test_keypair;
|
||||
use crate::sys::State;
|
||||
use crate::tests::firmware_builder::MinimalFirmware;
|
||||
|
||||
/// ARMv8 NOP instruction encoding (matches firmware_builder's NOP sled).
|
||||
const ARM64_NOP: u32 = 0xD503_201F;
|
||||
|
||||
// ── End-to-end boot succeeds ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn bootrom_cpu_e2e_boot_succeeds() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (pub_key, priv_key) = generate_test_keypair();
|
||||
|
||||
// Build valid firmware signed with the test private key
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
let fw_bytes = fw.as_bytes();
|
||||
|
||||
let mut state = State::new();
|
||||
let result = state
|
||||
.boot_rom_with_key(&efuse, fw_bytes, &pub_key)
|
||||
.expect("boot should succeed with valid firmware");
|
||||
|
||||
assert_eq!(result.phase, BootPhase::Package2Placement);
|
||||
assert_eq!(result.package2_load_addr, PACKAGE2_LOAD_ADDR);
|
||||
assert_eq!(result.package2_size, 64, "Package2 should be 64 bytes (16 NOPs)");
|
||||
|
||||
let diag = &result.diagnostics;
|
||||
assert_eq!(diag.phases_completed.len(), 7, "all 7 boot phases must complete");
|
||||
assert!(diag.signature_valid, "signature must be reported valid");
|
||||
}
|
||||
|
||||
// ── Verify Package2 in core 0 memory after boot ───────────────────
|
||||
|
||||
#[test]
|
||||
fn bootrom_cpu_e2e_package2_in_memory() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (pub_key, priv_key) = generate_test_keypair();
|
||||
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
let fw_bytes = fw.as_bytes();
|
||||
|
||||
let mut state = State::new();
|
||||
let result = state
|
||||
.boot_rom_with_key(&efuse, fw_bytes, &pub_key)
|
||||
.expect("boot should succeed");
|
||||
|
||||
assert_eq!(result.package2_size, 64);
|
||||
|
||||
// Read back the first word at the Package2 load address from core 0
|
||||
let core = state
|
||||
.cpu_manager
|
||||
.get_core(0)
|
||||
.expect("core 0 must exist");
|
||||
let first_word = core.read_u32(PACKAGE2_LOAD_ADDR);
|
||||
|
||||
assert_eq!(
|
||||
first_word, ARM64_NOP,
|
||||
"first word at 0x{PACKAGE2_LOAD_ADDR:08X} must be ARMv8 NOP"
|
||||
);
|
||||
|
||||
// Also verify the last word (offset 60 bytes = 15 * 4) is a NOP
|
||||
let last_word = core.read_u32(PACKAGE2_LOAD_ADDR + 60);
|
||||
assert_eq!(
|
||||
last_word, ARM64_NOP,
|
||||
"last word at offset 60 must also be ARMv8 NOP"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Bad signature should fail ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn bootrom_cpu_e2e_bad_signature_fails() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (pub_key, priv_key) = generate_test_keypair();
|
||||
|
||||
let mut fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
let fw_bytes_mut = fw.into_vec();
|
||||
|
||||
// Tamper with a byte in the signature region (first 256 bytes)
|
||||
let mut tampered = fw_bytes_mut;
|
||||
tampered[0] ^= 0xFF; // flip all bits of the first byte
|
||||
|
||||
let mut state = State::new();
|
||||
let err = state
|
||||
.boot_rom_with_key(&efuse, &tampered, &pub_key)
|
||||
.expect_err("boot must fail with a tampered signature");
|
||||
|
||||
assert!(
|
||||
matches!(err, BootError::SignatureVerify(_)),
|
||||
"expected SignatureVerify error, got: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Boot phase ordering is correct ────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn bootrom_cpu_e2e_phase_ordering() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
|
||||
let mut state = State::new();
|
||||
let result = state
|
||||
.boot_rom_with_key(&efuse, fw.as_bytes(), &pub_key)
|
||||
.expect("boot should succeed");
|
||||
|
||||
let phases = &result.diagnostics.phases_completed;
|
||||
assert_eq!(phases.len(), 7);
|
||||
assert_eq!(phases[0], BootPhase::EfuseInit);
|
||||
assert_eq!(phases[1], BootPhase::KeyDerivation);
|
||||
assert_eq!(phases[2], BootPhase::Pk11Parse);
|
||||
assert_eq!(phases[3], BootPhase::RsaVerify);
|
||||
assert_eq!(phases[4], BootPhase::CtrDecrypt);
|
||||
assert_eq!(phases[5], BootPhase::Pk11Validate);
|
||||
assert_eq!(phases[6], BootPhase::Package2Placement);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Memory map test — placeholder.
|
||||
//! Full memory layout verification deferred to later tasks (T02/T03).
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn placeholder_stub() {
|
||||
// This module is a placeholder — memory layout verification tests
|
||||
// will be added in T02/T03 when KernelInit places kernel objects
|
||||
// at known addresses in guest memory.
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ pub mod bootrom_cpu_e2e_test;
|
||||
pub mod bootrom_integration_test;
|
||||
pub mod memory_map_test;
|
||||
pub mod nca_integration_test;
|
||||
pub mod hipc_sm_test;
|
||||
pub mod firmware_builder;
|
||||
|
||||
pub use run::run_tests;
|
||||
pub use gpu_test::run_gpu_tests;
|
||||
|
||||
Reference in New Issue
Block a user