diff --git a/Cargo.lock b/Cargo.lock index 8ff8ce3211a..23508649e0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3015,9 +3015,8 @@ dependencies = [ "flate2", "tar", "toyos", + "toyos-crypto", "toyos-manifest", - "toyos-sha2", - "toyos-sha2-hw", "toyos-tmpdir", ] @@ -4438,6 +4437,7 @@ dependencies = [ "toml", "toyos-abi", "toyos-blackbox", + "toyos-crypto", "toyos-elf", "toyos-fat32", "toyos-fat32-check", @@ -4453,7 +4453,6 @@ dependencies = [ "toyos-osrelease", "toyos-parity", "toyos-quiesce", - "toyos-sha2-hw", "toyos-ssh", "toyos-swap", "toyos-symbols", @@ -4592,7 +4591,7 @@ name = "toyos-icon" version = "0.1.0" dependencies = [ "libm", - "toyos-sha2-hw", + "toyos-crypto", ] [[package]] @@ -4809,20 +4808,6 @@ version = "0.1.0" name = "toyos-scsi" version = "0.1.0" -[[package]] -name = "toyos-sha2" -version = "0.1.0" -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" @@ -4858,8 +4843,8 @@ dependencies = [ name = "toyos-swap" version = "0.1.0" dependencies = [ + "toyos-crypto", "toyos-manifest", - "toyos-sha2-hw", ] [[package]] @@ -4897,8 +4882,7 @@ name = "toyos-update" version = "0.1.0" dependencies = [ "ed25519-dalek 2.2.0", - "toyos-sha2", - "toyos-sha2-hw", + "toyos-crypto", "toyos-wallclock", ] @@ -5102,9 +5086,9 @@ version = "0.1.0" dependencies = [ "diskserver", "toyos", + "toyos-crypto", "toyos-disk", "toyos-fat32", - "toyos-sha2-hw", "toyos-update", ] @@ -5112,10 +5096,10 @@ dependencies = [ name = "update-copies" version = "0.1.0" dependencies = [ + "toyos-crypto", "toyos-disk", "toyos-fat32", "toyos-fat32-check", - "toyos-sha2-hw", "toyos-update", ] diff --git a/Cargo.toml b/Cargo.toml index 2fe5f857e0c..5f946a9f75e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,8 +64,6 @@ members = [ "lib/random", "lib/rootimage", "lib/scsi", - "lib/sha2", - "lib/sha2-hw", "lib/swap", "lib/symbols", "lib/tco", @@ -267,9 +265,8 @@ toyos-ssh = { path = "build/ssh" } # `classify`, so the host reading a log volume and `logkeeper` writing it cannot # disagree about which files on it are `logkeeper`'s. toyos-wallclock = { path = "lib/wallclock" } -# Every SHA-256 the build takes: `NOTICE`'s record of a committed file, a -# store key, a ROOT's name and a release asset's digest. -toyos-sha2-hw = { path = "lib/sha2-hw" } +# Every SHA-256 the build takes (`toyos_build::sha256`). +toyos-crypto = { path = "lib/crypto" } # The toolchain release's tarball, packed in-process (`build/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 9a763a81d8c..750b6dacfa6 100644 --- a/NOTICE +++ b/NOTICE @@ -474,8 +474,8 @@ image carries them. These are every file of that directory for an interface the crate offers. -lib/crypto/tests/data/nist-*.json — NIST ACVP HMAC and ML-KEM vectors, NIST-Software ------------------------------------------------------------------------------------- +tests/acvp/nist-*.json — NIST ACVP HMAC and ML-KEM vectors, NIST-Software +------------------------------------------------------------------------- nist-hmac-sha1.json sha256 6e96eb5f53065219c437a99fedec7299c916c76e170d6b74507dc5409ed9cd6b HMAC-SHA-1-2.0/internalProjection.json @@ -491,7 +491,7 @@ lib/crypto/tests/data/nist-*.json — NIST ACVP HMAC and ML-KEM vectors, NIST-So ML-KEM-keyGen-FIPS203/internalProjection.json Upstream: https://github.com/usnistgov/ACVP-Server, `gen-val/json-files/` at commit 975de31eb83d87039ec88934fdc47d8c312b892d, byte for byte - Licence text: lib/crypto/tests/data/nist-LICENSE (the `License` section of upstream's + Licence text: tests/acvp/nist-LICENSE (the `License` section of upstream's `README.md` at that commit, sha256 8e6833bd6bd35dadd415147ba53a8eca4c3d31c53cb589e97abba6a4efd5b953) SPDX-License-Identifier: NIST-Software @@ -618,9 +618,8 @@ tests/cavp/SHA*.rsp — 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, `toyos-sha2-hw`'s and `toyos-crypto`'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 +the external oracle `toyos-crypto`'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, SHA-384's and SHA-512's files, the hashes diff --git a/build/cicache.rs b/build/cicache.rs index 1b84e32a9e6..25e3da79cd7 100644 --- a/build/cicache.rs +++ b/build/cicache.rs @@ -373,7 +373,7 @@ fn sources(root: &Path) -> Result { continue; } let bytes = fs::read(&file).map_err(|e| format!("read {path}: {e}"))?; - sources.insert(path, crate::release::sha256_hex(&bytes)); + sources.insert(path, crate::sha256_hex(&bytes)); } Ok(sources) } diff --git a/build/hostedclang.rs b/build/hostedclang.rs index 539dd8aafb4..3cf2bbe1534 100644 --- a/build/hostedclang.rs +++ b/build/hostedclang.rs @@ -189,7 +189,7 @@ fn pack(product: &Path, sysroot: &Path, licence: &Path, version: &str, into: &Pa let packed = tar.into_inner().and_then(|gzip| gzip.finish()).and_then(|mut file| file.flush()); packed.unwrap_or_else(|e| panic!("{}: {e}", archive.display())); - let digest: String = toyos_sha2_hw::sha256_digest(read(&archive)).iter().map(|b| format!("{b:02x}")).collect(); + let digest = crate::sha256_hex(&read(&archive)); let sums = into.join("SHA256SUMS"); fs::write(&sums, format!("{digest} {name}\n")).unwrap_or_else(|e| panic!("write {}: {e}", sums.display())); Packaged { archive, sums } diff --git a/build/image.rs b/build/image.rs index e6d563fef73..3c44d0af849 100644 --- a/build/image.rs +++ b/build/image.rs @@ -134,10 +134,10 @@ 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 = toyos_sha2_hw::sha256(); + let mut hasher = toyos_crypto::hash::Sha256::new(); let mut field = |bytes: &[u8]| { - hasher.update((bytes.len() as u64).to_le_bytes()); - hasher.update(bytes); + hasher.update(&(bytes.len() as u64).to_le_bytes()).expect(crate::SHA256_BOUND); + hasher.update(bytes).expect(crate::SHA256_BOUND); }; for (name, data) in files { field(name.as_bytes()); @@ -1362,8 +1362,8 @@ mod tests { for (name, data) in &files { let read = fs.read_file(name).unwrap_or_else(|e| panic!("read {name}: {e:?}")); assert_eq!( - toyos_sha2_hw::sha256_digest(&read), - toyos_sha2_hw::sha256_digest(data), + crate::sha256(&read), + crate::sha256(data), "{name} reads back as {} bytes that are not the {} it was given", read.len(), data.len() diff --git a/build/lib.rs b/build/lib.rs index da6903e326c..e3dc4360e14 100644 --- a/build/lib.rs +++ b/build/lib.rs @@ -57,6 +57,21 @@ pub mod wallpaper; use std::path::{Path, PathBuf}; use std::process::Command; +/// The SHA-256 every digest the build takes is: `NOTICE`'s record of a +/// committed file, a store key, a ROOT's name and a release asset's digest. +pub fn sha256(bytes: &[u8]) -> [u8; 32] { + toyos_crypto::hash::Sha256::digest(bytes).expect(SHA256_BOUND) +} + +/// [`sha256`] in lower-case hex, the spelling `NOTICE` and `SHA256SUMS` take. +pub fn sha256_hex(bytes: &[u8]) -> String { + sha256(bytes).iter().map(|b| format!("{b:02x}")).collect() +} + +/// Why a build hash cannot refuse: FIPS 180-4 bounds a message at 2^61 bytes, +/// which nothing the build reads nears. +pub const SHA256_BOUND: &str = "a build input is far below SHA-256's 2^61-byte bound"; + /// The `.git` directory every worktree of this repository shares. /// /// `git rev-parse --git-common-dir` answers relatively from the primary diff --git a/build/licence.rs b/build/licence.rs index e46485a3046..2cc6faf9aa2 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -901,37 +901,37 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ Terms::Spdx("Apache-2.0"), ), ( - "lib/crypto/tests/data/nist-hmac-sha1.json", + "tests/acvp/nist-hmac-sha1.json", "6e96eb5f53065219c437a99fedec7299c916c76e170d6b74507dc5409ed9cd6b", "NOTICE", Terms::Spdx("NIST-Software"), ), ( - "lib/crypto/tests/data/nist-hmac-sha256.json", + "tests/acvp/nist-hmac-sha256.json", "a9d0734b93aee71bc6f30b47daab782fd96b39688afe4fbddb009e99402ec989", "NOTICE", Terms::Spdx("NIST-Software"), ), ( - "lib/crypto/tests/data/nist-hmac-sha384.json", + "tests/acvp/nist-hmac-sha384.json", "0ffa9332b9430ae94cfac0f23b3843293903fa34a3ac9c11a8af2bfdf7276ed3", "NOTICE", Terms::Spdx("NIST-Software"), ), ( - "lib/crypto/tests/data/nist-hmac-sha512.json", + "tests/acvp/nist-hmac-sha512.json", "ea6e03f012d5237b95421b7d50c504f8ab010c4f7f18cd46fe61d8912d3442a0", "NOTICE", Terms::Spdx("NIST-Software"), ), ( - "lib/crypto/tests/data/nist-mlkem-encapdecap.json", + "tests/acvp/nist-mlkem-encapdecap.json", "5e88fbd351dc2915b2ab399f5dffd269e1cdbdd0186103ab0e90b374cf592187", "NOTICE", Terms::Spdx("NIST-Software"), ), ( - "lib/crypto/tests/data/nist-mlkem-keygen.json", + "tests/acvp/nist-mlkem-keygen.json", "d7a62a2c3476957f56dd8d24f9004ea6776ccfe995ffe71a65bb9506dc9c7b1b", "NOTICE", Terms::Spdx("NIST-Software"), diff --git a/build/release.rs b/build/release.rs index e2b841d4561..497608ca296 100644 --- a/build/release.rs +++ b/build/release.rs @@ -113,9 +113,7 @@ pub(crate) fn named_key(text: &str) -> Option { text.lines().next()?.strip_prefix("toolchain ")?.strip_prefix(TAG).and_then(Key::parse) } -pub(crate) fn sha256_hex(bytes: &[u8]) -> String { - toyos_sha2_hw::sha256_digest(bytes).iter().map(|b| format!("{b:02x}")).collect() -} +use crate::sha256_hex; fn on_runner() -> bool { std::env::var("GITHUB_ACTIONS").is_ok_and(|v| v == "true") diff --git a/build/sdkversion.rs b/build/sdkversion.rs index 80ad33b2004..64499f2b100 100644 --- a/build/sdkversion.rs +++ b/build/sdkversion.rs @@ -78,7 +78,7 @@ fn plan_of( // A requirement's build metadata is ignored, and cargo warns of it. spec.insert("version".into(), dep.version.split('+').next().unwrap_or(&dep.version).into()); } - let key = crate::release::sha256_hex(hashed.as_bytes())[..16].to_string(); + let key = crate::sha256_hex(hashed.as_bytes())[..16].to_string(); let (version, publish) = assign(&index(krate.name)?, &key)?; let package = manifest.get_mut("package").and_then(|p| p.as_table_mut()).ok_or("no [package]")?; package.insert("version".into(), version.clone().into()); diff --git a/build/sourcegate.rs b/build/sourcegate.rs index 9b1ef3a5c81..3e4b8419252 100644 --- a/build/sourcegate.rs +++ b/build/sourcegate.rs @@ -290,7 +290,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_hw::sha256_digest(bytes).iter().map(|b| format!("{b:02x}")).collect() + crate::sha256_hex(bytes) } /// The shapes of a value that identifies a machine or the network it is on, diff --git a/build/sysroot.rs b/build/sysroot.rs index 6d01b5a01d6..ee3d762c0ab 100644 --- a/build/sysroot.rs +++ b/build/sysroot.rs @@ -311,7 +311,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(&toyos_sha2_hw::sha256_digest(data))[..16].to_string() + hex(&crate::sha256(data))[..16].to_string() } /// Every file under `dir` a build reads, sorted: no `target/` and no dotted @@ -375,18 +375,19 @@ 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 = toyos_sha2_hw::sha256(); + let mut hasher = toyos_crypto::hash::Sha256::new(); + let mut put = |bytes: &[u8]| hasher.update(bytes).expect(crate::SHA256_BOUND); for (path, commit) in sources { - hasher.update(path.strip_prefix(base).unwrap_or(&path).to_string_lossy().as_bytes()); - hasher.update([0]); + put(path.strip_prefix(base).unwrap_or(&path).to_string_lossy().as_bytes()); + put(&[0]); match commit { - Some(commit) => hasher.update(commit.as_bytes()), + Some(commit) => put(commit.as_bytes()), None => { let data = fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); - hasher.update(&*identity::of(&path, &data)); + put(&identity::of(&path, &data)); } } - hasher.update([0]); + put(&[0]); } hex(&hasher.finalize())[..16].to_string() } diff --git a/build/wallpaper.rs b/build/wallpaper.rs index 3b0b52b84da..c3416d8b3d9 100644 --- a/build/wallpaper.rs +++ b/build/wallpaper.rs @@ -375,7 +375,7 @@ mod tests { /// point answers differently reds here rather than naming a different ROOT. #[test] fn the_wallpaper_is_the_same_on_every_host() { - let digest: String = toyos_sha2_hw::sha256_digest(rgb()).iter().map(|b| format!("{b:02x}")).collect(); + let digest = crate::sha256_hex(&rgb()); assert_eq!(digest, "675f4fbc92985dc5e60b899fd869fa8e574b1147e2b1c9b42808a72bcfff1d91"); } 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 index 11b6b80ed3c..304a58251d1 100644 --- 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 @@ -6,9 +6,10 @@ opened: 2026-10-09 # No T14 reading of `update`'s ROOT hash before and after SHA-NI -`update`'s streamed ROOT hash (`system/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 +`update`'s streamed ROOT hash (`system/update/src/install/mod.rs`, +`stream_root`) runs on `toyos-crypto`'s SHA-NI compression, which replaced +`toyos-sha2`'s scalar one and then `toyos-sha2-hw`'s SHA-NI one, and no +machine has timed it on any of them. `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 @@ -17,4 +18,5 @@ reading: it runs on a soft-float UEFI target before ExitBootServices, and 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. +`toyos-crypto`'s portable rounds (its `portable` feature) and with its SHA-NI +compression. diff --git a/issues/the-tree-says-who-uses-each-thing.md b/issues/the-tree-says-who-uses-each-thing.md index 116962016ea..d1d000e9656 100644 --- a/issues/the-tree-says-who-uses-each-thing.md +++ b/issues/the-tree-says-who-uses-each-thing.md @@ -174,9 +174,9 @@ Where this differs from the tree the owner adopted: sshserver serves on russh (rule step 4). - `tests/metal/` is the metal loop. A machine's records are in the host's store since #852, so there is no `tests/machines/t14/` and no descriptor. -- `tests/cavp/` stays: `lib/sha2`'s SHAVS driver and `toyos-crypto`'s - `tests/nist.rs` read it, and `lib/sha2-hw`'s tests include that driver. The orchestrator ruled it shared test data, - which inside `lib/sha2` the licence gate would count as shipping. +- `tests/cavp/` stays: `toyos-crypto`'s `tests/nist.rs` reads it. It was + ruled shared test data, which inside a crate's directory the licence gate + would count as shipping. - There is no `tests/judges/ssh/`: no SSH judge is in the tree. ## Stages diff --git a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md index 7497b316bbe..0abb535a765 100644 --- a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md +++ b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md @@ -45,10 +45,15 @@ review has been done, and passing vectors does not show it is safer than ring. and 0x7 on the AMD bench (AVX). The parity instrument records what feature detection admits there, AVX2 and AES-NI on both benches and VAES/AVX-512 on the T14, so both crates' vector paths are open. -- **AArch64 is scalar.** No ARM bench exists to measure an ARM path, so the - crate's AArch64 AES and PMULL paths are not imported. They stay in the - archive until an ARM bench measures them. `toyos-sha2-hw` follows the same - rule. +- **AArch64 is scalar but for SHA-2.** No ARM bench exists to measure an ARM + path, so the crate's AArch64 AES and PMULL paths are not imported. They + stay in the archive until an ARM bench measures them. SHA-256 and SHA-512 + run on the Armv8 SHA instructions where the target promises them, by the + owner's word (2026-10-22): the development Mac's, where the build hashes + every image. There (exploratory, an M4 Pro) SHA-256 of 1 MiB runs at + 2.6 GB/s against the portable rounds' 0.52, and SHA-512 at 1.36 against + 0.83. ToyOS's own AArch64 targets do not promise them and nothing detects + them at run time, so they hash portably; see the weaknesses. ## Consumers and what each needs @@ -57,8 +62,8 @@ review has been done, and passing vectors does not show it is safer than ring. | `toyos-ssh`'s SSH protocol (`build/ssh`: `kex`, the server, the client). Host-only until `issues/the-ssh-server-is-toyos-own-and-russh-is-gone.md` stage 2 | the crate, since stage 1 | X25519, ML-KEM-768 and its hybrid, Ed25519 sign/verify, `chacha20-poly1305@openssh.com`, SHA-256/512, and a caller-supplied `Random` for replays | | `toyos-ssh`'s `hostkey` on the signing path. `build/signing.rs` reads the owner's image-signing key through `openssh_seed` and writes a minted one through `openssh_private`; both derive the public key from the seed | `toyos_update::sig::public_of`, since stage 1 | nothing: this derivation is the root of trust's; the verifier itself switches in stage 6 | | `system/sshserver` | russh with the RustCrypto stack, including `rsa` (RUSTSEC-2023-0071) | none directly; it reaches the crate through `toyos-ssh` | -| Image and package signing: `lib/update` (the loader, `/system/bin/update`, `build/signing.rs`, `build/root`, #892's repository keys) | `ed25519-dalek` `verify_strict`; `toyos-sha2` SHA-512; the loader's `sha2` with `force-soft` | strict Ed25519 verification in the soft-float UEFI loader, and signing behind `sign` on the host. The track's last stage, stage 6 (owner, 2026-10-13) | -| Every SHA-256: the build, `pkg`, `lib/swap`, `system/update` | `toyos-sha2` / `toyos-sha2-hw` (SHA-NI on x86-64, scalar on AArch64) | one SHA-2 implementation, no slower than `toyos-sha2-hw` | +| Image and package signing: `lib/update` (the loader, `/system/bin/update`, `build/signing.rs`, `build/root`, #892's repository keys) | `ed25519-dalek` `verify_strict`, which hashes with the loader's `sha2` under `force-soft`; the crate's SHA-512 over the signed header, since #1016 | strict Ed25519 verification in the soft-float UEFI loader, and signing behind `sign` on the host. The track's last stage, stage 6 (owner, 2026-10-13) | +| Every SHA-256: the build, `pkg`, `lib/swap`, `system/update`, `lib/update` and the loader | the crate, since #1016: SHA-NI on x86-64, the UEFI loader's in a block that saves the XMM registers it uses; the Armv8 SHA instructions where the target promises them. No slower than `toyos-sha2-hw`, which it replaces, on either bench: on the T14 4.6 times as fast at 64 B and 9 and 6 percent faster at 16 KiB and 1 MiB, on the AMD bench 2.8 times at 64 B, 2 percent at 16 KiB and equal at 1 MiB (#1016's evidence) | nothing | | ToyOS's own HTTPS clients. On the host: the build's `ureq` (`build/release.rs`) and doom's build script (`apps/doom/build.rs`). In a guest, none today: `https_tls13` and `pkg install ` are stages 3 and 5 of `issues/the-internet-clients-work-unchanged.md` | rustls on `ring`, which is `ToyOSOrg/ring` in a guest | the rustls provider through ureq's `rustls-no-provider` feature. Stage 5, once parity and security hold (owner, 2026-10-13) | | Wi-Fi's `wlan` (`issues/there-is-no-wifi.md` P6, W2) | nothing yet | PBKDF2-HMAC-SHA1, the 802.11i PRF and RFC 3394 unwrap, which the crate has. AES-CMAC-128, SAE over P-256 by hash-to-element, a public constant-time comparison and a public wiping secret, which it lacks | | `issues/programs-hold-aes-keys-as-key-locker-handles.md` | blocked on a session-long AES key | a TLS client on the crate is that consumer: AES behind a Key Locker handle. SSH holds no AES key | @@ -404,7 +409,9 @@ Not consumers: - Negative controls: 15 checked patches, each red by its named test, two only after a test was added for it. - Miri over every `portable` unit test but the provider's (ring is - C): 25 pass and ML-KEM's exhaustive reduction is ignored under it; + C), with `MIRIFLAGS=-Zmiri-disable-isolation` since #1016 moved + NIST's ML-KEM keygen vectors to `tests/acvp/`, whose file the test + reads: 25 pass and ML-KEM's exhaustive reduction is ignored under it; the heavy tests take a spread of their inputs there. A patch that writes past `erase`'s array is caught. - Evidence: `ToyOSOrg/evidence` `pr-993`, @@ -451,9 +458,9 @@ Not consumers: its primitives. Parity for those primitives is a later stage, not a gate. - Each consumer's old dependency is deleted in the same landing. - - The NIST ML-KEM vectors in `lib/crypto/tests/data` are under - `NIST-Software`, which the licence allowlist (`build/licence.rs`) does - not name. It decides that licence before `lib/crypto` enters an image. + - NIST's ACVP vectors are under `NIST-Software`, which the licence + allowlist (`build/licence.rs`) does not name. They are test input in + `tests/acvp/`, outside the crate's directory, so no image ships them. The consumers: - `toyos-ssh`'s protocol offers `mlkem768x25519-sha256` first, at any time @@ -461,13 +468,12 @@ Not consumers: (`issues/toyos-ssh-offers-only-a-classical-key-exchange.md`). sshserver's switch, stage 2 of the SSH track, is the shipping step. It waits for stage 4 on SSH's primitives. - - The crate's SHA-2 gains x86-64's SHA instructions; AArch64 stays scalar. - These users of `toyos-sha2` and `toyos-sha2-hw` switch to the crate: - `build/`'s digests, `pkg` and `lib/swap`. Three keep them until stage 6: - - `lib/update`; - - the loader; - - `system/update`, whose `stream_root` holds ROOT to the signed header's - digest. + - Every SHA-2 is the crate's since #1016 (owner, 2026-10-22: "use our + own crypto where possible"): `build/`'s digests, `pkg`, `lib/swap`, + `sdk/icon`'s test, `lib/update`, the loader and `system/update`. + `toyos-sha2` and `toyos-sha2-hw` are deleted; the crate's SHAVS tests + replace theirs. NIST's ACVP vectors moved to `tests/acvp/`, outside the + crate's directory, once the crate entered an image. - ToyOS's own HTTPS clients use the crate's rustls provider once stages 3 and 4 hold for TLS's primitives. The owner ruled this on 2026-10-13: the crypto goal overrides his ruling of 2026-10-02 in @@ -495,8 +501,6 @@ Not consumers: its verification time measured on each. - An AArch64 QEMU loader boot verifies a signed image, because the crate's UEFI path has never run. - - `lib/update`, the loader and `system/update` hash with the crate's SHA-2. `toyos-sha2` - and `toyos-sha2-hw` are deleted, and their SHAVS tests move to the crate. - `ed25519-dalek` and the loader's `sha2` leave `Cargo.lock`'s non-dev packages. @@ -510,6 +514,11 @@ Not consumers: per-temporary erasure on the T14, 0.79–0.88 on the AMD bench) and a reviewed contract. Compiler copies, registers, spills and caller-held bytes are not wiped. +- ToyOS's AArch64 targets hash with the portable SHA-2 rounds: no target + promises the Armv8 SHA instructions, and neither the kernel nor the crate + detects them at run time. **Owner**: this track. **Exit**: the crate takes + them where a run-time word from the kernel says the CPU has them, measured + on an ARM bench. - **Owner**: the orchestrator. **Exit**: stage 4 states what the shipped targets keep, and a consumer holding a long-lived key stores it under a wiping owner or a Key Locker handle. diff --git a/lib/crypto/src/arch/aarch64.rs b/lib/crypto/src/arch/aarch64.rs new file mode 100644 index 00000000000..6cec2e96993 --- /dev/null +++ b/lib/crypto/src/arch/aarch64.rs @@ -0,0 +1,126 @@ +//! SHA-256 and SHA-512 on the Armv8 Cryptographic Extension, where the target +//! guarantees it at compile time (`sha2`: FEAT_SHA256; `sha3`: FEAT_SHA512, as +//! Rust names them). No AArch64 target ToyOS runs on detects them at run time +//! yet, so a target that does not promise them takes the portable rounds. +//! +//! The instructions' semantics are the Arm Architecture Reference Manual's +//! (DDI 0487), SHA256H, SHA256H2, SHA256SU0, SHA256SU1, SHA512H, SHA512H2, +//! SHA512SU0 and SHA512SU1. No address or branch depends on the message. + +#![allow(clippy::needless_range_loop, reason = "the rounds index fixed-size arrays")] + +#[cfg(any(target_feature = "sha2", target_feature = "sha3"))] +use core::arch::aarch64::*; + +#[cfg(target_feature = "sha2")] +pub(super) fn sha256_blocks(state: &mut [u32; 8], blocks: &[[u8; 64]]) -> bool { + if cfg!(feature = "portable") { + return false; + } + // SAFETY: the target guarantees FEAT_SHA256. + unsafe { sha256(state, blocks) } + true +} + +#[cfg(target_feature = "sha2")] +#[target_feature(enable = "neon,sha2")] +fn sha256(state: &mut [u32; 8], blocks: &[[u8; 64]]) { + let k: [uint32x4_t; 16] = core::array::from_fn(|i| { + // SAFETY: four plain words. + unsafe { vld1q_u32(crate::hash::K256[4 * i..].as_ptr()) } + }); + // SAFETY: `state` holds eight plain words. + let (mut abcd, mut efgh) = unsafe { (vld1q_u32(state.as_ptr()), vld1q_u32(state[4..].as_ptr())) }; + for block in blocks { + let (saved_abcd, saved_efgh) = (abcd, efgh); + // SAFETY: the block's 64 bytes, read 16 at a time: its sixteen + // big-endian words, four to a vector. + let mut w: [uint32x4_t; 4] = + core::array::from_fn(|i| unsafe { vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(block[16 * i..].as_ptr()))) }); + for i in 0..16 { + if i >= 4 { + // W[4i..4i+4] from the four groups before it, oldest first. + w[i % 4] = vsha256su1q_u32(vsha256su0q_u32(w[i % 4], w[(i + 1) % 4]), w[(i + 2) % 4], w[(i + 3) % 4]); + } + let wk = vaddq_u32(w[i % 4], k[i]); + let previous = abcd; + abcd = vsha256hq_u32(abcd, efgh, wk); + efgh = vsha256h2q_u32(efgh, previous, wk); + } + abcd = vaddq_u32(abcd, saved_abcd); + efgh = vaddq_u32(efgh, saved_efgh); + } + // SAFETY: `state` has room for eight words. + unsafe { + vst1q_u32(state.as_mut_ptr(), abcd); + vst1q_u32(state[4..].as_mut_ptr(), efgh); + } +} + +#[cfg(not(target_feature = "sha2"))] +pub(super) fn sha256_blocks(state: &mut [u32; 8], blocks: &[[u8; 64]]) -> bool { + let _ = (state, blocks); + false +} + +/// Two rounds to a SHA512H and SHA512H2 pair. The working variables are held +/// in pairs, the lower lane first: `ab` is (A, B), `cd` (C, D), `ef` (E, F) +/// and `gh` (G, H). SHA512H takes (H + K[t] + W[t]) in its high lane and +/// (G + K[t+1] + W[t+1]) in its low one, with (D, E) and (F, G), and answers +/// T1 of both rounds; SHA512H2 turns those, with (A, B) and C, into the two +/// new A. +#[cfg(target_feature = "sha3")] +pub(super) fn sha512_blocks(state: &mut [u64; 8], blocks: &[[u8; 128]]) -> bool { + if cfg!(feature = "portable") { + return false; + } + // SAFETY: the target guarantees FEAT_SHA512. + unsafe { sha512(state, blocks) } + true +} + +#[cfg(target_feature = "sha3")] +#[target_feature(enable = "neon,sha3")] +fn sha512(state: &mut [u64; 8], blocks: &[[u8; 128]]) { + // SAFETY: `state` holds eight plain words. + let mut s: [uint64x2_t; 4] = core::array::from_fn(|i| unsafe { vld1q_u64(state[2 * i..].as_ptr()) }); + for block in blocks { + let saved = s; + let [mut ab, mut cd, mut ef, mut gh] = s; + // SAFETY: the block's 128 bytes, read 16 at a time: its sixteen + // big-endian words, two to a vector. + let mut w: [uint64x2_t; 8] = + core::array::from_fn(|i| unsafe { vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(block[16 * i..].as_ptr()))) }); + for i in 0..40 { + if i >= 8 { + // W[2i] and W[2i+1]: the pair eight before, σ0 of the words + // after it, σ1 of the pair just before and W[2i-7..2i-5]. + let sigma0 = vsha512su0q_u64(w[i % 8], w[(i + 1) % 8]); + w[i % 8] = vsha512su1q_u64(sigma0, w[(i + 7) % 8], vextq_u64::<1>(w[(i + 4) % 8], w[(i + 5) % 8])); + } + // SAFETY: two plain words of K. + let kw = vaddq_u64(w[i % 8], unsafe { vld1q_u64(crate::hash::K512[2 * i..].as_ptr()) }); + let t1 = vsha512hq_u64( + vaddq_u64(gh, vextq_u64::<1>(kw, kw)), + vextq_u64::<1>(ef, gh), + vextq_u64::<1>(cd, ef), + ); + let a = vsha512h2q_u64(t1, cd, ab); + (ab, cd, ef, gh) = (a, ab, vaddq_u64(cd, t1), ef); + } + s = [ab, cd, ef, gh]; + for i in 0..4 { + s[i] = vaddq_u64(s[i], saved[i]); + } + } + for i in 0..4 { + // SAFETY: `state` has room for eight words. + unsafe { vst1q_u64(state[2 * i..].as_mut_ptr(), s[i]) } + } +} + +#[cfg(not(target_feature = "sha3"))] +pub(super) fn sha512_blocks(state: &mut [u64; 8], blocks: &[[u8; 128]]) -> bool { + let _ = (state, blocks); + false +} diff --git a/lib/crypto/src/arch/mod.rs b/lib/crypto/src/arch/mod.rs index c4952793c34..64adabca33a 100644 --- a/lib/crypto/src/arch/mod.rs +++ b/lib/crypto/src/arch/mod.rs @@ -20,6 +20,9 @@ compile_error!( #[cfg(target_arch = "x86_64")] mod x86_64; +#[cfg(target_arch = "aarch64")] +mod aarch64; + /// Hosted x86-64 alone: the paths below with ordinary register ABI. macro_rules! hosted { ($hosted:expr, $otherwise:expr) => {{ @@ -39,7 +42,7 @@ macro_rules! hosted { #[cfg(not(miri))] #[inline(always)] pub(crate) fn value_barrier(mut value: u64) -> u64 { - // The template reads and writes only the operand's register. + // SAFETY: the template reads and writes only the operand's register. unsafe { core::arch::asm!("/* {0} */", inout(reg) value, options(pure, nomem, nostack, preserves_flags)) }; @@ -160,13 +163,21 @@ pub(crate) fn chacha_xor(key: &[u8; 32], counter: u64, nonce: &[u8; 8], data: &m } pub(crate) fn sha256_blocks(state: &mut [u32; 8], blocks: &[[u8; 64]]) -> bool { - hosted!(x86_64::sha256_blocks(state, blocks), { + #[cfg(target_arch = "x86_64")] + return x86_64::sha256_blocks(state, blocks); + #[cfg(target_arch = "aarch64")] + return aarch64::sha256_blocks(state, blocks); + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { let _ = (state, blocks); false - }) + } } pub(crate) fn sha512_blocks(state: &mut [u64; 8], blocks: &[[u8; 128]]) -> bool { + #[cfg(target_arch = "aarch64")] + return aarch64::sha512_blocks(state, blocks); + #[cfg(not(target_arch = "aarch64"))] hosted!(x86_64::sha512_blocks(state, blocks), { let _ = (state, blocks); false diff --git a/lib/crypto/src/arch/x86_64.rs b/lib/crypto/src/arch/x86_64.rs index 1778f7a60ca..bb0d1eb0d46 100644 --- a/lib/crypto/src/arch/x86_64.rs +++ b/lib/crypto/src/arch/x86_64.rs @@ -21,6 +21,8 @@ mod poly1305; mod sha; #[cfg(not(target_os = "uefi"))] mod vaes512; +#[cfg(any(target_os = "uefi", test))] +mod sha_saving; /// What CPUID and XCR0 admit, read once per process. The module contract /// makes features uniform across the CPUs a process runs on. @@ -65,7 +67,6 @@ impl Features { self.has(Self::BMI2) } /// SHA-NI with the SSSE3 and SSE4.1 shuffles its message schedule takes. - #[cfg(not(target_os = "uefi"))] pub(super) fn sha(self) -> bool { self.has(Self::SHA | Self::SSSE3 | Self::SSE41) } @@ -93,16 +94,21 @@ impl Features { /// too. #[allow(unused_unsafe, reason = "CPUID is unsafe on the supported Rust 1.89")] fn read() -> Self { - // Leaf 0 and leaf 1 exist on every x86-64 CPU; leaf 7 where leaf 0 - // says so; XGETBV where OSXSAVE says so. + // SAFETY: leaf 0 and leaf 1 exist on every x86-64 CPU. let (leaf1, max) = unsafe { (__cpuid(1).ecx, __cpuid(0).eax) }; let (leaf7b, leaf7c) = if max >= 7 { + // SAFETY: leaf 0 says leaf 7 exists. let leaf7 = unsafe { __cpuid_count(7, 0) }; (leaf7.ebx, leaf7.ecx) } else { (0, 0) }; - let xcr0 = if leaf1 & (3 << 27) == 3 << 27 { unsafe { _xgetbv(0) } } else { 0 }; + let xcr0 = if leaf1 & (3 << 27) == 3 << 27 { + // SAFETY: OSXSAVE says XGETBV is enabled. + unsafe { _xgetbv(0) } + } else { + 0 + }; Self::decode(leaf1, leaf7b, leaf7c, xcr0) } @@ -196,7 +202,7 @@ pub(super) fn ghash_multiply(a: u128, b: u128) -> Option { let b = crate::secret::Secret(b.reverse_bits().to_le_bytes()); let mut products = crate::secret::Secret([0u8; 48]); let mut saved = crate::secret::Secret([0u8; 80]); - // CPUID admitted CLMUL. All five used registers are restored; each memory + // SAFETY: CPUID admitted CLMUL. All five used registers are restored; each memory // operand refers to a distinct live array of the required size. unsafe { asm!( @@ -235,7 +241,7 @@ pub(super) fn aes_encrypt(keys: &[u8; 240], rounds: usize, block: &mut [u8; 16]) return false; } let mut saved = crate::secret::Secret([0u8; 32]); - // CPUID admitted AES. The schedule contains every round, `block` and + // SAFETY: CPUID admitted AES. The schedule contains every round, `block` and // `saved` are exclusive, and both XMM registers are restored before return. unsafe { asm!( @@ -252,6 +258,16 @@ pub(super) fn aes_encrypt(keys: &[u8; 240], rounds: usize, block: &mut [u8; 16]) true } +#[cfg(target_os = "uefi")] +pub(super) fn sha256_blocks(state: &mut [u32; 8], blocks: &[[u8; 64]]) -> bool { + if !features().sha() { + return false; + } + // SAFETY: CPUID admitted SHA and SSSE3. + unsafe { sha_saving::sha256(state, blocks) } + true +} + #[cfg(not(target_os = "uefi"))] pub(super) use hosted::*; @@ -473,6 +489,29 @@ mod tests { } } + /// The loader's block, which only UEFI dispatches to, against the portable + /// rounds. A host without the instructions cannot test it, and says so + /// rather than passing. + #[test] + fn the_register_saving_sha256_block_matches_the_portable_rounds() { + assert!( + features().sha() || cfg!(feature = "portable"), + "this host's CPU has no SHA extensions, so nothing here can test the register-saving block" + ); + if !features().sha() { + return; + } + let bytes: std::vec::Vec = (0..64 * 9).map(|i: usize| (i * 151 + i / 7) as u8).collect(); + for n in 0..=9 { + let (blocks, _) = bytes[..64 * n].as_chunks::<64>(); + let (mut saving, mut portable) = ([0x89ab_cdefu32; 8], [0x89ab_cdefu32; 8]); + // CPUID admitted SHA and SSSE3. + unsafe { sha_saving::sha256(&mut saving, blocks) }; + crate::hash::sha256_rounds(&mut portable, blocks); + assert_eq!(saving, portable, "{n} blocks"); + } + } + #[test] fn features_match_host_detection_and_portable_override() { let f = features(); diff --git a/lib/sha2-hw/src/x86_64.rs b/lib/crypto/src/arch/x86_64/sha_saving.rs similarity index 75% rename from lib/sha2-hw/src/x86_64.rs rename to lib/crypto/src/arch/x86_64/sha_saving.rs index 344af61eeec..c5403e48494 100644 --- a/lib/sha2-hw/src/x86_64.rs +++ b/lib/crypto/src/arch/x86_64/sha_saving.rs @@ -1,47 +1,37 @@ -//! 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). +//! SHA-256 on the SHA extensions in one `asm!` block, for the UEFI loader: +//! `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. +//! **The 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. 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; +static K: [u32; 64] = crate::hash::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]]) { +/// +/// # Safety +/// CPUID reports SHA and SSSE3. +pub(super) unsafe fn sha256(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. + // SAFETY: the caller admitted 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})", diff --git a/lib/crypto/src/field25519.rs b/lib/crypto/src/field25519.rs index e878ef878ff..25567032c43 100644 --- a/lib/crypto/src/field25519.rs +++ b/lib/crypto/src/field25519.rs @@ -318,5 +318,5 @@ const fn square(a: &[u64; 5]) -> Tight { carry(v) } -// Five words: zero is a valid element. +// SAFETY: five words, and zero is a valid element. unsafe impl crate::secret::Zeroable for Tight {} diff --git a/lib/crypto/src/mlkem.rs b/lib/crypto/src/mlkem.rs index af2b3074926..f6cdb91a653 100644 --- a/lib/crypto/src/mlkem.rs +++ b/lib/crypto/src/mlkem.rs @@ -17,6 +17,7 @@ struct Poly([u16; 256]); impl Drop for Poly { fn drop(&mut self) { for word in &mut self.0 { + // SAFETY: the address is a live, exclusively borrowed coefficient. unsafe { core::ptr::write_volatile(word, 0) } } core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst) @@ -377,10 +378,13 @@ mod tests { } } + /// NIST's keygen vectors are read, not embedded: they sit outside this + /// crate's directory, as test input the image does not carry. Miri reads + /// the file with its isolation off (`-Zmiri-disable-isolation`). #[test] fn nist_expanded_keygen_secret_matches() { - let data: serde_json::Value = - serde_json::from_str(include_str!("../tests/data/nist-mlkem-keygen.json")).unwrap(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/acvp/nist-mlkem-keygen.json"); + let data: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); let mut count = 0; for group in data["testGroups"].as_array().unwrap() { if group["parameterSet"] != "ML-KEM-768" { diff --git a/lib/crypto/src/montgomery.rs b/lib/crypto/src/montgomery.rs index d8450a434ec..d3f6b5569e9 100644 --- a/lib/crypto/src/montgomery.rs +++ b/lib/crypto/src/montgomery.rs @@ -51,7 +51,7 @@ impl, const N: usize> Clone for Residue { } } impl, const N: usize> Copy for Residue {} -// Words and a zero-sized marker: zero is a valid residue. +// SAFETY: words and a zero-sized marker, and zero is a valid residue. unsafe impl, const N: usize> crate::secret::Zeroable for Residue {} impl, const N: usize> Residue { diff --git a/lib/crypto/src/nist.rs b/lib/crypto/src/nist.rs index 3924148948d..cbe767662f2 100644 --- a/lib/crypto/src/nist.rs +++ b/lib/crypto/src/nist.rs @@ -128,7 +128,7 @@ impl, const N: usize> Clone for Point { } } impl, const N: usize> Copy for Point {} -// Three residues: zero is a valid point representation. +// SAFETY: three residues, and zero is a valid point representation. unsafe impl, const N: usize> crate::secret::Zeroable for Point {} impl, const N: usize> Point { fn identity() -> Self { diff --git a/lib/crypto/src/secret.rs b/lib/crypto/src/secret.rs index 84249f6e9b6..d9edd6b0777 100644 --- a/lib/crypto/src/secret.rs +++ b/lib/crypto/src/secret.rs @@ -37,7 +37,7 @@ use core::sync::atomic::{Ordering, compiler_fence}; pub(crate) fn wipe(bytes: &mut [u8]) { for byte in bytes { - // The address is a live, exclusively borrowed byte. + // SAFETY: the address is a live, exclusively borrowed byte. unsafe { core::ptr::write_volatile(byte, 0) }; } compiler_fence(Ordering::SeqCst); @@ -46,7 +46,7 @@ pub(crate) fn wipe(bytes: &mut [u8]) { #[cfg(feature = "wpa2")] pub(crate) fn wipe32(words: &mut [u32]) { for word in words { - // The address is a live, exclusively borrowed word. + // SAFETY: the address is a live, exclusively borrowed word. unsafe { core::ptr::write_volatile(word, 0) }; } compiler_fence(Ordering::SeqCst); @@ -54,7 +54,7 @@ pub(crate) fn wipe32(words: &mut [u32]) { pub(crate) fn wipe64(words: &mut [u64]) { for word in words { - // The address is a live, exclusively borrowed word. + // SAFETY: the address is a live, exclusively borrowed word. unsafe { core::ptr::write_volatile(word, 0) }; } compiler_fence(Ordering::SeqCst); @@ -66,12 +66,17 @@ pub(crate) fn wipe64(words: &mut [u64]) { /// The all-zero bit pattern must be a valid `Self`: no references, /// `NonZero` fields or enums without a zero discriminant. pub(crate) unsafe trait Zeroable: Copy {} -// Integers and arrays of zeroable elements are valid at zero. +// SAFETY: an integer is valid at zero. unsafe impl Zeroable for u8 {} +// SAFETY: as `u8`. unsafe impl Zeroable for i8 {} +// SAFETY: as `u8`. unsafe impl Zeroable for u32 {} +// SAFETY: as `u8`. unsafe impl Zeroable for u64 {} +// SAFETY: as `u8`. unsafe impl Zeroable for u128 {} +// SAFETY: an array of elements valid at zero is valid at zero. unsafe impl Zeroable for [T; N] {} /// Erases a whole value, 16 bytes per volatile store. A single volatile store @@ -80,12 +85,12 @@ pub(crate) fn erase(value: &mut T) { let bytes = core::mem::size_of::(); let base = (value as *mut T).cast::(); for i in 0..bytes / 16 { - // The 16 bytes lie inside the live, exclusively borrowed value, and - // zero bytes are a valid `T`. + // SAFETY: the 16 bytes lie inside the live, exclusively borrowed value, + // and zero bytes are a valid `T`. unsafe { core::ptr::write_volatile(base.add(16 * i).cast::<[u8; 16]>(), [0; 16]) }; } for i in bytes / 16 * 16..bytes { - // As above, one byte. + // SAFETY: as above, one byte. unsafe { core::ptr::write_volatile(base.add(i), 0) }; } compiler_fence(Ordering::SeqCst); diff --git a/lib/crypto/tests/nist.rs b/lib/crypto/tests/nist.rs index 502896e359a..8f0975abd36 100644 --- a/lib/crypto/tests/nist.rs +++ b/lib/crypto/tests/nist.rs @@ -1,6 +1,6 @@ //! NIST's vectors (`NOTICE`): CAVP's response files under the repository's //! `tests/cavp/` for SHA-2, AES-GCM and ECDSA, and ACVP's for HMAC and -//! ML-KEM under `tests/data`. A case whose parameters the crate does not +//! ML-KEM under its `tests/acvp/`. A case whose parameters the crate does not //! offer is counted outside the interface by the reason it names, and each //! file's cases are held to a count taken from its text apart from the //! parse: its `Count = `, `Len = `, `Msg = ` or `Qx = ` lines, or its @@ -11,7 +11,7 @@ mod support; use std::collections::BTreeMap; -use support::{Given, Tally, cavp, data, der_signature, ecdsa, fixed_be, hex, integer}; +use support::{Given, Tally, acvp, cavp, der_signature, ecdsa, fixed_be, hex, integer}; use toyos_crypto::{ aead::SealingKey, hash::{Sha256, Sha384, Sha512}, @@ -246,8 +246,7 @@ fn cavp_ecdsa() { /// An ACVP file's cases, counted in its text rather than its parse. fn tc_ids(name: &str) -> usize { - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data").join(name); - std::fs::read_to_string(path).unwrap().matches("\"tcId\"").count() + support::acvp_text(name).matches("\"tcId\"").count() } /// ACVP's HMAC vectors. A `macLen` below the hash's is the prefix of the @@ -256,7 +255,7 @@ fn tc_ids(name: &str) -> usize { fn acvp_hmac() { fn run(file: &str, mac: impl Fn(&[u8], &[u8]) -> Vec) { let mut count = 0; - for g in data(file)["testGroups"].as_array().unwrap() { + for g in acvp(file)["testGroups"].as_array().unwrap() { for t in g["tests"].as_array().unwrap() { let field = |name: &str| hex(t[name].as_str().unwrap()); let want = field("mac"); @@ -294,7 +293,7 @@ fn acvp_mlkem768() { let field = |t: &serde_json::Value, name: &str| hex(t[name].as_str().unwrap()); let seed = |t: &serde_json::Value| [field(t, "d"), field(t, "z")].concat(); let mut tally = Tally::default(); - for g in data("nist-mlkem-keygen.json")["testGroups"].as_array().unwrap() { + for g in acvp("nist-mlkem-keygen.json")["testGroups"].as_array().unwrap() { for t in g["tests"].as_array().unwrap() { if g["parameterSet"] != "ML-KEM-768" { tally.outside("not ML-KEM-768"); @@ -308,7 +307,7 @@ fn acvp_mlkem768() { tally.report("nist-mlkem-keygen.json", Some(tc_ids("nist-mlkem-keygen.json"))); let mut tally = Tally::default(); - for g in data("nist-mlkem-encapdecap.json")["testGroups"].as_array().unwrap() { + for g in acvp("nist-mlkem-encapdecap.json")["testGroups"].as_array().unwrap() { for t in g["tests"].as_array().unwrap() { let id = format!("tcId {} {}", t["tcId"], t["reason"]); if g["parameterSet"] != "ML-KEM-768" { diff --git a/lib/crypto/tests/support/mod.rs b/lib/crypto/tests/support/mod.rs index 00d96706b10..f8fd0cdb205 100644 --- a/lib/crypto/tests/support/mod.rs +++ b/lib/crypto/tests/support/mod.rs @@ -11,6 +11,19 @@ pub fn data(name: &str) -> serde_json::Value { serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("{}: {e}", path.display())) } +/// A NIST ACVP vector file under the repository's `tests/acvp/`, outside +/// this crate's directory: test input, which the licence gate would count as +/// shipping inside it. +pub fn acvp(name: &str) -> serde_json::Value { + serde_json::from_str(&acvp_text(name)).unwrap() +} + +/// [`acvp`]'s file as text. +pub fn acvp_text(name: &str) -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/acvp").join(name); + std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())) +} + /// A response file under the repository's `tests/cavp/`. pub fn cavp(name: &str) -> String { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/cavp").join(name); diff --git a/lib/sha2-hw/Cargo.toml b/lib/sha2-hw/Cargo.toml deleted file mode 100644 index e7bd0924e92..00000000000 --- a/lib/sha2-hw/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[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 = "../sha2" } diff --git a/lib/sha2-hw/src/lib.rs b/lib/sha2-hw/src/lib.rs deleted file mode 100644 index 46e13737d42..00000000000 --- a/lib/sha2-hw/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! 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/lib/sha2-hw/src/tests.rs b/lib/sha2-hw/src/tests.rs deleted file mode 100644 index 5e5bb8f36fb..00000000000 --- a/lib/sha2-hw/src/tests.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! 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 = "../../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/lib/sha2/Cargo.toml b/lib/sha2/Cargo.toml deleted file mode 100644 index 5c45a35c4b9..00000000000 --- a/lib/sha2/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "toyos-sha2" -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" -publish = false - -[dev-dependencies] -# The differential oracle: RustCrypto's implementation, which shares no code -# with this crate. Already resolved here, where `ed25519-dalek` hashes with it. -sha2 = "0.10" diff --git a/lib/sha2/src/lib.rs b/lib/sha2/src/lib.rs deleted file mode 100644 index 9158685fca2..00000000000 --- a/lib/sha2/src/lib.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! SHA-256 and SHA-512, FIPS 180-4 §6.2 and §6.4, over whole bytes: what the -//! loader holds a signed image's sections to, what `update` and `swap` hold -//! what they install to, and what the build names its inputs by. -//! -//! [`Sha256`] and [`Sha512`] take a message in any number of [`update`]s and -//! give its digest at [`finalize`]; `digest` is both at once. The digest of a -//! 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.** 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 - -#![cfg_attr(not(test), no_std)] -#![forbid(unsafe_code)] - -#[cfg(test)] -mod tests; - -/// SHA-256's round constants, §4.2.2: an instruction path's too. -#[rustfmt::skip] -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, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, -]; - -/// SHA-256's initial hash value, §5.3.3. -#[rustfmt::skip] -const H256: [u32; 8] = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, -]; - -/// SHA-512's round constants, §4.2.3. -#[rustfmt::skip] -const K512: [u64; 80] = [ - 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, - 0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, - 0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, - 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694, - 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, - 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, - 0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, - 0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70, - 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df, - 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, - 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, - 0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, - 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, - 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3, - 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, - 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, - 0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, - 0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b, - 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c, - 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, -]; - -/// SHA-512's initial hash value, §5.3.5. -#[rustfmt::skip] -const H512: [u64; 8] = [ - 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, - 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, -]; - -/// `$step::$args` for each `I` of a pass's sixteen rounds, written out so -/// that every index into the schedule and the working variables is a -/// constant. -macro_rules! sixteen { - ($step:ident $args:tt) => { - sixteen!(@ $step $args 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15) - }; - (@ $step:ident $args:tt $($i:literal)*) => { - $($step::<$i> $args;)* - }; -} - -/// The two compression functions differ in their word, block, constants and -/// rotations, and in nothing about how a block is computed. -macro_rules! compress { - ($(#[$doc:meta])* $name:ident: $word:ty, block $block:literal, $k:expr, - sigma0 $s0:expr, sigma1 $s1:expr, schedule0 $r0:expr, schedule1 $r1:expr) => { - $(#[$doc])* - fn $name(state: &mut [$word; 8], blocks: &[[u8; $block]]) { - /// Round `I` of a pass, the working variables standing where - /// they are rather than moved along: this round's `a` is - /// `v[(8 - I % 8) % 8]`, and it writes only its new `e` and `a`. - #[inline(always)] - fn round(v: &mut [$word; 8], k: &[$word; 16], w: &[$word; 16]) { - let at = |n: usize| (n + 8 - I % 8) % 8; - let (a, b, c) = (v[at(0)], v[at(1)], v[at(2)]); - let (e, f, g) = (v[at(4)], v[at(5)], v[at(6)]); - let [x, y, z] = $s1; - let t1 = v[at(7)] - .wrapping_add(k[I]) - .wrapping_add(w[I]) - .wrapping_add(g ^ (e & (f ^ g))) - .wrapping_add(e.rotate_right(x) ^ e.rotate_right(y) ^ e.rotate_right(z)); - let [x, y, z] = $s0; - let t2 = (a.rotate_right(x) ^ a.rotate_right(y) ^ a.rotate_right(z)) - .wrapping_add(b ^ ((a ^ b) & (b ^ c))); - v[at(3)] = v[at(3)].wrapping_add(t1); - v[at(7)] = t1.wrapping_add(t2); - } - - /// The schedule's word `I` of the next pass in place of this - /// pass's: `w[I]` holds W(t-16) going in and W(t) coming out. - #[inline(always)] - fn schedule(w: &mut [$word; 16]) { - let (w15, w2) = (w[(I + 1) % 16], w[(I + 14) % 16]); - let [x, y, z] = $r0; - let s0 = w15.rotate_right(x) ^ w15.rotate_right(y) ^ (w15 >> z); - let [x, y, z] = $r1; - let s1 = w2.rotate_right(x) ^ w2.rotate_right(y) ^ (w2 >> z); - w[I] = w[I] - .wrapping_add(s0) - .wrapping_add(w[(I + 9) % 16]) - .wrapping_add(s1); - } - - let mut hash = *state; - for block in blocks { - let mut w: [$word; 16] = [0; 16]; - for (word, bytes) in w.iter_mut().zip(block.as_chunks().0) { - *word = <$word>::from_be_bytes(*bytes); - } - let mut v = hash; - for (pass, k) in $k.as_chunks::<16>().0.iter().enumerate() { - if pass > 0 { - sixteen!(schedule(&mut w)); - } - sixteen!(round(&mut v, k, &w)); - } - for (word, v) in hash.iter_mut().zip(v) { - *word = word.wrapping_add(v); - } - } - *state = hash; - } - }; -} - -compress! { - /// Blocks into the hash value: SHA-256's computation, §6.2.2, with - /// §4.1.2's functions. - compress256: u32, block 64, K256, - sigma0 [2, 13, 22], sigma1 [6, 11, 25], schedule0 [7, 18, 3], schedule1 [17, 19, 10] -} - -compress! { - /// Blocks into the hash value: SHA-512's computation, §6.4.2, with - /// §4.1.3's functions. - compress512: u64, block 128, K512, - sigma0 [28, 34, 39], sigma1 [14, 18, 41], schedule0 [1, 8, 7], schedule1 [19, 61, 6] -} - -/// The two hashes differ in their word, block, length field, constants and -/// functions, and in nothing about how a message is buffered and padded. -macro_rules! hash { - ($(#[$doc:meta])* $name:ident: $word:ty, block $block:literal, length $length:literal, - digest $out:literal, $init:expr, $compress:ident) => { - $(#[$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, - /// The message's length so far, in bytes. - bytes: u64, - } - - impl $name { - pub const fn new() -> Self { - Self { state: $init, compress: $compress, block: [0; $block], filled: 0, bytes: 0 } - } - - /// The digest of `message`. - pub fn digest(message: impl AsRef<[u8]>) -> [u8; $out] { - let mut hash = Self::new(); - hash.update(message); - hash.finalize() - } - - /// `bytes` appended to the message. - pub fn update(&mut self, bytes: impl AsRef<[u8]>) { - self.absorb(bytes.as_ref()); - } - - fn absorb(&mut self, mut bytes: &[u8]) { - self.bytes += bytes.len() as u64; - if self.filled > 0 { - let take = bytes.len().min($block - self.filled); - self.block[self.filled..self.filled + take].copy_from_slice(&bytes[..take]); - self.filled += take; - bytes = &bytes[take..]; - if self.filled < $block { - return; - } - (self.compress)(&mut self.state, core::slice::from_ref(&self.block)); - self.filled = 0; - } - let (blocks, rest) = bytes.as_chunks::<$block>(); - (self.compress)(&mut self.state, blocks); - self.block[..rest.len()].copy_from_slice(rest); - self.filled = rest.len(); - } - - /// The message's digest: padded as §5.1 pads it — a one bit, the - /// fewest zero bits that end the block where the length field - /// begins, and the length in bits, big-endian. - pub fn finalize(mut self) -> [u8; $out] { - let bits = (u128::from(self.bytes) * 8).to_be_bytes(); - let (over, length) = bits.split_at(16 - $length); - assert!( - over.iter().all(|&b| b == 0), - "FIPS 180-4 §5.1 bounds a message by the bits its length field holds" - ); - let mut pad = [0u8; $block]; - pad[0] = 0x80; - self.absorb(&pad[..($block - $length - 1 + $block - self.filled) % $block + 1]); - self.absorb(length); - let mut out = [0u8; $out]; - for (bytes, word) in out.as_chunks_mut::<{ size_of::<$word>() }>().0.iter_mut().zip(self.state) { - *bytes = word.to_be_bytes(); - } - out - } - } - - impl Default for $name { - fn default() -> Self { - Self::new() - } - } - }; -} - -hash! { - /// SHA-256, §6.2: a 32-byte digest of a message shorter than 2^64 bits. - Sha256: u32, block 64, length 8, digest 32, H256, compress256 -} - -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/lib/sha2/src/tests.rs b/lib/sha2/src/tests.rs deleted file mode 100644 index 0120fdc4472..00000000000 --- a/lib/sha2/src/tests.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! The oracles: NIST's CAVP SHAVS response files (`tests/cavp/`, outside every -//! shipped package's directory; `NOTICE`), and RustCrypto's `sha2` over every -//! message length to 4096 bytes in random splits. - -use super::*; - -mod shavs; -use shavs::*; - -fn sha256(msg: &[u8]) -> Vec { - Sha256::digest(msg).to_vec() -} - -fn sha512(msg: &[u8]) -> Vec { - Sha512::digest(msg).to_vec() -} - -fn sha256_bytewise(msg: &[u8]) -> Vec { - let mut hash = Sha256::new(); - msg.iter().for_each(|b| hash.update([*b])); - hash.finalize().to_vec() -} - -fn sha512_bytewise(msg: &[u8]) -> Vec { - let mut hash = Sha512::new(); - 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 sha512_byte_vectors() { - byte_file("SHA512ShortMsg.rsp", sha512, sha512_bytewise); - byte_file("SHA512LongMsg.rsp", sha512, sha512_bytewise); -} - -#[test] -fn sha256_monte_carlo() { - monte_file("SHA256Monte.rsp", sha256); -} - -#[test] -fn sha512_monte_carlo() { - monte_file("SHA512Monte.rsp", sha512); -} - -/// **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. -#[test] -fn every_length_to_4096_in_random_splits_agrees_with_sha2() { - use sha2::Digest as _; - let mut draws = Draws(0x5eed_70e0_5a2b_0001); - for len in 0..=4096usize { - let msg: Vec = (0..len).map(|_| draws.next() as u8).collect(); - let want256: [u8; 32] = sha2::Sha256::digest(&msg).into(); - let want512: [u8; 64] = sha2::Sha512::digest(&msg).into(); - assert_eq!(Sha256::digest(&msg), want256, "SHA-256 of {len} bytes"); - assert_eq!(Sha512::digest(&msg), want512, "SHA-512 of {len} bytes"); - for _ in 0..3 { - let cuts = draws.splits(len); - let (mut a, mut b) = (Sha256::new(), Sha512::new()); - let mut at = 0; - for cut in cuts.iter().copied().chain([len]) { - a.update(&msg[at..cut]); - b.update(&msg[at..cut]); - at = cut; - } - assert_eq!( - a.finalize(), - want256, - "SHA-256 of {len} bytes cut at {cuts:?}" - ); - assert_eq!( - b.finalize(), - want512, - "SHA-512 of {len} bytes cut at {cuts:?}" - ); - } - } -} diff --git a/lib/sha2/src/tests/shavs.rs b/lib/sha2/src/tests/shavs.rs deleted file mode 100644 index b64dfb1a2ed..00000000000 --- a/lib/sha2/src/tests/shavs.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! 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/lib/swap/Cargo.toml b/lib/swap/Cargo.toml index 2e4509dbe92..03c87d37ec7 100644 --- a/lib/swap/Cargo.toml +++ b/lib/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 = "../manifest" } # The digest a swap is verified by. -toyos-sha2-hw = { path = "../sha2-hw" } +toyos-crypto = { path = "../crypto" } diff --git a/lib/swap/src/lib.rs b/lib/swap/src/lib.rs index bb57a626403..2fac0de07c9 100644 --- a/lib/swap/src/lib.rs +++ b/lib/swap/src/lib.rs @@ -87,7 +87,9 @@ pub type Digest = [u8; 32]; /// The digest of `bytes`. pub fn digest(bytes: &[u8]) -> Digest { - toyos_sha2_hw::sha256_digest(bytes) + // FIPS 180-4 bounds a message at 2^61 bytes; a service binary is far + // below it. + toyos_crypto::hash::Sha256::digest(bytes).expect("a binary below SHA-256's 2^61-byte bound") } pub fn hex(digest: &Digest) -> String { diff --git a/lib/update/Cargo.toml b/lib/update/Cargo.toml index 2fb1758c6ac..5d1317ea646 100644 --- a/lib/update/Cargo.toml +++ b/lib/update/Cargo.toml @@ -28,10 +28,8 @@ 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-512 over the signed header, as SSHSIG names it. -toyos-sha2 = { path = "../sha2" } -# SHA-256 over the sections and a package archive, on the CPU's SHA -# instructions where it has them. -toyos-sha2-hw = { path = "../sha2-hw" } +# SHA-512 over the signed header, as SSHSIG names it, and SHA-256 over the +# sections and a package archive. +toyos-crypto = { path = "../crypto" } # The calendar a repository document's `expires` is written in, the tree's one. toyos-wallclock = { path = "../wallclock" } diff --git a/lib/update/src/lib.rs b/lib/update/src/lib.rs index f9c304975c0..57af6c1b008 100644 --- a/lib/update/src/lib.rs +++ b/lib/update/src/lib.rs @@ -39,9 +39,14 @@ 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_hw::sha256_digest(bytes) + toyos_crypto::hash::Sha256::digest(bytes).expect(SHA_BOUND) } +/// Why a hash here cannot refuse: FIPS 180-4 bounds a message at 2^61 bytes, +/// and what these hash is an image's section or a signed header, each held in +/// memory, or a streamed archive, which [`repo::ARCHIVE_CAP`] holds to 1 GiB. +pub(crate) const SHA_BOUND: &str = "a hashed input is far below SHA-2's 2^61-byte bound"; + /// A digest in lowercase hex, into `out`. pub fn hex<'a>(digest: &[u8], out: &'a mut [u8]) -> &'a str { const DIGITS: &[u8; 16] = b"0123456789abcdef"; diff --git a/lib/update/src/repo.rs b/lib/update/src/repo.rs index b147e1123f1..70ce0262382 100644 --- a/lib/update/src/repo.rs +++ b/lib/update/src/repo.rs @@ -39,7 +39,7 @@ use alloc::string::String; use alloc::vec::Vec; use core::fmt; -use toyos_sha2::Sha256; +use toyos_crypto::hash::Sha256; use crate::sig; use crate::{sha256, Digest}; @@ -684,7 +684,7 @@ pub struct Archive { impl Archive { pub fn of(item: &Item) -> Self { - Archive { length: item.length, sha256: item.sha256, seen: 0, hash: toyos_sha2_hw::sha256() } + Archive { length: item.length, sha256: item.sha256, seen: 0, hash: Sha256::new() } } /// The next bytes, refused where they run past the signed length. @@ -693,7 +693,7 @@ impl Archive { if seen > self.length { return Err(Refused::ArchivePast { length: self.length }); } - self.hash.update(chunk); + self.hash.update(chunk).expect(crate::SHA_BOUND); self.seen = seen; Ok(()) } diff --git a/lib/update/src/sig.rs b/lib/update/src/sig.rs index 06c7c40a234..cf209b208d2 100644 --- a/lib/update/src/sig.rs +++ b/lib/update/src/sig.rs @@ -17,7 +17,7 @@ //! from verifying as this. use ed25519_dalek::{Signature, VerifyingKey}; -use toyos_sha2::Sha512; +use toyos_crypto::hash::Sha512; use crate::image::{HEADER_BYTES, SIGNATURE_BYTES}; @@ -74,7 +74,7 @@ pub fn message(signed: &S) -> Message { put(field); } put(&64u32.to_be_bytes()); - put(&Sha512::digest(signed.bytes())); + put(&Sha512::digest(signed.bytes()).expect(crate::SHA_BOUND)); out } @@ -245,7 +245,7 @@ mod tests { assert_eq!(&m[25..29], &6u32.to_be_bytes()); assert_eq!(&m[29..35], b"sha512"); assert_eq!(&m[35..39], &64u32.to_be_bytes()); - assert_eq!(&m[39..], &Sha512::digest(header)[..]); + assert_eq!(&m[39..], &Sha512::digest(&header).unwrap()[..]); } /// A seed's key-file text reads back as the seed. diff --git a/sdk/icon/Cargo.toml b/sdk/icon/Cargo.toml index 40b045ea73c..8dbf83735c3 100644 --- a/sdk/icon/Cargo.toml +++ b/sdk/icon/Cargo.toml @@ -14,7 +14,7 @@ doctest = false libm = "0.2.16" [dev-dependencies] -toyos-sha2-hw = { path = "../../lib/sha2-hw" } +toyos-crypto = { path = "../../lib/crypto" } [lints.rust] warnings = "deny" diff --git a/sdk/icon/src/lib.rs b/sdk/icon/src/lib.rs index 1a551550d43..2a5682d8c80 100644 --- a/sdk/icon/src/lib.rs +++ b/sdk/icon/src/lib.rs @@ -805,13 +805,13 @@ mod tests { #[test] fn the_icons_are_the_pixels_compared_with_resvg() { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let mut digest = toyos_sha2_hw::sha256(); + let mut digest = toyos_crypto::hash::Sha256::new(); for &(stem, edge) in SHIPPED { let svg = std::fs::read(root.join(format!("assets/icons/{stem}.svg"))).expect("a committed icon"); let mask = Icon::parse(std::str::from_utf8(&svg).unwrap()).mask(edge as usize); - digest.update(edge.to_le_bytes()); - digest.update(edge.to_le_bytes()); - digest.update(mask.alpha()); + digest.update(&edge.to_le_bytes()).unwrap(); + digest.update(&edge.to_le_bytes()).unwrap(); + digest.update(mask.alpha()).unwrap(); } let hex: String = digest.finalize().iter().map(|b| format!("{b:02x}")).collect(); assert_eq!( diff --git a/system/pkg/Cargo.toml b/system/pkg/Cargo.toml index 5f9be099a3c..fe9a0da30ce 100644 --- a/system/pkg/Cargo.toml +++ b/system/pkg/Cargo.toml @@ -15,8 +15,7 @@ doctest = false # The room `/apps`'s volume has, which only its file server answers. toyos = { path = "../../sdk/toyos" } toyos-manifest = { path = "../../lib/manifest" } -toyos-sha2 = { path = "../../lib/sha2" } -toyos-sha2-hw = { path = "../../lib/sha2-hw" } +toyos-crypto = { path = "../../lib/crypto" } flate2 = { version = "1", default-features = false, features = ["rust_backend"] } [dev-dependencies] diff --git a/system/pkg/src/main.rs b/system/pkg/src/main.rs index 5133a1660e9..126ce12a3c0 100644 --- a/system/pkg/src/main.rs +++ b/system/pkg/src/main.rs @@ -212,13 +212,13 @@ fn gunzip(r: R) -> MultiGzDecoder> { /// A file read through a SHA-256 of every byte read. struct Hashed { file: fs::File, - hash: toyos_sha2::Sha256, + hash: toyos_crypto::hash::Sha256, } impl Hashed { fn open(path: &Path) -> Result { let file = fs::File::open(path).map_err(|e| format!("pkg: cannot read {}: {e}", path.display()))?; - Ok(Self { file, hash: toyos_sha2_hw::sha256() }) + Ok(Self { file, hash: toyos_crypto::hash::Sha256::new() }) } /// The whole file's digest: what was read, and the rest, read now. A tar @@ -233,7 +233,7 @@ impl Hashed { impl Read for Hashed { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let n = self.file.read(buf)?; - self.hash.update(&buf[..n]); + self.hash.update(&buf[..n]).map_err(|_| std::io::Error::other("a file past SHA-256's 2^61-byte bound"))?; Ok(n) } } @@ -459,7 +459,7 @@ mod tests { }; let mut whole = tool(0); whole.extend(vec![0u8; 3 * CHUNK]); - assert_eq!(survey_of(&whole).unwrap(), sums::hex(&toyos_sha2_hw::sha256_digest(&whole))); + assert_eq!(survey_of(&whole).unwrap(), sums::hex(&toyos_crypto::hash::Sha256::digest(&whole).unwrap())); assert!(survey_of(&whole[..tool(0).len() / 3]).unwrap_err().contains("tar:")); assert!(survey_of(b"not gzip at all").is_err()); } diff --git a/system/update/Cargo.toml b/system/update/Cargo.toml index 1ee276091c9..bd9d7be2be7 100644 --- a/system/update/Cargo.toml +++ b/system/update/Cargo.toml @@ -17,7 +17,7 @@ toyos-update = { path = "../../lib/update" } # A slot's volume is FAT, written with the driver the kernel mounts FAT with. toyos-fat32 = { path = "../../lib/fat32" } # ROOT is hashed as it streams onto its partition, never held whole. -toyos-sha2-hw = { path = "../../lib/sha2-hw" } +toyos-crypto = { path = "../../lib/crypto" } [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/system/update/copies/Cargo.toml b/system/update/copies/Cargo.toml index d567cb0e3c7..ef6de85057a 100644 --- a/system/update/copies/Cargo.toml +++ b/system/update/copies/Cargo.toml @@ -18,7 +18,7 @@ toyos-disk = { path = "../../../lib/disk" } # The tests sign the image they install, as the host that holds the key does. toyos-update = { path = "../../../lib/update", features = ["sign"] } toyos-fat32 = { path = "../../../lib/fat32" } -toyos-sha2-hw = { path = "../../../lib/sha2-hw" } +toyos-crypto = { path = "../../../lib/crypto" } # The volume an install wrote, judged by the checker that shares no code with # the driver it was written with. toyos-fat32-check = { path = "../../../lib/fat32/check" } diff --git a/system/update/src/install/mod.rs b/system/update/src/install/mod.rs index d38f6f1547f..d4c86cfc19f 100644 --- a/system/update/src/install/mod.rs +++ b/system/update/src/install/mod.rs @@ -139,7 +139,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_hw::sha256(); + let mut hasher = toyos_crypto::hash::Sha256::new(); let mut run = vec![0u8; STREAM_BLOCKS * BLOCK]; let blocks = len / BLOCK as u64; let mut at = 0u64; @@ -147,7 +147,7 @@ fn stream_root(input: &mut impl Read, root: &mut dyn Disk, len: u64, sha256: toy let n = (blocks - at).min(STREAM_BLOCKS as u64) as usize; let bytes = &mut run[..n * BLOCK]; input.read_exact(bytes).map_err(|e| format!("the input ended inside ROOT, at block {at}: {e}"))?; - hasher.update(&*bytes); + hasher.update(bytes).map_err(|_| "ROOT runs past SHA-256's 2^61-byte bound".to_string())?; root.write(at, bytes).map_err(|e| format!("ROOT's blocks from {at} would not write: {e:?}"))?; at += n as u64; } diff --git a/lib/crypto/tests/data/nist-LICENSE b/tests/acvp/nist-LICENSE similarity index 100% rename from lib/crypto/tests/data/nist-LICENSE rename to tests/acvp/nist-LICENSE diff --git a/lib/crypto/tests/data/nist-hmac-sha1.json b/tests/acvp/nist-hmac-sha1.json similarity index 100% rename from lib/crypto/tests/data/nist-hmac-sha1.json rename to tests/acvp/nist-hmac-sha1.json diff --git a/lib/crypto/tests/data/nist-hmac-sha256.json b/tests/acvp/nist-hmac-sha256.json similarity index 100% rename from lib/crypto/tests/data/nist-hmac-sha256.json rename to tests/acvp/nist-hmac-sha256.json diff --git a/lib/crypto/tests/data/nist-hmac-sha384.json b/tests/acvp/nist-hmac-sha384.json similarity index 100% rename from lib/crypto/tests/data/nist-hmac-sha384.json rename to tests/acvp/nist-hmac-sha384.json diff --git a/lib/crypto/tests/data/nist-hmac-sha512.json b/tests/acvp/nist-hmac-sha512.json similarity index 100% rename from lib/crypto/tests/data/nist-hmac-sha512.json rename to tests/acvp/nist-hmac-sha512.json diff --git a/lib/crypto/tests/data/nist-mlkem-encapdecap.json b/tests/acvp/nist-mlkem-encapdecap.json similarity index 100% rename from lib/crypto/tests/data/nist-mlkem-encapdecap.json rename to tests/acvp/nist-mlkem-encapdecap.json diff --git a/lib/crypto/tests/data/nist-mlkem-keygen.json b/tests/acvp/nist-mlkem-keygen.json similarity index 100% rename from lib/crypto/tests/data/nist-mlkem-keygen.json rename to tests/acvp/nist-mlkem-keygen.json diff --git a/tests/common/hostedclang.rs b/tests/common/hostedclang.rs index 3178859ed1e..d206881d1da 100644 --- a/tests/common/hostedclang.rs +++ b/tests/common/hostedclang.rs @@ -59,7 +59,7 @@ pub fn hosted_clang_hello() -> Result<(), String> { stage("src/hello.c", &sources.join("hello.c"))?; for (name, product) in [("hello.o", &object), ("a.out", &linked)] { let bytes = fs::read(product).map_err(|e| format!("read {}: {e}", product.display()))?; - let sha256: String = toyos_sha2_hw::sha256_digest(&bytes).iter().map(|b| format!("{b:02x}")).collect(); + let sha256 = toyos_build::sha256_hex(&bytes); eprintln!(" [{JOB}] the host's {name}: {} bytes, sha256 {sha256}", bytes.len()); stage(&format!("host/{name}"), product)?; }