diff --git a/Cargo.lock b/Cargo.lock index 4df80a99005..7977733927d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4094,7 +4094,7 @@ version = "0.1.0" dependencies = [ "flate2", "toyos-manifest", - "toyos-sha2", + "toyos-sha2-hw", "toyos-tmpdir", ] @@ -5682,7 +5682,7 @@ dependencies = [ "toyos-manifest", "toyos-osrelease", "toyos-quiesce", - "toyos-sha2", + "toyos-sha2-hw", "toyos-ssh", "toyos-swap", "toyos-symbols", @@ -5938,6 +5938,13 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "toyos-sha2-hw" +version = "0.1.0" +dependencies = [ + "toyos-sha2", +] + [[package]] name = "toyos-smmu" version = "0.1.0" @@ -5958,7 +5965,7 @@ name = "toyos-swap" version = "0.1.0" dependencies = [ "toyos-manifest", - "toyos-sha2", + "toyos-sha2-hw", ] [[package]] @@ -6009,6 +6016,7 @@ version = "0.1.0" dependencies = [ "ed25519-dalek 2.2.0", "toyos-sha2", + "toyos-sha2-hw", "toyos-wallclock", ] @@ -6267,7 +6275,7 @@ dependencies = [ "diskserver", "toyos", "toyos-fat32", - "toyos-sha2", + "toyos-sha2-hw", "toyos-update", ] diff --git a/Cargo.toml b/Cargo.toml index f0da1b65531..078af6613a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ members = [ "toyos-random", "toyos-rootimage", "toyos-sha2", + "toyos-sha2-hw", "toyos-smmu", "toyos-ssh", "toyos-swap", @@ -213,7 +214,7 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] } # Every SHA-256 the build takes: `NOTICE`'s record of a committed file, a # store key, a ROOT's name, a release asset's digest, and the harness's of a # body a guest fetches over TLS. -toyos-sha2 = { path = "toyos-sha2" } +toyos-sha2-hw = { path = "toyos-sha2-hw" } # The toolchain release's tarball, packed in-process (`src/release.rs`): Rust's # own tar and gzip, where the binaries are hosts' tools. tar = { version = "0.4.46", default-features = false } diff --git a/NOTICE b/NOTICE index 891fa9109f7..d4bd1850233 100644 --- a/NOTICE +++ b/NOTICE @@ -451,10 +451,10 @@ tests/cavp/ — NIST's SHA-2 test vectors, CAVP SHAVS The response files NIST's Cryptographic Algorithm Validation Program publishes for the SHA Validation System: messages and the digests FIPS 180-4 gives them, -the external oracle `toyos-sha2`'s tests hold it to. A work of the United States -government, not under copyright in the United States (17 U.S.C. §105). Test -input only: nothing built from this repository carries them, and no shipped -package's directory holds them. +the external oracle `toyos-sha2`'s and `toyos-sha2-hw`'s tests hold them to. A +work of the United States government, not under copyright in the United States +(17 U.S.C. §105). Test input only: nothing built from this repository carries +them, and no shipped package's directory holds them. Of the archive, only SHA-256's and SHA-512's files, the two hashes the crate has. diff --git a/issues/no-t14-reading-of-updates-root-hash-before-and-after-sha-ni.md b/issues/no-t14-reading-of-updates-root-hash-before-and-after-sha-ni.md new file mode 100644 index 00000000000..c09872649d1 --- /dev/null +++ b/issues/no-t14-reading-of-updates-root-hash-before-and-after-sha-ni.md @@ -0,0 +1,20 @@ +--- +status: open +kind: tooling +opened: 2026-10-09 +--- + +# No T14 reading of `update`'s ROOT hash before and after SHA-NI + +`update`'s streamed ROOT hash (`userland/update/src/main.rs`, `stream_root`) +moved from `toyos-sha2`'s scalar compression to `toyos-sha2-hw`'s SHA-NI one, +and no machine has timed it on either. `update` reports no hash time, and no +metal row runs `update`. + +The loader's ROOT hash, the same function on the same CPU, is not this +reading: it runs on a soft-float UEFI target before ExitBootServices, and +`update` runs in userland under the ToyOS kernel, streaming a ROOT it also +writes. + +Exit: the T14's reading of `update`'s ROOT hash on one image, with +`toyos-sha2`'s scalar compression and with `toyos-sha2-hw`'s. diff --git a/issues/the-build-runs-host-tools-outside-rust-and-qemu.md b/issues/the-build-runs-host-tools-outside-rust-and-qemu.md index 9c96a5a6928..0d4eaa0282d 100644 --- a/issues/the-build-runs-host-tools-outside-rust-and-qemu.md +++ b/issues/the-build-runs-host-tools-outside-rust-and-qemu.md @@ -33,7 +33,7 @@ arrives and is not one. M4 and M5 are stages of `issues/toyos-builds-itself.md`. | `cc`, `c++`, `ar` and `xcrun` on a macOS host, Apple's Command Line Tools | what the Linux row's tools do, and rustc asks `xcrun` for the SDK on every host link that names no `SDKROOT` (`rust/compiler/rustc_codegen_ssa/src/back/apple.rs`), as `src/llvm.rs` does for the LLVM's key | refused: one host OS alone | M5: no host in the loop | | `diag/flash.sh` | the owner's flash of a stick by hand: `bash`, and the `stat`, `seq`, `tr`, `grep`, `cut` and `sync` it strings together | refused: shell of our own | `issues/the-owners-flash-script-runs-diskutil.md` | | `diskutil` and `plutil` | `diag/flash.sh`, and `diskutil` in the README's flashing steps | refused: one host OS alone | `issues/the-owners-flash-script-runs-diskutil.md` | -| `shasum`, a Perl script on macOS | `diag/flash.sh` hashes the image with it | refused: a Rust tool does it, `toyos-sha2`, which `src/release.rs` hashes with | `issues/the-owners-flash-script-runs-diskutil.md` | +| `shasum`, a Perl script on macOS | `diag/flash.sh` hashes the image with it | refused: a Rust tool does it, `toyos-sha2-hw`, which `src/release.rs` hashes with | `issues/the-owners-flash-script-runs-diskutil.md` | | `lsblk` | the README's Linux flashing steps find the stick with it | refused: one host OS alone | `issues/the-owners-flash-script-runs-diskutil.md` | | `dd` | `diag/flash.sh` and the README's flashing steps write the stick with it | refused: a Rust tool does it, the build system can write the image itself | `issues/the-owners-flash-script-runs-diskutil.md` | | `sync` | the README's Linux flashing steps flush the stick with it | refused: a Rust tool does it, the build system can flush what it writes | `issues/the-owners-flash-script-runs-diskutil.md` | diff --git a/issues/x86-64-hashing-runs-scalar-where-the-cpu-has-sha-instructions.md b/issues/x86-64-hashing-runs-scalar-where-the-cpu-has-sha-instructions.md deleted file mode 100644 index db910a40d39..00000000000 --- a/issues/x86-64-hashing-runs-scalar-where-the-cpu-has-sha-instructions.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -status: owner -kind: question -opened: 2026-10-09 ---- - -# x86-64 hashing runs scalar where the CPU has SHA instructions - -`toyos-sha2` is scalar on every target and forbids `unsafe`, so the callers -that took `sha2`'s x86 backend — SHA-NI for SHA-256 and AVX2 for SHA-512, -chosen by CPUID — lost it: `update`'s streamed ROOT hash, `pkg` and `swap` on -the T14, and the build's hashing (`src/cicache.rs`, `src/image.rs`'s -`root_uuid`, `src/sysroot.rs`) on x86-64 CI runners. The loader is not among -them: its target is soft-float and it was scalar before. - -The loss is bounded by the scalar time itself, which the T14 measured: the -loader on main at d6298c83e hashed the `testcases` image's 320,864,256-byte -ROOT with `sha2`'s soft backend in 5,343,577,520 counter ticks at 2,419,200,000 -Hz, 2.21 s (138.5 MiB/s), and read the same ROOT off the stick in 9.31 s. So -`update` gives up at most 2.2 s of a 306 MiB ROOT it also writes, and at most -0.4 s of the default image's 56 MiB one; the build's largest hash, every -tracked file for `cicache` (47,906,184 bytes at c8aad54b1), at most 0.33 s -at that rate. `update`'s own hash time on the T14 is unmeasured. - -An instruction path needs `core::arch` intrinsics, which are `unsafe`, in an -x86-64 module of the crate with a CPUID selector, for userland callers alone. - -The question: is up to 2.2 s of an update worth `unsafe` in the crate every -signed image is verified with? - -Exit: the owner's ruling; on a yes, the T14's `update` reading of its ROOT hash -before and after the instruction path lands. diff --git a/src/image.rs b/src/image.rs index 548528626ac..2f4407abd53 100644 --- a/src/image.rs +++ b/src/image.rs @@ -6,7 +6,6 @@ use std::path::Path; use bcachefs::{BlockBuf, BlockIO, BlockNum, Formatted, FsUuid, Superblock, VecBlockIO}; use crate::arch::Arch; -use toyos_sha2::Sha256; use toyos_fat32::{BlockAccess, Fat32, FatTime, IoError}; /// The image that goes on the ROOT partition, named by a UUID **derived, never @@ -135,7 +134,7 @@ fn format_root( /// the bytes, so no two entries can run together into an input a different /// split would also produce. fn root_uuid(files: &[&(String, Vec)], symlinks: &[&(String, String)]) -> FsUuid { - let mut hasher = Sha256::new(); + let mut hasher = toyos_sha2_hw::sha256(); let mut field = |bytes: &[u8]| { hasher.update((bytes.len() as u64).to_le_bytes()); hasher.update(bytes); @@ -1412,8 +1411,8 @@ mod tests { for (name, data) in &files { let read = fs.read_file(name).unwrap_or_else(|e| panic!("read {name}: {e:?}")); assert_eq!( - Sha256::digest(&read), - Sha256::digest(data), + toyos_sha2_hw::sha256_digest(&read), + toyos_sha2_hw::sha256_digest(data), "{name} reads back as {} bytes that are not the {} it was given", read.len(), data.len() diff --git a/src/release.rs b/src/release.rs index 3bd013310d0..e12d7c8afa7 100644 --- a/src/release.rs +++ b/src/release.rs @@ -24,7 +24,6 @@ use std::path::{Path, PathBuf}; use std::process::Command; use serde_json::Value; -use toyos_sha2::Sha256; use toyos_tmpdir::TempDir; use crate::buildlock::Keyed; @@ -83,7 +82,7 @@ pub(crate) fn named_key(text: &str) -> Option { } pub(crate) fn sha256_hex(bytes: &[u8]) -> String { - Sha256::digest(bytes).iter().map(|b| format!("{b:02x}")).collect() + toyos_sha2_hw::sha256_digest(bytes).iter().map(|b| format!("{b:02x}")).collect() } fn on_runner() -> bool { diff --git a/src/sourcegate.rs b/src/sourcegate.rs index aa98e730fe6..3513602cc47 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -277,7 +277,7 @@ fn host_files() -> Vec { /// `bytes` as lower-case hex SHA-256, the spelling `NOTICE` records. #[cfg(test)] fn digest(bytes: &[u8]) -> String { - toyos_sha2::Sha256::digest(bytes).iter().map(|b| format!("{b:02x}")).collect() + toyos_sha2_hw::sha256_digest(bytes).iter().map(|b| format!("{b:02x}")).collect() } /// The shapes of a value that identifies a machine or the network it is on, diff --git a/src/sysroot.rs b/src/sysroot.rs index 6f2111ed35d..6496892ca8d 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -46,8 +46,6 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; -use toyos_sha2::Sha256; - use crate::arch::Arch; use crate::buildlock::{self, Guard, Held, Keyed}; use crate::compiler::{self, Compiler}; @@ -246,7 +244,7 @@ fn hex(digest: &[u8]) -> String { /// The first 16 hex digits of the SHA-256 of `data`. pub(crate) fn short(data: &[u8]) -> String { - hex(&Sha256::digest(data))[..16].to_string() + hex(&toyos_sha2_hw::sha256_digest(data))[..16].to_string() } /// Every file under `dir` a build reads, sorted: no `target/` and no dotted @@ -310,7 +308,7 @@ pub(crate) fn tree_identity(base: &Path, paths: &[&str], links: Links) -> String let mut sources = Vec::new(); source_files(base, paths, links, &mut sources); sources.sort(); - let mut hasher = Sha256::new(); + let mut hasher = toyos_sha2_hw::sha256(); for (path, commit) in sources { hasher.update(path.strip_prefix(base).unwrap_or(&path).to_string_lossy().as_bytes()); hasher.update([0]); diff --git a/tests/toyos.rs b/tests/toyos.rs index 5dc34455148..d0bd577e26e 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -3611,7 +3611,7 @@ fn https_fetch() -> Result<(), String> { let trusted = Authority::new("ToyOS harness test authority"); let stranger = Authority::new("ToyOS harness authority nothing trusts"); let body = std::sync::Arc::new(https::body()); - let hex: String = toyos_sha2::Sha256::digest(body.as_slice()).iter().map(|b| format!("{b:02x}")).collect(); + let hex: String = toyos_sha2_hw::sha256_digest(body.as_slice()).iter().map(|b| format!("{b:02x}")).collect(); let want = format!("{JOB}: ok bytes={} sha256={hex}", body.len()); let fetched = Server::start(trusted.leaf(host), body.clone())?; let wrong_name = Server::start(trusted.leaf([192, 0, 2, 1].into()), body.clone())?; diff --git a/toyos-sha2-hw/Cargo.toml b/toyos-sha2-hw/Cargo.toml new file mode 100644 index 00000000000..6203d3a71b3 --- /dev/null +++ b/toyos-sha2-hw/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "toyos-sha2-hw" +description = "SHA-256 on the CPU's SHA instructions where CPUID offers them, toyos-sha2's scalar compression where it does not: what every SHA-256 the tree takes hashes with." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +# The buffering, the padding and the round constants, and the compression +# where the CPU has no instructions for it. +toyos-sha2 = { path = "../toyos-sha2" } diff --git a/toyos-sha2-hw/src/lib.rs b/toyos-sha2-hw/src/lib.rs new file mode 100644 index 00000000000..46e13737d42 --- /dev/null +++ b/toyos-sha2-hw/src/lib.rs @@ -0,0 +1,33 @@ +//! SHA-256 on the CPU's own instructions where it has them: x86-64's SHA +//! extensions, chosen by CPUID as each hash begins. Where the CPU has none, the +//! blocks are `toyos-sha2`'s scalar compression. Both compute FIPS 180-4 +//! §6.2.2, so which one ran decides how fast a digest came, never what it is. +//! +//! **AArch64 is scalar.** ToyOS has no AArch64 metal, so no gain from its SHA2 +//! instructions could be measured. + +#![cfg_attr(not(test), no_std)] + +use toyos_sha2::Sha256; + +#[cfg(target_arch = "x86_64")] +mod x86_64; + +#[cfg(all(test, target_arch = "x86_64"))] +mod tests; + +/// A SHA-256 hash on this CPU's fastest compression. +pub fn sha256() -> Sha256 { + #[cfg(target_arch = "x86_64")] + if let Some(compress) = x86_64::compress() { + return Sha256::with(compress); + } + Sha256::new() +} + +/// The SHA-256 digest of `bytes`, on this CPU's fastest compression. +pub fn sha256_digest(bytes: impl AsRef<[u8]>) -> [u8; 32] { + let mut hash = sha256(); + hash.update(bytes); + hash.finalize() +} diff --git a/toyos-sha2-hw/src/tests.rs b/toyos-sha2-hw/src/tests.rs new file mode 100644 index 00000000000..500c96b695f --- /dev/null +++ b/toyos-sha2-hw/src/tests.rs @@ -0,0 +1,62 @@ +//! The instruction path against NIST's CAVP SHA-256 response files and, over +//! every message length to 4096 bytes in random splits, against `toyos-sha2`'s +//! scalar compression, which those files and RustCrypto's `sha2` hold. + +use toyos_sha2::Sha256; + +#[path = "../../toyos-sha2/src/tests/shavs.rs"] +mod shavs; +use shavs::*; + +/// A hash on the instructions, which a host without them cannot test: it says +/// so rather than passing on the scalar path. +fn hashing() -> Sha256 { + let compress = super::x86_64::compress() + .expect("this host's CPU has no SHA extensions, so nothing here can test them"); + Sha256::with(compress) +} + +fn sha256(msg: &[u8]) -> Vec { + let mut hash = hashing(); + hash.update(msg); + hash.finalize().to_vec() +} + +fn sha256_bytewise(msg: &[u8]) -> Vec { + let mut hash = hashing(); + msg.iter().for_each(|b| hash.update([*b])); + hash.finalize().to_vec() +} + +#[test] +fn sha256_byte_vectors() { + byte_file("SHA256ShortMsg.rsp", sha256, sha256_bytewise); + byte_file("SHA256LongMsg.rsp", sha256, sha256_bytewise); +} + +#[test] +fn sha256_monte_carlo() { + monte_file("SHA256Monte.rsp", sha256); +} + +/// **The differential**: every length from empty to 4096 bytes, of random +/// bytes, digested whole and streamed in three random splits each. +#[test] +fn every_length_to_4096_in_random_splits_agrees_with_the_scalar_compression() { + let mut draws = Draws(0x5eed_70e0_5a2b_0002); + for len in 0..=4096usize { + let msg: Vec = (0..len).map(|_| draws.next() as u8).collect(); + let want = Sha256::digest(&msg); + assert_eq!(sha256(&msg), want, "{len} bytes"); + for _ in 0..3 { + let cuts = draws.splits(len); + let mut hash = hashing(); + let mut at = 0; + for cut in cuts.iter().copied().chain([len]) { + hash.update(&msg[at..cut]); + at = cut; + } + assert_eq!(hash.finalize(), want, "{len} bytes cut at {cuts:?}"); + } + } +} diff --git a/toyos-sha2-hw/src/x86_64.rs b/toyos-sha2-hw/src/x86_64.rs new file mode 100644 index 00000000000..344af61eeec --- /dev/null +++ b/toyos-sha2-hw/src/x86_64.rs @@ -0,0 +1,148 @@ +//! SHA-256's compression on the SHA extensions: `SHA256RNDS2` computes two +//! rounds over the working variables held as ABEF and CDGH, and `SHA256MSG1` +//! and `SHA256MSG2` the message schedule four words at a time (Intel SDM Vol. +//! 2B). +//! +//! **One `asm!` block saves every XMM register it uses and restores it before +//! it ends, and so declares none.** The loader's target is soft-float and has +//! no XMM register class to name a clobber in, and the UEFI calling convention +//! leaves XMM6–XMM15 the firmware's; every other target runs the same block. + +use core::arch::asm; +use core::arch::x86_64::{__cpuid, __cpuid_count}; + +use toyos_sha2::{Compress256, K256}; + +/// `K256` at an address the block reads it from. +static K: [u32; 64] = K256; + +/// `PSHUFB`'s control that reverses each 32-bit word's bytes: a block's +/// big-endian words into the lanes' little-endian ones. +static FLIP: [u8; 16] = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12]; + +/// The compression, where CPUID reports the SHA extensions and SSSE3, whose +/// `PSHUFB` and `PALIGNR` the block takes; nothing else hands out [`blocks`]. +pub fn compress() -> Option { + // SDM Vol. 2A, CPUID: leaf 7 sub-leaf 0's EBX bit 29 is SHA, leaf 1's ECX + // bit 9 is SSSE3, and leaf 0's EAX is the highest leaf there is. + let sha = __cpuid(0).eax >= 7 && __cpuid_count(7, 0).ebx & (1 << 29) != 0; + let ssse3 = __cpuid(1).ecx & (1 << 9) != 0; + (sha && ssse3).then_some(blocks as Compress256) +} + +/// `blocks` into `state`, four rounds to a `rounds4` and the hash value held +/// in XMM1 and XMM2 across the run. +fn blocks(state: &mut [u32; 8], blocks: &[[u8; 64]]) { + // The loop below runs at least once. + if blocks.is_empty() { + return; + } + let mut saved = [0u8; 11 * 16]; + // SAFETY: `compress` hands this function out only where CPUID reports SHA + // and SSSE3. The block reads `blocks` from its start to `end`, which is a + // whole number of 64-byte blocks past it, reads `state`, `K` and `FLIP`, + // writes `state` and `saved`, and leaves XMM0–XMM10 as it found them. + unsafe { + asm!( + "movdqu %xmm0, 0({saved})", + "movdqu %xmm1, 16({saved})", + "movdqu %xmm2, 32({saved})", + "movdqu %xmm3, 48({saved})", + "movdqu %xmm4, 64({saved})", + "movdqu %xmm5, 80({saved})", + "movdqu %xmm6, 96({saved})", + "movdqu %xmm7, 112({saved})", + "movdqu %xmm8, 128({saved})", + "movdqu %xmm9, 144({saved})", + "movdqu %xmm10, 160({saved})", + // `state` is DCBA and HGFE, low lane last; the instructions take + // ABEF in XMM1 and CDGH in XMM2. + "movdqu ({state}), %xmm1", + "movdqu 16({state}), %xmm2", + "movdqa %xmm1, %xmm7", + "punpcklqdq %xmm2, %xmm1", + "punpckhqdq %xmm7, %xmm2", + "pshufd $0x1b, %xmm1, %xmm1", + "pshufd $0xb1, %xmm2, %xmm2", + "movdqu ({flip}), %xmm8", + // `rounds4 i, m0, m1, m2, m3`: rounds `i` to `i + 3`, the + // schedule's four words for them in `m0` and the twelve before in + // `m1`–`m3`, oldest first. From round 4 `SHA256MSG1` starts the + // words sixteen rounds on, and from round 12 `PALIGNR` and + // `SHA256MSG2` finish the ones four rounds on, until the + // schedule's last word. + ".macro rounds4 i, m0, m1, m2, m3", + ".if \\i < 16", + "movdqu \\i*4({data}), \\m0", + "pshufb %xmm8, \\m0", + ".endif", + "movdqu \\i*4({k}), %xmm0", + "paddd \\m0, %xmm0", + "sha256rnds2 %xmm1, %xmm2", + ".if \\i >= 12 && \\i < 60", + "movdqa \\m0, %xmm7", + "palignr $4, \\m3, %xmm7", + "paddd %xmm7, \\m1", + "sha256msg2 \\m0, \\m1", + ".endif", + "punpckhqdq %xmm0, %xmm0", + "sha256rnds2 %xmm2, %xmm1", + ".if \\i >= 4 && \\i < 52", + "sha256msg1 \\m0, \\m3", + ".endif", + ".endm", + "2:", + "movdqa %xmm1, %xmm9", + "movdqa %xmm2, %xmm10", + // The schedule's sixteen words live in XMM3–XMM6, four to a + // register, each group of four rounds a rotation of them. + "rounds4 0, %xmm3, %xmm4, %xmm5, %xmm6", + "rounds4 4, %xmm4, %xmm5, %xmm6, %xmm3", + "rounds4 8, %xmm5, %xmm6, %xmm3, %xmm4", + "rounds4 12, %xmm6, %xmm3, %xmm4, %xmm5", + "rounds4 16, %xmm3, %xmm4, %xmm5, %xmm6", + "rounds4 20, %xmm4, %xmm5, %xmm6, %xmm3", + "rounds4 24, %xmm5, %xmm6, %xmm3, %xmm4", + "rounds4 28, %xmm6, %xmm3, %xmm4, %xmm5", + "rounds4 32, %xmm3, %xmm4, %xmm5, %xmm6", + "rounds4 36, %xmm4, %xmm5, %xmm6, %xmm3", + "rounds4 40, %xmm5, %xmm6, %xmm3, %xmm4", + "rounds4 44, %xmm6, %xmm3, %xmm4, %xmm5", + "rounds4 48, %xmm3, %xmm4, %xmm5, %xmm6", + "rounds4 52, %xmm4, %xmm5, %xmm6, %xmm3", + "rounds4 56, %xmm5, %xmm6, %xmm3, %xmm4", + "rounds4 60, %xmm6, %xmm3, %xmm4, %xmm5", + "paddd %xmm9, %xmm1", + "paddd %xmm10, %xmm2", + "add $64, {data}", + "cmp {end}, {data}", + "jne 2b", + ".purgem rounds4", + "movdqa %xmm1, %xmm7", + "punpcklqdq %xmm2, %xmm1", + "punpckhqdq %xmm7, %xmm2", + "pshufd $0xb1, %xmm1, %xmm1", + "pshufd $0x1b, %xmm2, %xmm2", + "movdqu %xmm2, ({state})", + "movdqu %xmm1, 16({state})", + "movdqu 0({saved}), %xmm0", + "movdqu 16({saved}), %xmm1", + "movdqu 32({saved}), %xmm2", + "movdqu 48({saved}), %xmm3", + "movdqu 64({saved}), %xmm4", + "movdqu 80({saved}), %xmm5", + "movdqu 96({saved}), %xmm6", + "movdqu 112({saved}), %xmm7", + "movdqu 128({saved}), %xmm8", + "movdqu 144({saved}), %xmm9", + "movdqu 160({saved}), %xmm10", + state = in(reg) state.as_mut_ptr(), + data = inout(reg) blocks.as_ptr() => _, + end = in(reg) blocks.as_ptr_range().end, + k = in(reg) K.as_ptr(), + flip = in(reg) FLIP.as_ptr(), + saved = in(reg) saved.as_mut_ptr(), + options(att_syntax, nostack), + ); + } +} diff --git a/toyos-sha2/Cargo.toml b/toyos-sha2/Cargo.toml index 5f9ea48a829..5c45a35c4b9 100644 --- a/toyos-sha2/Cargo.toml +++ b/toyos-sha2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toyos-sha2" -description = "SHA-256 and SHA-512 from FIPS 180-4, scalar: what the loader, update, swap, pkg and the build hash with; pure." +description = "SHA-256 and SHA-512 from FIPS 180-4, scalar, and the buffering and padding toyos-sha2-hw's instruction path compresses inside: pure." version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/toyos-sha2/src/lib.rs b/toyos-sha2/src/lib.rs index 3e85b56fa56..9158685fca2 100644 --- a/toyos-sha2/src/lib.rs +++ b/toyos-sha2/src/lib.rs @@ -7,10 +7,10 @@ //! message is the same however it was split. A message is whole bytes: FIPS //! 180-4 also hashes a trailing partial byte, and nothing here needs one. //! -//! **Scalar, on every target.** The loader runs as a UEFI application on a -//! soft-float target and may not assume the SIMD or SHA-extension registers -//! are its own, and an instruction path is `unsafe`, which this crate -//! forbids. +//! **Scalar, on every target.** An instruction path is `unsafe`, which this +//! crate forbids: `toyos-sha2-hw`'s compresses SHA-256's blocks on a CPU that +//! has the instructions, through [`Sha256::with`], and this crate buffers and +//! pads around it. //! //! [`update`]: Sha256::update //! [`finalize`]: Sha256::finalize @@ -21,9 +21,9 @@ #[cfg(test)] mod tests; -/// SHA-256's round constants, §4.2.2. +/// SHA-256's round constants, §4.2.2: an instruction path's too. #[rustfmt::skip] -const K256: [u32; 64] = [ +pub const K256: [u32; 64] = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, @@ -171,6 +171,9 @@ macro_rules! hash { $(#[$doc])* pub struct $name { state: [$word; 8], + /// What compresses whole blocks: this crate's, or the instruction + /// path [`Sha256::with`] was given. + compress: fn(&mut [$word; 8], &[[u8; $block]]), block: [u8; $block], /// How much of `block` holds message bytes not yet compressed. filled: usize, @@ -180,7 +183,7 @@ macro_rules! hash { impl $name { pub const fn new() -> Self { - Self { state: $init, block: [0; $block], filled: 0, bytes: 0 } + Self { state: $init, compress: $compress, block: [0; $block], filled: 0, bytes: 0 } } /// The digest of `message`. @@ -205,11 +208,11 @@ macro_rules! hash { if self.filled < $block { return; } - $compress(&mut self.state, core::slice::from_ref(&self.block)); + (self.compress)(&mut self.state, core::slice::from_ref(&self.block)); self.filled = 0; } let (blocks, rest) = bytes.as_chunks::<$block>(); - $compress(&mut self.state, blocks); + (self.compress)(&mut self.state, blocks); self.block[..rest.len()].copy_from_slice(rest); self.filled = rest.len(); } @@ -253,3 +256,15 @@ hash! { /// SHA-512, §6.4: a 64-byte digest. Sha512: u64, block 128, length 16, digest 64, H512, compress512 } + +/// What compresses whole blocks into SHA-256's hash value, as §6.2.2 does. +pub type Compress256 = fn(&mut [u32; 8], &[[u8; 64]]); + +impl Sha256 { + /// A hash whose blocks `compress` computes: an instruction path's, which + /// must give §6.2.2's hash value for every run of blocks, the empty one + /// among them. + pub const fn with(compress: Compress256) -> Self { + Self { compress, ..Self::new() } + } +} diff --git a/toyos-sha2/src/tests.rs b/toyos-sha2/src/tests.rs index 52db2f9438a..0120fdc4472 100644 --- a/toyos-sha2/src/tests.rs +++ b/toyos-sha2/src/tests.rs @@ -4,125 +4,8 @@ use super::*; -/// A response file under the repository's `tests/cavp/`. -fn rsp(name: &str) -> String { - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../tests/cavp") - .join(name); - std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) -} - -fn unhex(text: &str) -> Vec { - assert!( - text.len().is_multiple_of(2), - "{text:?} is not whole bytes of hex" - ); - (0..text.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex")) - .collect() -} - -/// Every `key = value` line of a response file, in order. -fn fields(text: &str) -> impl Iterator { - text.lines().filter_map(|line| { - line.trim() - .trim_start_matches('[') - .trim_end_matches(']') - .split_once(" = ") - }) -} - -/// A digest, held to the length its section's `[L = n]` header names. -fn md(name: &str, l: Option, value: &str) -> Vec { - let md = unhex(value); - assert_eq!( - Some(md.len()), - l, - "{name}: an MD that is not its section's L bytes" - ); - md -} - -/// A message file's vectors: its length in bits, its bytes and its digest. -fn messages(name: &str) -> Vec<(usize, Vec, Vec)> { - let text = rsp(name); - let mut out = Vec::new(); - let mut len = None; - let mut msg = None; - let mut l = None; - for (key, value) in fields(&text) { - match key { - "Len" => len = Some(value.parse().expect("a length")), - "Msg" => msg = Some(unhex(value)), - "L" => l = Some(value.parse().expect("a digest length")), - "MD" => out.push(( - len.take().expect("Len"), - msg.take().expect("Msg"), - md(name, l, value), - )), - _ => panic!("{name}: no field {key:?} in a message file"), - } - } - assert!(!out.is_empty(), "{name} holds no vectors"); - out -} - -/// A Monte Carlo file's seed and its hundred checkpoints. -fn monte(name: &str) -> (Vec, Vec>) { - let text = rsp(name); - let mut seed = None; - let mut want = Vec::new(); - let mut l = None; - for (key, value) in fields(&text) { - match key { - "Seed" => seed = Some(unhex(value)), - "COUNT" => assert_eq!( - value.parse::().expect("a count"), - want.len(), - "{name}" - ), - "L" => l = Some(value.parse().expect("a digest length")), - "MD" => want.push(md(name, l, value)), - _ => panic!("{name}: no field {key:?} in a Monte Carlo file"), - } - } - assert_eq!(want.len(), 100, "{name}"); - (seed.expect("Seed"), want) -} - -/// Every vector of a byte-oriented file, whole and streamed a byte at a time. -fn byte_file(name: &str, digest: impl Fn(&[u8]) -> Vec, streamed: impl Fn(&[u8]) -> Vec) { - for (len, msg, want) in messages(name) { - assert_eq!(len % 8, 0, "{name}: Len = {len} in a byte-oriented file"); - let msg = &msg[..len / 8]; - assert_eq!(hexed(&digest(msg)), hexed(&want), "{name}: Len = {len}"); - assert_eq!( - hexed(&streamed(msg)), - hexed(&want), - "{name}: Len = {len}, a byte at a time" - ); - } -} - -fn hexed(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{b:02x}")).collect() -} - -/// SHAVS §6.4's Monte Carlo test: each digest is of the three before it, and -/// every thousandth is a checkpoint and the next round's seed. -fn monte_file(name: &str, digest: impl Fn(&[u8]) -> Vec) { - let (mut seed, want) = monte(name); - for (count, want) in want.iter().enumerate() { - let mut md = [seed.clone(), seed.clone(), seed]; - for _ in 3..1003 { - let next = digest(&md.concat()); - md = [md[1].clone(), md[2].clone(), next]; - } - seed = md[2].clone(); - assert_eq!(hexed(&seed), hexed(want), "{name}: COUNT = {count}"); - } -} +mod shavs; +use shavs::*; fn sha256(msg: &[u8]) -> Vec { Sha256::digest(msg).to_vec() @@ -166,28 +49,6 @@ fn sha512_monte_carlo() { monte_file("SHA512Monte.rsp", sha512); } -/// SplitMix64: the splits' source, seeded so a red names a reproducible one. -struct Draws(u64); - -impl Draws { - fn next(&mut self) -> u64 { - self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15); - let mut z = self.0; - z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); - z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); - z ^ (z >> 31) - } - - /// Cut points that split `0..len` into random pieces, empty ones among them. - fn splits(&mut self, len: usize) -> Vec { - let mut cuts: Vec = (0..self.next() % 8) - .map(|_| (self.next() % (len as u64 + 1)) as usize) - .collect(); - cuts.sort_unstable(); - cuts - } -} - /// **The differential**: every length from empty to 4096 bytes, of random /// bytes, digested whole and streamed in three random splits each, against /// RustCrypto's `sha2` — which shares no code with this crate. diff --git a/toyos-sha2/src/tests/shavs.rs b/toyos-sha2/src/tests/shavs.rs new file mode 100644 index 00000000000..77c3c64ab50 --- /dev/null +++ b/toyos-sha2/src/tests/shavs.rs @@ -0,0 +1,145 @@ +//! SHAVS's response files (`tests/cavp/`) read and driven, and the seeded +//! draws a differential splits its messages with: this crate's tests', and +//! `toyos-sha2-hw`'s, which include this file by path. + +/// A response file under the repository's `tests/cavp/`. +fn rsp(name: &str) -> String { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../tests/cavp") + .join(name); + std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) +} + +fn unhex(text: &str) -> Vec { + assert!( + text.len().is_multiple_of(2), + "{text:?} is not whole bytes of hex" + ); + (0..text.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex")) + .collect() +} + +/// Every `key = value` line of a response file, in order. +fn fields(text: &str) -> impl Iterator { + text.lines().filter_map(|line| { + line.trim() + .trim_start_matches('[') + .trim_end_matches(']') + .split_once(" = ") + }) +} + +/// A digest, held to the length its section's `[L = n]` header names. +fn md(name: &str, l: Option, value: &str) -> Vec { + let md = unhex(value); + assert_eq!( + Some(md.len()), + l, + "{name}: an MD that is not its section's L bytes" + ); + md +} + +/// A message file's vectors: its length in bits, its bytes and its digest. +fn messages(name: &str) -> Vec<(usize, Vec, Vec)> { + let text = rsp(name); + let mut out = Vec::new(); + let mut len = None; + let mut msg = None; + let mut l = None; + for (key, value) in fields(&text) { + match key { + "Len" => len = Some(value.parse().expect("a length")), + "Msg" => msg = Some(unhex(value)), + "L" => l = Some(value.parse().expect("a digest length")), + "MD" => out.push(( + len.take().expect("Len"), + msg.take().expect("Msg"), + md(name, l, value), + )), + _ => panic!("{name}: no field {key:?} in a message file"), + } + } + assert!(!out.is_empty(), "{name} holds no vectors"); + out +} + +/// A Monte Carlo file's seed and its hundred checkpoints. +fn monte(name: &str) -> (Vec, Vec>) { + let text = rsp(name); + let mut seed = None; + let mut want = Vec::new(); + let mut l = None; + for (key, value) in fields(&text) { + match key { + "Seed" => seed = Some(unhex(value)), + "COUNT" => assert_eq!( + value.parse::().expect("a count"), + want.len(), + "{name}" + ), + "L" => l = Some(value.parse().expect("a digest length")), + "MD" => want.push(md(name, l, value)), + _ => panic!("{name}: no field {key:?} in a Monte Carlo file"), + } + } + assert_eq!(want.len(), 100, "{name}"); + (seed.expect("Seed"), want) +} + +/// Every vector of a byte-oriented file, whole and streamed a byte at a time. +pub fn byte_file(name: &str, digest: impl Fn(&[u8]) -> Vec, streamed: impl Fn(&[u8]) -> Vec) { + for (len, msg, want) in messages(name) { + assert_eq!(len % 8, 0, "{name}: Len = {len} in a byte-oriented file"); + let msg = &msg[..len / 8]; + assert_eq!(hexed(&digest(msg)), hexed(&want), "{name}: Len = {len}"); + assert_eq!( + hexed(&streamed(msg)), + hexed(&want), + "{name}: Len = {len}, a byte at a time" + ); + } +} + +fn hexed(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// SHAVS §6.4's Monte Carlo test: each digest is of the three before it, and +/// every thousandth is a checkpoint and the next round's seed. +pub fn monte_file(name: &str, digest: impl Fn(&[u8]) -> Vec) { + let (mut seed, want) = monte(name); + for (count, want) in want.iter().enumerate() { + let mut md = [seed.clone(), seed.clone(), seed]; + for _ in 3..1003 { + let next = digest(&md.concat()); + md = [md[1].clone(), md[2].clone(), next]; + } + seed = md[2].clone(); + assert_eq!(hexed(&seed), hexed(want), "{name}: COUNT = {count}"); + } +} + +/// SplitMix64: the splits' source, seeded so a red names a reproducible one. +pub struct Draws(pub u64); + +impl Draws { + pub fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z ^ (z >> 31) + } + + /// Cut points that split `0..len` into random pieces, empty ones among them. + pub fn splits(&mut self, len: usize) -> Vec { + let mut cuts: Vec = (0..self.next() % 8) + .map(|_| (self.next() % (len as u64 + 1)) as usize) + .collect(); + cuts.sort_unstable(); + cuts + } +} diff --git a/toyos-swap/Cargo.toml b/toyos-swap/Cargo.toml index 29637b81178..55be37c96ca 100644 --- a/toyos-swap/Cargo.toml +++ b/toyos-swap/Cargo.toml @@ -9,4 +9,4 @@ license = "MIT OR Apache-2.0" # The program-key bound a service name is held to, which is the manifest's. toyos-manifest = { path = "../toyos-manifest" } # The digest a swap is verified by. -toyos-sha2 = { path = "../toyos-sha2" } +toyos-sha2-hw = { path = "../toyos-sha2-hw" } diff --git a/toyos-swap/src/lib.rs b/toyos-swap/src/lib.rs index e42be4343f1..3b92967f7f1 100644 --- a/toyos-swap/src/lib.rs +++ b/toyos-swap/src/lib.rs @@ -33,8 +33,6 @@ #![forbid(unsafe_code)] -use toyos_sha2::Sha256; - /// The name the supervisor serves swap requests on — a `supervisor-serve` /// record — which the manifest names because its holder starts only in a login /// session. @@ -89,7 +87,7 @@ pub type Digest = [u8; 32]; /// The digest of `bytes`. pub fn digest(bytes: &[u8]) -> Digest { - Sha256::digest(bytes) + toyos_sha2_hw::sha256_digest(bytes) } pub fn hex(digest: &Digest) -> String { diff --git a/toyos-update/Cargo.toml b/toyos-update/Cargo.toml index 40c9130465d..82fe0776761 100644 --- a/toyos-update/Cargo.toml +++ b/toyos-update/Cargo.toml @@ -28,8 +28,10 @@ sign = [] # which refuses the small-order and non-canonical encodings RFC 8032 §5.1.7 # leaves to the implementation. ed25519-dalek = { version = "2.2", default-features = false } -# SHA-256 over the sections and SHA-512 over the signed header, as SSHSIG -# names it. +# SHA-512 over the signed header, as SSHSIG names it. toyos-sha2 = { path = "../toyos-sha2" } +# SHA-256 over the sections and a package archive, on the CPU's SHA +# instructions where it has them. +toyos-sha2-hw = { path = "../toyos-sha2-hw" } # The calendar a repository document's `expires` is written in, the tree's one. toyos-wallclock = { path = "../toyos-wallclock" } diff --git a/toyos-update/src/lib.rs b/toyos-update/src/lib.rs index 7d0adb25eda..3ede32a18fc 100644 --- a/toyos-update/src/lib.rs +++ b/toyos-update/src/lib.rs @@ -39,7 +39,7 @@ pub type Digest = [u8; 32]; /// The SHA-256 of `bytes`: the one definition the Mac, the loader and the /// updater share. pub fn sha256(bytes: &[u8]) -> Digest { - toyos_sha2::Sha256::digest(bytes) + toyos_sha2_hw::sha256_digest(bytes) } /// A digest in lowercase hex, into `out`. diff --git a/toyos-update/src/repo.rs b/toyos-update/src/repo.rs index 35adf1c2432..db0fd2274c6 100644 --- a/toyos-update/src/repo.rs +++ b/toyos-update/src/repo.rs @@ -654,7 +654,7 @@ pub struct Archive { impl Archive { pub fn of(item: &Item) -> Self { - Archive { length: item.length, sha256: item.sha256, seen: 0, hash: Sha256::new() } + Archive { length: item.length, sha256: item.sha256, seen: 0, hash: toyos_sha2_hw::sha256() } } /// The next bytes, refused where they run past the signed length. diff --git a/userland/pkg/Cargo.toml b/userland/pkg/Cargo.toml index 1698474b273..85d0663d0b5 100644 --- a/userland/pkg/Cargo.toml +++ b/userland/pkg/Cargo.toml @@ -13,7 +13,7 @@ doctest = false [dependencies] toyos-manifest = { path = "../../toyos-manifest" } -toyos-sha2 = { path = "../../toyos-sha2" } +toyos-sha2-hw = { path = "../../toyos-sha2-hw" } flate2 = { version = "1", default-features = false, features = ["rust_backend"] } [dev-dependencies] diff --git a/userland/pkg/src/main.rs b/userland/pkg/src/main.rs index 12010bd5bc7..e1dc2a89bed 100644 --- a/userland/pkg/src/main.rs +++ b/userland/pkg/src/main.rs @@ -15,7 +15,6 @@ use std::path::Path; use flate2::read::GzDecoder; use pkg::{archive, sums}; -use toyos_sha2::Sha256; use toyos_manifest::package::{self, Package}; /// Answers the consent prompt in advance, for a caller with no terminal. @@ -66,7 +65,7 @@ fn install(file: &Path, assume_yes: bool) -> Result<(), String> { let want = sums::digest_for(&sums_text, &name)?; let bytes = fs::read(file).map_err(|e| format!("pkg: cannot read {}: {e}", file.display()))?; - let got = sums::hex(&Sha256::digest(&bytes)); + let got = sums::hex(&toyos_sha2_hw::sha256_digest(&bytes)); if got != want { return Err(format!("pkg: {name} hashes to {got} and {SUMS} says {want}")); } diff --git a/userland/update/Cargo.toml b/userland/update/Cargo.toml index d6ebd3b4bbb..edc11280a9b 100644 --- a/userland/update/Cargo.toml +++ b/userland/update/Cargo.toml @@ -15,7 +15,7 @@ toyos-update = { path = "../../toyos-update" } # A slot's volume is FAT, written with the driver the kernel mounts FAT with. toyos-fat32 = { path = "../../toyos-fat32" } # ROOT is hashed as it streams onto its partition, never held whole. -toyos-sha2 = { path = "../../toyos-sha2" } +toyos-sha2-hw = { path = "../../toyos-sha2-hw" } [package.metadata.toyos.host] exempt.manages = "ToyOS's boot slots: it writes the idle one, onto the partitions the supervisor grants it" diff --git a/userland/update/src/main.rs b/userland/update/src/main.rs index 4d6e996de40..e96bcef535f 100644 --- a/userland/update/src/main.rs +++ b/userland/update/src/main.rs @@ -170,7 +170,7 @@ const STREAM_BLOCKS: usize = 32; /// ROOT onto the idle ROOT partition as it arrives, [`STREAM_BLOCKS`] at a /// time, and held to `sha256` once whole. fn stream_root(input: &mut impl Read, root: &mut dyn Disk, len: u64, sha256: toyos_update::Digest) -> Result<(), String> { - let mut hasher = toyos_sha2::Sha256::new(); + let mut hasher = toyos_sha2_hw::sha256(); let mut run = vec![0u8; STREAM_BLOCKS * BLOCK]; let blocks = len / BLOCK as u64; let mut at = 0u64;