From 019215c4b404510ed1f6ab48bed8ce843201b8b8 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 16:52:07 +0200 Subject: [PATCH 1/8] An unchanged ureq/rustls client on a ring fork fetches over TLS 1.3 in a guest ring is ToyOSOrg/ring's toyos-0.17.14, pinned by the lock: the published 0.17.14 package imported byte for byte, a build script that tells a packaged tree from a source tree by `pregenerated/` instead of `.git` (so as a git dependency it uses the package's assembly, runs no perl, and leaves C asserts and warnings-as-errors off, so nothing calls `__assert_fail`), and `toyos` in its Linux-ABI and getrandom lists. The root workspace and the guest test crate both patch it; the test crate also takes the getrandom 0.2 fork, which reaches SYS_RANDOM. The build writes webpki-root-certs' Mozilla roots to every ROOT as /system/etc/ssl/cert.pem (build::TRUST_ROOTS). A test image stages that file whole, the build's roots with its harness's authority after them, and the build refuses one that does not start with its own. A test crate's C is compiled by the toolchain's clang against libc's sysroot, as a program's is: TestBuild hands cargo the cc crate's variables. https_fetch boots tests/netcase with three rustls servers on the host, each under an authority made for the run: https_get, ureq 3 on rustls 0.23 with the roots file and the project's User-Agent, fetches 384 KiB over TLS 1.3 and its ring SHA-256 matches the harness's sha2 one; it refuses a certificate for another address and one from an authority the file does not hold, by name. ring_kat runs on the shared boot: ring against RFC and NIST known answers. doom's build script downloads with ureq on ring and the project's User-Agent; nothing names rustls-rustcrypto. Filed: a refused connection's close that never reached the host's peer, and the roots being data no licence gate reads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- Cargo.lock | 659 ++++++++---------- Cargo.toml | 9 + ...ed-handshake-can-leave-its-peer-waiting.md | 40 ++ issues/the-internet-clients-work-unchanged.md | 34 +- ...ry-image-and-no-licence-gate-reads-them.md | 25 + src/build.rs | 73 +- tests/common/https.rs | 165 +++++ tests/common/mod.rs | 2 + tests/toyos-rust-tests/Cargo.lock | 260 ++++++- tests/toyos-rust-tests/Cargo.toml | 7 + tests/toyos-rust-tests/src/bin/https_get.rs | 77 ++ tests/toyos-rust-tests/src/bin/ring_kat.rs | 166 +++++ tests/toyos.rs | 86 +++ userland/doom/Cargo.toml | 4 +- userland/doom/build.rs | 16 +- 15 files changed, 1211 insertions(+), 412 deletions(-) create mode 100644 issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md create mode 100644 issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md create mode 100644 tests/common/https.rs create mode 100644 tests/toyos-rust-tests/src/bin/https_get.rs create mode 100644 tests/toyos-rust-tests/src/bin/ring_kat.rs diff --git a/Cargo.lock b/Cargo.lock index 3be3dc36d98..6c58b651222 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,45 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -230,12 +269,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base16ct" version = "1.0.0" @@ -278,6 +311,15 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -546,19 +588,6 @@ dependencies = [ "rand_core 0.10.0", ] -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead 0.5.2", - "chacha20 0.9.1", - "cipher 0.4.4", - "poly1305", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.44" @@ -580,7 +609,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", "inout 0.1.4", - "zeroize", ] [[package]] @@ -821,18 +849,6 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-bigint" version = "0.7.3" @@ -878,7 +894,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" dependencies = [ - "crypto-bigint 0.7.3", + "crypto-bigint", "libm", "rand_core 0.10.0", ] @@ -930,7 +946,6 @@ dependencies = [ "fiat-crypto 0.2.9", "rustc_version", "subtle", - "zeroize", ] [[package]] @@ -1045,26 +1060,35 @@ dependencies = [ [[package]] name = "der" -version = "0.7.10" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468 0.7.0", + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] [[package]] -name = "der" -version = "0.8.0" +name = "der-parser" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "const-oid 0.10.2", - "pem-rfc7468 1.0.0", - "zeroize", + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "derive-into-owned" version = "0.2.0" @@ -1127,6 +1151,17 @@ dependencies = [ "objc2 0.6.4", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "dlib" version = "0.5.3" @@ -1154,12 +1189,10 @@ dependencies = [ "cc", "cpal", "flate2", - "rustls-rustcrypto", "rustysynth", "softbuffer", "tar", "ureq", - "webpki-roots", "winit", ] @@ -1180,32 +1213,18 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der 0.7.10", - "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", -] - [[package]] name = "ecdsa" version = "0.17.0-rc.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91bbdd377139884fafcad8dc43a760a3e1e681aa26db910257fa6535b70e1829" dependencies = [ - "der 0.8.0", + "der", "digest 0.11.1", - "elliptic-curve 0.14.0-rc.29", - "rfc6979 0.5.0-rc.5", + "elliptic-curve", + "rfc6979", "signature 3.0.0-rc.10", - "spki 0.8.0-rc.4", + "spki", "zeroize", ] @@ -1215,7 +1234,6 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8 0.10.2", "signature 2.2.0", ] @@ -1225,7 +1243,7 @@ version = "3.0.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" dependencies = [ - "pkcs8 0.11.0-rc.11", + "pkcs8", "signature 3.0.0-rc.10", ] @@ -1237,10 +1255,8 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek 4.1.3", "ed25519 2.2.3", - "serde", "sha2 0.10.9", "subtle", - "zeroize", ] [[package]] @@ -1268,46 +1284,25 @@ dependencies = [ "toyos-window", ] -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", - "digest 0.10.7", - "ff", - "generic-array 0.14.7", - "group", - "hkdf 0.12.4", - "pem-rfc7468 0.7.0", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1 0.7.3", - "subtle", - "zeroize", -] - [[package]] name = "elliptic-curve" version = "0.14.0-rc.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e84043d573efd4ac9d2d125817979a379204bf7e328b25a4a30487e8d100e618" dependencies = [ - "base16ct 1.0.0", - "crypto-bigint 0.7.3", + "base16ct", + "crypto-bigint", "crypto-common 0.2.1", "digest 0.11.1", - "hkdf 0.13.0-rc.5", + "hkdf", "hybrid-array", "once_cell", "pem-rfc7468 1.0.0", - "pkcs8 0.11.0-rc.11", + "pkcs8", "rand_core 0.10.0", "rustcrypto-ff", "rustcrypto-group", - "sec1 0.8.1", + "sec1", "subtle", "zeroize", ] @@ -1411,16 +1406,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1672,7 +1657,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -2565,17 +2549,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "hash32" version = "0.3.1" @@ -2658,15 +2631,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - [[package]] name = "hkdf" version = "0.13.0-rc.5" @@ -2825,18 +2789,18 @@ checksum = "25f8a978272e3cbdf4768f7363eb1c8e1e6ba63c52a3ed05e29e222da4aec7cb" dependencies = [ "argon2", "bcrypt-pbkdf", - "crypto-bigint 0.7.3", - "ecdsa 0.17.0-rc.16", + "crypto-bigint", + "ecdsa", "ed25519-dalek 3.0.0-pre.6", "hex", "hmac 0.13.0-rc.5", "num-bigint-dig", - "p256 0.14.0-rc.8", - "p384 0.14.0-rc.8", + "p256", + "p384", "p521", "rand_core 0.10.0", - "rsa 0.10.0-rc.17", - "sec1 0.8.1", + "rsa", + "sec1", "sha1 0.11.0-rc.5", "sha2 0.11.0-rc.5", "signature 3.0.0-rc.10", @@ -3254,6 +3218,12 @@ dependencies = [ "toyos-tco", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3384,6 +3354,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nonempty" version = "0.12.0" @@ -3399,6 +3379,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -3413,7 +3403,6 @@ dependencies = [ "rand 0.8.5", "serde", "smallvec", - "zeroize", ] [[package]] @@ -3425,6 +3414,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -3463,7 +3458,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3820,6 +3814,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3842,54 +3845,30 @@ dependencies = [ "libredox", ] -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - [[package]] name = "p256" version = "0.14.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44f0a10fe314869359cb2901342b045f4e5a962ef9febc006f03d2a8c848fe4c" dependencies = [ - "ecdsa 0.17.0-rc.16", - "elliptic-curve 0.14.0-rc.29", + "ecdsa", + "elliptic-curve", "primefield", - "primeorder 0.14.0-rc.8", + "primeorder", "sha2 0.11.0-rc.5", ] -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - [[package]] name = "p384" version = "0.14.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b079e66810c55ab3d6ba424e056dc4aefcdb8046c8c3f3816142edbdd7af7721" dependencies = [ - "ecdsa 0.17.0-rc.16", - "elliptic-curve 0.14.0-rc.29", + "ecdsa", + "elliptic-curve", "fiat-crypto 0.3.0", "primefield", - "primeorder 0.14.0-rc.8", + "primeorder", "sha2 0.11.0-rc.5", ] @@ -3899,11 +3878,11 @@ version = "0.14.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eecc34c4c6e6596d5271fecf90ac4f16593fa198e77282214d0c22736aa9266" dependencies = [ - "base16ct 1.0.0", - "ecdsa 0.17.0-rc.16", - "elliptic-curve 0.14.0-rc.29", + "base16ct", + "ecdsa", + "elliptic-curve", "primefield", - "primeorder 0.14.0-rc.8", + "primeorder", "sha2 0.11.0-rc.5", ] @@ -3968,12 +3947,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.12.2" @@ -4005,6 +3978,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -4061,35 +4044,14 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - [[package]] name = "pkcs1" version = "0.8.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" dependencies = [ - "der 0.8.0", - "spki 0.8.0-rc.4", -] - -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", ] [[package]] @@ -4101,23 +4063,12 @@ dependencies = [ "aes 0.9.0-rc.4", "aes-gcm 0.11.0-rc.3", "cbc 0.2.0-rc.4", - "der 0.8.0", + "der", "pbkdf2 0.13.0-rc.9", "rand_core 0.10.0", "scrypt", "sha2 0.11.0-rc.5", - "spki 0.8.0-rc.4", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "pkcs5 0.7.1", - "spki 0.7.3", + "spki", ] [[package]] @@ -4126,10 +4077,10 @@ version = "0.11.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" dependencies = [ - "der 0.8.0", - "pkcs5 0.8.0-rc.13", + "der", + "pkcs5", "rand_core 0.10.0", - "spki 0.8.0-rc.4", + "spki", ] [[package]] @@ -4230,6 +4181,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4264,7 +4221,7 @@ version = "0.14.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6543f5eec854fbf74ba5ef651fbdc9408919b47c3e1526623687135c16d12e9" dependencies = [ - "crypto-bigint 0.7.3", + "crypto-bigint", "crypto-common 0.2.1", "rand_core 0.10.0", "rustcrypto-ff", @@ -4272,22 +4229,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve 0.13.8", -] - [[package]] name = "primeorder" version = "0.14.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "569d9ad6ef822bb0322c7e7d84e5e286244050bd5246cac4c013535ae91c2c90" dependencies = [ - "elliptic-curve 0.14.0-rc.29", + "elliptic-curve", ] [[package]] @@ -4458,6 +4406,20 @@ name = "raw-window-handle" version = "0.6.2" source = "git+https://github.com/ToyOSOrg/raw-window-handle?branch=toyos-0.6.2#2a5d102bdbd5dc987b653a7ffff5bedbbbfc53b3" +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "realfft" version = "3.5.0" @@ -4525,16 +4487,6 @@ dependencies = [ "usvg", ] -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac 0.12.1", - "subtle", -] - [[package]] name = "rfc6979" version = "0.5.0-rc.5" @@ -4557,8 +4509,7 @@ dependencies = [ [[package]] name = "ring" version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +source = "git+https://github.com/ToyOSOrg/ring?branch=toyos-0.17.14#34818c4599e7d6233cf42e80de5cd309190e523c" dependencies = [ "cc", "cfg-if", @@ -4577,27 +4528,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1 0.7.5", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sha2 0.10.9", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", -] - [[package]] name = "rsa" version = "0.10.0-rc.17" @@ -4605,15 +4535,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87ed3e93fc7e473e464b9726f4759659e72bc8665e4b8ea227547024f416d905" dependencies = [ "const-oid 0.10.2", - "crypto-bigint 0.7.3", + "crypto-bigint", "crypto-primes", "digest 0.11.1", - "pkcs1 0.8.0-rc.4", - "pkcs8 0.11.0-rc.11", + "pkcs1", + "pkcs8", "rand_core 0.10.0", "sha2 0.11.0-rc.5", "signature 3.0.0-rc.10", - "spki 0.8.0-rc.4", + "spki", "zeroize", ] @@ -4643,16 +4573,16 @@ dependencies = [ "cbc 0.1.2", "chacha20 0.9.1", "cipher 0.5.1", - "crypto-bigint 0.7.3", + "crypto-bigint", "ctr 0.9.2", "curve25519-dalek 5.0.0-pre.6", "data-encoding", "delegate", - "der 0.8.0", + "der", "digest 0.10.7", - "ecdsa 0.17.0-rc.16", + "ecdsa", "ed25519-dalek 3.0.0-pre.6", - "elliptic-curve 0.14.0-rc.29", + "elliptic-curve", "enum_dispatch", "flate2", "futures", @@ -4667,26 +4597,26 @@ dependencies = [ "md5", "ml-kem", "module-lattice", - "p256 0.14.0-rc.8", - "p384 0.14.0-rc.8", + "p256", + "p384", "p521", "pageant", "pbkdf2 0.12.2", - "pkcs1 0.8.0-rc.4", - "pkcs5 0.8.0-rc.13", - "pkcs8 0.11.0-rc.11", + "pkcs1", + "pkcs5", + "pkcs8", "poly1305", "polyval 0.7.1", "rand 0.10.0", "rand_core 0.10.0", - "rsa 0.10.0-rc.17", + "rsa", "russh-cryptovec", "russh-util", - "sec1 0.8.1", + "sec1", "sha1 0.10.6", "sha2 0.10.9", "signature 3.0.0-rc.10", - "spki 0.8.0-rc.4", + "spki", "ssh-encoding", "subtle", "thiserror 2.0.18", @@ -4774,6 +4704,15 @@ dependencies = [ "transpose", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4810,7 +4749,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.15", + "rustls-webpki", "subtle", "zeroize", ] @@ -4824,47 +4763,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-rustcrypto" -version = "0.0.2-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12052947763ab8515f753315357599e9b0b4dab3b8ba15f30f725fe6d025557" -dependencies = [ - "aead 0.5.2", - "aes-gcm 0.10.3", - "chacha20poly1305", - "crypto-common 0.1.7", - "der 0.7.10", - "digest 0.10.7", - "ecdsa 0.16.9", - "ed25519-dalek 2.2.0", - "hmac 0.12.1", - "p256 0.13.2", - "p384 0.13.1", - "paste", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "rsa 0.9.10", - "rustls", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "sec1 0.7.3", - "sha2 0.10.9", - "signature 2.2.0", - "x25519-dalek", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.15" @@ -4931,29 +4829,15 @@ dependencies = [ "sha2 0.11.0-rc.5", ] -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct 0.2.0", - "der 0.7.10", - "generic-array 0.14.7", - "pkcs8 0.10.2", - "subtle", - "zeroize", -] - [[package]] name = "sec1" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ - "base16ct 1.0.0", + "base16ct", "ctutils", - "der 0.8.0", + "der", "hybrid-array", "subtle", "zeroize", @@ -5023,7 +4907,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" dependencies = [ - "base16ct 1.0.0", + "base16ct", "serde", ] @@ -5115,10 +4999,6 @@ name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] [[package]] name = "signature" @@ -5295,16 +5175,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "spki" version = "0.8.0-rc.4" @@ -5312,7 +5182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" dependencies = [ "base64ct", - "der 0.8.0", + "der", ] [[package]] @@ -5463,6 +5333,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tar" version = "0.4.46" @@ -5554,6 +5435,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.12.0" @@ -5751,6 +5662,7 @@ name = "toyos-build" version = "0.1.0" dependencies = [ "acpiserver-api", + "base64 0.22.1", "bcachefs", "fatfs", "flate2", @@ -5761,6 +5673,8 @@ dependencies = [ "image", "kernel", "libc", + "rcgen", + "rustls", "serde", "serde_json", "sha2 0.10.9", @@ -5787,6 +5701,7 @@ dependencies = [ "toyos-xhci", "ureq", "uuid", + "webpki-root-certs", ] [[package]] @@ -6675,6 +6590,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -7096,14 +7020,21 @@ dependencies = [ ] [[package]] -name = "x25519-dalek" -version = "2.0.1" +name = "x509-parser" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "curve25519-dalek 4.1.3", - "rand_core 0.6.4", - "zeroize", + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", ] [[package]] @@ -7147,6 +7078,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + [[package]] name = "zerocopy" version = "0.8.47" @@ -7172,20 +7113,6 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zlib-rs" diff --git a/Cargo.toml b/Cargo.toml index 70e32e5f104..1a9035819df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -217,6 +217,10 @@ ureq = { version = "3", default-features = false, features = ["rustls"] } # The commit an image is built from, its time and whether the tree is that # commit's, where `git` is a host's C tool (`src/build.rs`'s `release`). gix = { version = "0.88", default-features = false, features = ["status", "sha1"] } +# The authorities `build::trust_roots` writes to ROOT, as the rustls project +# publishes the Mozilla root program, and the PEM it writes them in. +webpki-root-certs = "1" +base64 = "0.22" [dev-dependencies] # `USER_TOP`, so the harness judges a held `rsp` against the bound the kernel @@ -231,6 +235,10 @@ toyos-xhci = { path = "toyos-xhci" } # The ACPI server's count line, so the judge reads the words the server writes # and the job that waits for them matches. acpiserver-api = { path = "userland/acpiserver-api" } +# `common::https`'s server, the peer a guest's HTTPS client is judged +# against, and the authority and certificates it presents, made per run. +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } +rcgen = "0.14" [patch.crates-io] loom = { git = "https://github.com/ToyOSOrg/loom", branch = "toyos" } @@ -253,6 +261,7 @@ raw-window-handle = { git = "https://github.com/ToyOSOrg/raw-window-handle", bra cpal = { git = "https://github.com/ToyOSOrg/cpal", branch = "toyos-0.18.0" } softbuffer = { git = "https://github.com/ToyOSOrg/softbuffer", branch = "toyos-0.4.8" } winit = { git = "https://github.com/ToyOSOrg/winit", branch = "toyos-0.30.13" } +ring = { git = "https://github.com/ToyOSOrg/ring", branch = "toyos-0.17.14" } # The one profile every guest binary is built with. # Optimised, because an unoptimised guest mismeasures everything under TCG; diff --git a/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md new file mode 100644 index 00000000000..bf107395cea --- /dev/null +++ b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md @@ -0,0 +1,40 @@ +--- +status: open +kind: defect +opened: 2026-10-09 +--- + +# A guest's close after a refused handshake can leave its peer waiting + +`https_fetch` (`tests/toyos.rs`) runs `https_get` three times on one boot of +`tests/netcase`, each a new process and one TCP connection through +`/system/bin/netstack` (smoltcp, the stack `main` ships) to a `rustls` server +on the host. The second and third runs refuse the server's certificate during +the handshake, print their refusal, drop the connection and exit 2. On the +host, the server's read of the handshake's next record is answered by +nothing: in one boot its peer's end of stream arrived and in the other it did +not, 180 seconds after the process exited, no FIN and no reset. netstack +logged nothing about either connection. + +The same client and server both on the host (macOS) see each refused +connection end at once with an end of stream and no alert: the client sends +no alert, so the close is all the peer is owed, and it is what is missing. + +Measured at `a944746d5` plus the branch that added `https_fetch`, under QEMU +with slirp, once (logs on that branch's pull request): the +second connection of the boot never ended at the peer; the third did, by the +end of the 180 seconds. Two earlier boots of the same test reported the +second connection silent after 30 seconds. Not reduced: whether it is the +connection's state when its owner exits mid-handshake (the server's flight +unread, nothing sent since the client's first flight), or the second +connection of a boot, is unmeasured, and so is whether it reaches the stack +the move installs (`issues/toyos-has-its-own-network-stack.md`). +`OWNERLESS_LIFE` (`userland/netstack/src/main.rs`) should have reset it at 100 +seconds and no reset arrived either. + +**Exit condition**: a guest test whose process closes a connection mid- +handshake, with the peer's flight unread, and exits, sees the host's peer +read the end of the stream within a stated bound, on each of several +connections in one boot. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. diff --git a/issues/the-internet-clients-work-unchanged.md b/issues/the-internet-clients-work-unchanged.md index 3385af0af72..7b32241e42c 100644 --- a/issues/the-internet-clients-work-unchanged.md +++ b/issues/the-internet-clients-work-unchanged.md @@ -33,27 +33,19 @@ netd, `toyos::net` and std's ToyOS networking in the `rust/` fork. program on `rustls`; the program and its judge's server installed `rustls-rustcrypto` 0.0.2-alpha, and #660 cut both. The row comes back on `ring` and waits for it; `rustls-rustcrypto` does not come back (owner, - 2026-10-02). Published `ring` 0.17.14 does not link for - `x86_64-unknown-toyos`: its `build.rs` picks its assembly by an OS list - that lacks `toyos`, and `src/rand.rs` has an OS list of its own. With - `"toyos"` added to `build.rs`'s `LINUX_ABI` and `target_os = "toyos"` to - `src/rand.rs` it builds for both ToyOS targets. In an x86-64 QEMU guest - it passes known answers (SHA-256, - SHA-512, HMAC, X25519, ChaCha20-Poly1305, AES-GCM, Ed25519, P-256, - `SystemRandom`), and `ureq` 3.4.2 on `rustls` 0.23.45 fetches 320000 - bytes over TLS 1.3 from a server on the host and refuses a wrong name and - an untrusted root (#682, comment 5968053596). What stands before it - lands: a git fork's - `build.rs` runs `perl`, an arrival - `issues/the-build-runs-host-tools-outside-rust-and-qemu.md` does not - declare, and leaves C asserts on, so `__assert_fail` is undefined unless - ring builds with `debug = false` or `toyos_c` is linked; `src/build.rs` - gives the C compiler's environment (`cc_env`) to userland builds alone, so - a test crate's C is compiled by the host's `cc`; and not run: AArch64, the - T14, a `git =` dependency, the licence gate over ring in an image. - `rustls-rustcrypto` is - still named by doom's build script, which installs it on the host. - Open: what doom's build script installs instead. + 2026-10-02). `ring` is `ToyOSOrg/ring`'s `toyos-0.17.14`: the published + 0.17.14 package byte for byte, its build script telling a packaged tree + from a source tree by `pregenerated/` rather than `.git` (so a `git` + dependency runs no `perl`, and its C asserts and warnings-as-errors stay + off, leaving nothing to call `__assert_fail`), and `toyos` in `LINUX_ABI` + and in `src/rand.rs`'s `getrandom` list. The build writes the Mozilla + roots of `webpki-root-certs` to ROOT as `/system/etc/ssl/cert.pem` + (`build::TRUST_ROOTS`), and a test image stages that file with its + harness's authority after them. In an x86-64 QEMU guest `ring_kat` + passes known answers, and `https_fetch` has an unchanged `ureq` 3 on + `rustls` 0.23 fetch a body over TLS 1.3 byte-exact, send the project's + `User-Agent`, and refuse a wrong name and an untrusted root. Not run: + AArch64, the T14. **Exit**: `https_tls13` is a `METAL` row: on the T14's I219 an unmodified `ureq` and `rustls` client on the `ring` provider fetches over TLS 1.3 from a server the harness runs, and refuses a wrong name and an untrusted diff --git a/issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md b/issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md new file mode 100644 index 00000000000..e3a1b827ff8 --- /dev/null +++ b/issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md @@ -0,0 +1,25 @@ +--- +status: open +kind: tooling +opened: 2026-10-09 +--- + +# The trust roots ship in every image and no licence gate reads them + +`build::trust_roots` (`src/build.rs`) writes the Mozilla root program's +authorities, as the `webpki-root-certs` crate publishes them, to every ROOT as +`/system/etc/ssl/cert.pem`. That crate's licence is `CDLA-Permissive-2.0`, +which `licence::ALLOWED` does not name, and the gate (`src/licence.rs`) never +sees it: it judges the crates an image's programs link and the files the tree +commits, and the roots are neither — a dependency of the build system whose +data the build copies into the image. The image carries no copy of that +licence's text. + +**Exit**: the licence gate judges data the build writes into an image from a +third-party crate, and the roots file passes it: under a licence `ALLOWED` +names, or an `EXCEPTIONS` row, with whatever notice its licence asks of a +redistribution in the image. + +**Owner**: the internet-clients track +(`issues/the-internet-clients-work-unchanged.md`), whose stage 3 put the file +there. diff --git a/src/build.rs b/src/build.rs index b383792baab..0e6586d6367 100644 --- a/src/build.rs +++ b/src/build.rs @@ -710,6 +710,27 @@ fn release(root: &Path, toolchain: &crate::keystore::Key, arch: Arch) -> toyos_o } } +/// Where on ROOT the certificate authorities every TLS client trusts are: +/// `/system/etc/ssl/cert.pem`. +pub const TRUST_ROOTS: &str = "etc/ssl/cert.pem"; + +/// The Mozilla root program's authorities as `webpki-root-certs` publishes +/// them, each certificate a PEM block in the published order. +pub fn trust_roots() -> Vec { + use base64::Engine; + let mut pem = String::new(); + for cert in webpki_root_certs::TLS_SERVER_ROOT_CERTS { + pem.push_str("-----BEGIN CERTIFICATE-----\n"); + let body = base64::engine::general_purpose::STANDARD.encode(cert.as_ref()); + for line in body.as_bytes().chunks(64) { + pem.push_str(std::str::from_utf8(line).expect("base64 is ASCII")); + pem.push('\n'); + } + pem.push_str("-----END CERTIFICATE-----\n"); + } + pem.into_bytes() +} + fn build_and_assemble( root: &Path, config: &SystemConfig, @@ -731,6 +752,17 @@ fn build_and_assemble( root_files.extend(assets::collect(&config.assets, &programs)); } + // A test image's roots are these and its harness's CA after them, staged + // whole under the same name, so the client reads one file either way. + let roots = trust_roots(); + match extra_files.iter().find(|(name, _)| name == TRUST_ROOTS) { + Some((_, staged)) => assert!( + staged.starts_with(&roots), + "a test image staged {TRUST_ROOTS} with other than the build's roots at its head" + ), + None => root_files.push((TRUST_ROOTS.to_string(), roots)), + } + // Extra files (test binaries, shared libs) for (name, data) in extra_files { root_files.push((name.clone(), data.clone())); @@ -1991,7 +2023,7 @@ pub fn build_toyos_bins(root: &Path, arch: Arch, crate_path: &Path, quiet: bool) if !quiet { eprintln!("[build] Building cdylib subcrate: {lib_name}"); } - cargo_build(&sub_path, target, &[], env, &[], quiet); + cargo_build(&sub_path, target, &[], env, &build.cc_env(), quiet); let lib_out = sub_path.join(format!("target/{target}/{PROFILE}")); lib_search_dirs.push(lib_out.clone()); @@ -2013,11 +2045,10 @@ pub fn build_toyos_bins(root: &Path, arch: Arch, crate_path: &Path, quiet: bool) for dir in &lib_search_dirs { link_flags.push_str(&format!("-L {} ", dir.display())); } - let extra_env: Vec<(&str, &str)> = if link_flags.is_empty() { - vec![] - } else { - vec![("RUSTFLAGS", link_flags.trim_end())] - }; + let mut extra_env = build.cc_env(); + if !link_flags.is_empty() { + extra_env.push(("RUSTFLAGS", link_flags.trim_end())); + } cargo_build(crate_path, target, &["--bins"], env, &extra_env, quiet); let bin_dir = crate_path.join(format!("target/{target}/{PROFILE}")); @@ -2055,6 +2086,9 @@ pub fn build_toyos_bins(root: &Path, arch: Arch, crate_path: &Path, quiet: bool) struct TestBuild { target: &'static str, env: GuestEnv, + /// `CSysroot::cc_env` for the target: a test crate's C is compiled as a + /// program's is (`build_programs`), never by the host's `cc`. + cc_env: Vec<(String, String)>, _lock: buildlock::Held, _artifact: buildlock::Guard, } @@ -2065,7 +2099,12 @@ impl TestBuild { let env = GuestEnv::new(toolchain::ensure(root, &mut lock)); invalidate_stale(&mut lock, &env.sysroot.identity, stale_targets); let artifact = buildlock::artifact(root); - TestBuild { target: arch.userland(), env, _lock: lock, _artifact: artifact } + let cc_env = crate::clang::CSysroot::of(env.sysroot.dir(), arch).cc_env(); + TestBuild { target: arch.userland(), env, cc_env, _lock: lock, _artifact: artifact } + } + + fn cc_env(&self) -> Vec<(&str, &str)> { + self.cc_env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect() } } @@ -2075,7 +2114,7 @@ pub fn build_toyos_bin(root: &Path, arch: Arch, crate_path: &Path, name: &str, q let _building = Building::start(format!("{} {name} of {}", arch.name(), hostws::rel(root, crate_path))); let build = TestBuild::begin(root, arch, "a test binary", &[crate_path.to_path_buf()]); let (target, env) = (build.target, &build.env); - cargo_build(crate_path, target, &["--bin", name], env, &[], quiet); + cargo_build(crate_path, target, &["--bin", name], env, &build.cc_env(), quiet); let binary = crate_path.join(format!("target/{target}/{PROFILE}/{name}")); fs::read(&binary).unwrap_or_else(|e| panic!("read the test binary {}: {e}", binary.display())) } @@ -2086,6 +2125,24 @@ pub fn build_toyos_bin(root: &Path, arch: Arch, crate_path: &Path, name: &str, q mod tests { use super::*; + /// **The roots file reads back, by the reader a client reads it with, as + /// every published root in the published order and nothing else**: a + /// line break or a padding byte the writer got wrong is a root no client + /// trusts, or a file no client reads. + #[test] + fn the_trust_roots_read_back_as_the_published_set() { + let read: Vec> = ureq::tls::parse_pem(&trust_roots()) + .map(|item| match item.expect("every block of the roots file parses") { + ureq::tls::PemItem::Certificate(cert) => cert.der().to_vec(), + _ => panic!("the roots file holds a PEM block that is not a certificate"), + }) + .collect(); + let published: Vec> = + webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().map(|cert| cert.to_vec()).collect(); + assert!(!published.is_empty()); + assert_eq!(read, published); + } + /// **The release a tree records is its commit, at that commit's own time, /// and dirty from the first file that is not that commit's**: an /// untracked file counts, and so does an edit to a tracked one, staged or diff --git a/tests/common/https.rs b/tests/common/https.rs new file mode 100644 index 00000000000..d526f25968c --- /dev/null +++ b/tests/common/https.rs @@ -0,0 +1,165 @@ +//! The HTTPS server a guest's client is judged against: `rustls` on `ring` on +//! the host, listening on loopback and reached from the guest through slirp at +//! [`HOST`], presenting a certificate an [`Authority`] made for this run. +//! +//! It is the client's peer, not its oracle: both ends are `rustls` on `ring`. +//! What it reports is what only the peer sees — that a connection arrived, +//! the protocol version the client agreed to and the `User-Agent` it sent. + +use std::io::{BufRead, BufReader, Write}; +use std::net::{IpAddr, TcpListener, TcpStream}; +use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use rcgen::{BasicConstraints, CertificateParams, CertifiedIssuer, DnType, IsCa, KeyPair, KeyUsagePurpose, SanType}; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; + +/// The host as slirp shows it to the guest; it carries a connection to this +/// address to the host's loopback. +pub const HOST: &str = "10.0.2.2"; + +/// The bytes the server answers every request with: several hundred KiB of a +/// fixed xorshift stream, so a dropped, duplicated or reordered segment +/// changes the hash, and the same every run. +pub fn body() -> Vec { + const BYTES: usize = 384 * 1024; + let mut state: u64 = 0x9e37_79b9_7f4a_7c15; + let mut body = Vec::with_capacity(BYTES); + while body.len() < BYTES { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + body.extend_from_slice(&state.to_le_bytes()); + } + body.truncate(BYTES); + body +} + +/// A certificate authority made for this run alone, whose key never leaves +/// this process. +pub struct Authority(CertifiedIssuer<'static, KeyPair>); + +impl Authority { + pub fn new(name: &str) -> Self { + let mut params = CertificateParams::default(); + params.distinguished_name.push(DnType::CommonName, name); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature]; + let key = KeyPair::generate().expect("an authority's key"); + Self(CertifiedIssuer::self_signed(params, key).expect("an authority's certificate")) + } + + /// Its certificate, as one PEM block. + pub fn pem(&self) -> String { + self.0.pem() + } + + /// A certificate for the one address `san` and its key, signed by this + /// authority. + pub fn leaf(&self, san: IpAddr) -> (CertificateDer<'static>, PrivateKeyDer<'static>) { + let mut params = CertificateParams::default(); + params.subject_alt_names = vec![SanType::IpAddress(san)]; + let key = KeyPair::generate().expect("a server's key"); + let cert = params.signed_by(&key, &self.0).expect("a server's certificate"); + (cert.der().clone(), PrivatePkcs8KeyDer::from(key.serialize_der()).into()) + } +} + +/// What the server saw of a connection: [`Seen::Accepted`] as it arrives, +/// then one of the other two as it ends. +#[derive(Debug)] +pub enum Seen { + Accepted, + /// A handshake that completed and a request answered with the whole body. + Served { version: Option, path: String, user_agents: Vec }, + /// A handshake the server could not complete, and why. + Failed(rustls::Error), +} + +/// A server on a loopback port of its own, answering each connection in turn +/// with [`body`]-shaped bytes, until this process ends. +pub struct Server { + pub port: u16, + seen: Receiver, +} + +impl Server { + pub fn start(cert: (CertificateDer<'static>, PrivateKeyDer<'static>), body: Arc>) -> Result { + let (chain, key) = cert; + let config = rustls::ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) + .with_safe_default_protocol_versions() + .and_then(|b| b.with_no_client_auth().with_single_cert(vec![chain], key)) + .map_err(|e| format!("the HTTPS server's configuration: {e}"))?; + let config = Arc::new(config); + let listener = TcpListener::bind(("127.0.0.1", 0)).map_err(|e| format!("the HTTPS server: {e}"))?; + let port = listener.local_addr().map_err(|e| format!("the HTTPS server's port: {e}"))?.port(); + let (tx, seen) = mpsc::channel(); + // Ends with the process, or once nobody reads what it saw. + thread::spawn(move || { + for tcp in listener.incoming() { + let tcp = tcp.expect("accept a connection on the HTTPS server's port"); + if tx.send(Seen::Accepted).is_err() || tx.send(serve(&config, tcp, &body)).is_err() { + return; + } + } + }); + Ok(Self { port, seen }) + } + + /// The next thing seen, waited for up to `within`. + pub fn next(&self, within: Duration) -> Result { + self.seen.recv_timeout(within).map_err(|e| match e { + RecvTimeoutError::Timeout => format!("the server on port {} saw nothing more within {within:?}", self.port), + RecvTimeoutError::Disconnected => format!("the HTTPS server on port {} is gone", self.port), + }) + } + + /// What it has seen past what was read, without waiting. + pub fn more(&self) -> Vec { + self.seen.try_iter().collect() + } +} + +/// One connection: the handshake, one request read to its blank line, and +/// the body after a header that gives its length. +fn serve(config: &Arc, mut tcp: TcpStream, body: &[u8]) -> Seen { + let mut conn = rustls::ServerConnection::new(config.clone()).expect("a server connection"); + while conn.is_handshaking() { + if let Err(e) = conn.complete_io(&mut tcp) { + let why = e.get_ref().and_then(|inner| inner.downcast_ref::()).cloned(); + return Seen::Failed(why.unwrap_or_else(|| rustls::Error::General(e.to_string()))); + } + } + let version = conn.protocol_version(); + let mut stream = rustls::Stream::new(&mut conn, &mut tcp); + let mut request = BufReader::new(&mut stream); + let mut first = String::new(); + request.read_line(&mut first).expect("the request line"); + let path = first.split_whitespace().nth(1).unwrap_or_default().to_string(); + let mut user_agents = Vec::new(); + loop { + let mut line = String::new(); + request.read_line(&mut line).expect("a request header"); + let line = line.trim_end(); + if line.is_empty() { + break; + } + if let Some((name, value)) = line.split_once(':') { + if name.eq_ignore_ascii_case("user-agent") { + user_agents.push(value.trim().to_string()); + } + } + } + drop(request); + let head = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len()); + stream.write_all(head.as_bytes()).expect("the response's head"); + stream.write_all(body).expect("the response's body"); + stream.flush().expect("the response"); + conn.send_close_notify(); + while conn.wants_write() { + conn.write_tls(&mut tcp).expect("the close_notify"); + } + Seen::Served { version, path, user_agents } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 291983fd0b6..1a4e52d7ea3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,6 +7,8 @@ pub mod compile; /// The device boot: what `tests/metaldevicecase` measures. pub mod devices; pub mod faults; +/// The HTTPS server a guest's client fetches from. +pub mod https; pub mod iommu; pub mod irqcensus; /// The `isa` claim's rows on the T14. diff --git a/tests/toyos-rust-tests/Cargo.lock b/tests/toyos-rust-tests/Cargo.lock index d7afd1b6254..84089081454 100644 --- a/tests/toyos-rust-tests/Cargo.lock +++ b/tests/toyos-rust-tests/Cargo.lock @@ -34,6 +34,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bitflags" version = "2.13.1" @@ -61,6 +67,16 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -144,6 +160,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + [[package]] name = "futures-core" version = "0.3.33" @@ -168,12 +190,39 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "git+https://github.com/ToyOSOrg/getrandom?branch=toyos-0.2-sdk-0.12#2092d1ccd2833f00559ed27850c576ab37fe9347" +dependencies = [ + "cfg-if", + "libc", + "toyos-abi", + "wasi", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "indexmap" version = "2.14.0" @@ -193,6 +242,12 @@ dependencies = [ "toyos-inspect", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jni" version = "0.22.4" @@ -479,6 +534,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -518,6 +579,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "git+https://github.com/ToyOSOrg/ring?branch=toyos-0.17.14#34818c4599e7d6233cf42e80de5cd309190e523c" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -527,6 +601,41 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -568,6 +677,12 @@ dependencies = [ "syn 3.0.2", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -590,6 +705,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -725,6 +846,8 @@ dependencies = [ "libloading", "logkeeper-api", "memmap2", + "ring", + "rustls", "toyos", "toyos-abi", "toyos-inspect", @@ -732,6 +855,7 @@ dependencies = [ "toyos-tco", "toyos-trace", "toyos-window", + "ureq", ] [[package]] @@ -761,6 +885,46 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "walkdir" version = "2.5.0" @@ -771,6 +935,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -826,13 +996,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -936,6 +1115,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -945,6 +1133,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + [[package]] name = "windows-threading" version = "0.2.1" @@ -954,6 +1158,54 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "1.0.4" @@ -962,3 +1214,9 @@ checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" diff --git a/tests/toyos-rust-tests/Cargo.toml b/tests/toyos-rust-tests/Cargo.toml index c4e1f7285d4..32329f52a83 100644 --- a/tests/toyos-rust-tests/Cargo.toml +++ b/tests/toyos-rust-tests/Cargo.toml @@ -24,6 +24,11 @@ inspect = { path = "../../userland/inspect" } libloading = { git = "https://github.com/ToyOSOrg/rust_libloading", branch = "toyos-sdk-0.12" } cpal = { git = "https://github.com/ToyOSOrg/cpal", branch = "toyos-0.18.0" } memmap2 = "0.9" +# An HTTPS client as any Rust program writes one, for `https_get`, and the +# primitives under it, for `ring_kat`'s known answers. +ureq = { version = "3", default-features = false, features = ["rustls"] } +rustls = { version = "0.23", default-features = false } +ring = "0.17" # Outside the workspace, so it needs its own redirects: the forks # above depend on ToyOS crates by version, and only a patch maps those to the @@ -32,6 +37,8 @@ memmap2 = "0.9" toyos-abi = { path = "../../toyos-abi" } toyos = { path = "../../toyos" } memmap2 = { git = "https://github.com/ToyOSOrg/memmap2-rs", branch = "toyos-0.9.11" } +ring = { git = "https://github.com/ToyOSOrg/ring", branch = "toyos-0.17.14" } +getrandom = { git = "https://github.com/ToyOSOrg/getrandom", branch = "toyos-0.2-sdk-0.12", package = "getrandom" } [lints.rust] warnings = "deny" diff --git a/tests/toyos-rust-tests/src/bin/https_get.rs b/tests/toyos-rust-tests/src/bin/https_get.rs new file mode 100644 index 00000000000..e72b78dde80 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/https_get.rs @@ -0,0 +1,77 @@ +//! An HTTPS client as any Rust program writes one: `ureq` on `rustls` on +//! `ring`, as published, trusting the authorities of one roots file and no +//! others, and naming itself with the project's `User-Agent`. +//! +//! argv: the URL, then the roots file. It says one line and ends: +//! `https_get: ok bytes= sha256=` and 0 for a body read to its end, +//! hashed by `ring` over every byte; `https_get: refused ` and 2 for a +//! certificate the client refused; anything else panics. + +use std::io::Read; +use std::process::ExitCode; + +use ureq::tls::{PemItem, RootCerts, TlsConfig}; + +/// Root `CLAUDE.md`'s: the only `User-Agent` ToyOS sends. +const USER_AGENT: &str = "toyos-build (https://github.com/ToyOSOrg/ToyOS)"; + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let [url, roots] = args.as_slice() else { + panic!("usage: https_get , not {args:?}"); + }; + let pem = std::fs::read(roots).unwrap_or_else(|e| panic!("read {roots}: {e}")); + let certs: Vec<_> = ureq::tls::parse_pem(&pem) + .map(|item| match item.unwrap_or_else(|e| panic!("{roots}: {e}")) { + PemItem::Certificate(cert) => cert, + _ => panic!("{roots} holds a PEM block that is not a certificate"), + }) + .collect(); + let tls = TlsConfig::builder().root_certs(RootCerts::new_with_certs(&certs)).build(); + let agent = ureq::Agent::config_builder().user_agent(USER_AGENT).tls_config(tls).build().new_agent(); + + let response = match agent.get(url).call() { + Ok(response) => response, + Err(e) => { + let Some(why) = refused(&e) else { panic!("GET {url}: {e}") }; + let why = match why { + rustls::CertificateError::NotValidForName | rustls::CertificateError::NotValidForNameContext { .. } => { + "not-valid-for-name".to_string() + } + rustls::CertificateError::UnknownIssuer => "unknown-issuer".to_string(), + other => format!("{other:?}"), + }; + println!("https_get: refused {why}"); + return ExitCode::from(2); + } + }; + let mut body = response.into_body().into_reader(); + let mut digest = ring::digest::Context::new(&ring::digest::SHA256); + let mut chunk = vec![0u8; 16 * 1024]; + let mut bytes = 0usize; + loop { + let n = body.read(&mut chunk).unwrap_or_else(|e| panic!("the body of {url}: {e}")); + if n == 0 { + break; + } + digest.update(&chunk[..n]); + bytes += n; + } + let hex: String = digest.finish().as_ref().iter().map(|b| format!("{b:02x}")).collect(); + println!("https_get: ok bytes={bytes} sha256={hex}"); + ExitCode::SUCCESS +} + +/// The certificate `rustls` refused, where that is what `e` is: ureq hands a +/// handshake's error back inside the I/O error of the read that met it. +fn refused(e: &ureq::Error) -> Option<&rustls::CertificateError> { + let tls = match e { + ureq::Error::Rustls(tls) => Some(tls), + ureq::Error::Io(io) => io.get_ref().and_then(|inner| inner.downcast_ref::()), + _ => None, + }; + match tls { + Some(rustls::Error::InvalidCertificate(why)) => Some(why), + _ => None, + } +} diff --git a/tests/toyos-rust-tests/src/bin/ring_kat.rs b/tests/toyos-rust-tests/src/bin/ring_kat.rs new file mode 100644 index 00000000000..bcbeb8c4d9b --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/ring_kat.rs @@ -0,0 +1,166 @@ +//! `ring`'s primitives against the known answers their specifications +//! publish: the oracle under every TLS connection a ToyOS program makes, whose +//! own handshake only tests `ring` against `ring` on the host. Each answer is +//! the cited document's, not one this program or `ring` computed. + +#![allow(deprecated, reason = "`ring::test::rand` is the one way to give an X25519 key known bytes")] + +use ring::{aead, agreement, digest, hmac, rand, signature}; + +fn hex(s: &str) -> Vec { + assert!(s.len() % 2 == 0, "odd hex {s:?}"); + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex")).collect() +} + +fn check(what: &str, got: &[u8], want: &str) { + assert_eq!(got, hex(want).as_slice(), "{what}"); + println!("ring_kat: {what} ok"); +} + +fn digests() { + // FIPS 180-2, appendix B.1 and C.1, and B.3. + check("sha256(abc)", digest::digest(&digest::SHA256, b"abc").as_ref(), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + check("sha512(abc)", digest::digest(&digest::SHA512, b"abc").as_ref(), + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a\ + 2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"); + check("sha256(a * 1000000)", digest::digest(&digest::SHA256, &vec![b'a'; 1_000_000]).as_ref(), + "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"); +} + +fn hmac_sha256() { + // RFC 4231, test case 2. + let key = hmac::Key::new(hmac::HMAC_SHA256, b"Jefe"); + check("hmac-sha256 rfc4231 case 2", hmac::sign(&key, b"what do ya want for nothing?").as_ref(), + "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"); +} + +fn x25519() { + // RFC 7748, section 6.1. + let alice = hex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); + let bob_public = hex("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); + let rng = ring::test::rand::FixedSliceRandom { bytes: &alice }; + let private = agreement::EphemeralPrivateKey::generate(&agreement::X25519, &rng).expect("a key of alice's bytes"); + check("x25519 rfc7748 alice's public key", private.compute_public_key().expect("alice's public key").as_ref(), + "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"); + let peer = agreement::UnparsedPublicKey::new(&agreement::X25519, bob_public); + let shared = agreement::agree_ephemeral(private, &peer, |k| k.to_vec()).expect("the agreement"); + check("x25519 rfc7748 shared secret", &shared, + "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"); +} + +/// Seal `plaintext` and check the ciphertext and tag, then open it, and +/// refuse it with one bit of the tag flipped. +fn aead_case(what: &str, alg: &'static aead::Algorithm, key: &str, nonce: &str, aad: &str, plaintext: &[u8], ciphertext: &str, tag: &str) { + let key = aead::LessSafeKey::new(aead::UnboundKey::new(alg, &hex(key)).expect("the key")); + let nonce = || aead::Nonce::try_assume_unique_for_key(&hex(nonce)).expect("the nonce"); + let aad = hex(aad); + let mut sealed = plaintext.to_vec(); + key.seal_in_place_append_tag(nonce(), aead::Aad::from(&aad), &mut sealed).expect("seal"); + let (body, got_tag) = sealed.split_at(plaintext.len()); + check(&format!("{what} ciphertext"), body, ciphertext); + check(&format!("{what} tag"), got_tag, tag); + let mut opened = sealed.clone(); + let plain = key.open_in_place(nonce(), aead::Aad::from(&aad), &mut opened).expect("open"); + assert_eq!(plain, plaintext, "{what} opens to its plaintext"); + let mut forged = sealed; + *forged.last_mut().expect("a tag") ^= 1; + assert!(key.open_in_place(nonce(), aead::Aad::from(&aad), &mut forged).is_err(), "{what} opens a forged tag"); + println!("ring_kat: {what} opened, and refused with a flipped tag bit"); +} + +fn aeads() { + // RFC 8439, section 2.8.2. + aead_case( + "chacha20-poly1305 rfc8439 2.8.2", + &aead::CHACHA20_POLY1305, + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "070000004041424344454647", + "50515253c0c1c2c3c4c5c6c7", + b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.", + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6\ + 3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36\ + 92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc\ + 3ff4def08e4b7a9de576d26586cec64b6116", + "1ae10b594f09e26a7e902ecbd0600691", + ); + // McGrew and Viega, "The Galois/Counter Mode of Operation", test cases 4 + // and 16. + let plaintext = hex( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + ); + aead_case( + "aes-128-gcm case 4", + &aead::AES_128_GCM, + "feffe9928665731c6d6a8f9467308308", + "cafebabefacedbaddecaf888", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + &plaintext, + "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e\ + 21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091", + "5bc94fbc3221a5db94fae95ae7121a47", + ); + aead_case( + "aes-256-gcm case 16", + &aead::AES_256_GCM, + "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", + "cafebabefacedbaddecaf888", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + &plaintext, + "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662", + "76fc6ece0f4e1768cddf8853bb2d551b", + ); +} + +fn signatures() { + // RFC 8032, section 7.1, test 1: the key pair from its seed, the + // signature of the empty message, and a refusal of another message. + let pair = signature::Ed25519KeyPair::from_seed_unchecked(&hex( + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + )) + .expect("the seed"); + use signature::KeyPair; + check("ed25519 rfc8032 test 1 public key", pair.public_key().as_ref(), + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"); + let sig = pair.sign(b""); + check("ed25519 rfc8032 test 1 signature", sig.as_ref(), + "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"); + let public = signature::UnparsedPublicKey::new(&signature::ED25519, pair.public_key().as_ref().to_vec()); + assert!(public.verify(b"x", sig.as_ref()).is_err(), "ed25519 verifies another message"); + + // RFC 6979, appendix A.2.5: P-256, SHA-256, the message "sample". + let public = signature::UnparsedPublicKey::new( + &signature::ECDSA_P256_SHA256_FIXED, + hex("0460fed4ba255a9d31c961eb74c6356d68c049b8923b61fa6ce669622e60f29fb6\ + 7903fe1008b8bc99a41ae9e95628bc64f2f1b20c2d7e9f5177a3c294d4462299"), + ); + let sig = hex( + "efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716\ + f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8", + ); + public.verify(b"sample", &sig).expect("ecdsa p-256 rfc6979 a.2.5 verifies"); + assert!(public.verify(b"samplf", &sig).is_err(), "ecdsa p-256 verifies another message"); + println!("ring_kat: ecdsa p-256 rfc6979 a.2.5 verified, and refused for another message"); +} + +fn system_random() { + use rand::SecureRandom; + let rng = rand::SystemRandom::new(); + let (mut a, mut b) = ([0u8; 32], [0u8; 32]); + rng.fill(&mut a).expect("the system's random bytes"); + rng.fill(&mut b).expect("the system's random bytes"); + assert_ne!(a, b, "two fills of SystemRandom are the same 32 bytes"); + println!("ring_kat: SystemRandom filled two different 32-byte buffers"); +} + +fn main() { + digests(); + hmac_sha256(); + x25519(); + aeads(); + signatures(); + system_random(); + println!("ring_kat: ok"); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index fee49f300f0..b4d9b72da0e 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -137,6 +137,9 @@ const RUST_SKIP: &[&str] = &[ // It needs a host that dials its listeners when it says they wait: // `libc_sockets` runs it on `tests/netcase`. "nodelay_accepted", + // It needs a NIC in front of netstack and HTTPS servers behind it: + // `https_fetch` runs it on `tests/netcase`. + "https_get", // It asserts nothing at all: it holds `dump_nmi_probe`'s boot open for // twenty seconds. On a shared boot it would be twenty seconds of nothing. "lan_hold", @@ -271,6 +274,11 @@ const MACHINE_TESTS: &[&str] = &[ // peer that answers: the calls are requests on netstack's port, netstack // has no host build, and the T14's peer is the bench's network. "libc_sockets", + // An HTTPS client's fetch from a peer on the host, and its refusals: + // `ring`'s ToyOS build, the random source and wall clock it trusts a + // certificate by and the sockets under it exist in a ToyOS guest alone, + // and the T14's row is the next stage's, against a server on the bench. + "https_fetch", // The nested-NMI report is a raw write to the 16550, which the T14 does not // have. "nested_nmi_is_loud", @@ -3060,6 +3068,83 @@ fn libc_sockets() -> Result<(), String> { Ok(()) } +/// An unchanged HTTPS client — `ureq` on `rustls` on `ring`, as published but +/// for `ring`'s ToyOS arms — trusting `build::TRUST_ROOTS` and sending the +/// project's `User-Agent`, on one boot of `tests/netcase` against servers this +/// run starts on the host: it fetches a body over TLS 1.3 whole, by a hash over +/// every byte the host computes with another SHA-256 than the guest's; and it +/// refuses a certificate for another address and one from an authority the +/// roots file does not hold, each by its own name, at the handshake. +fn https_fetch() -> Result<(), String> { + use common::https::{self, Authority, Seen, Server}; + use sha2::Digest; + const JOB: &str = "https_get"; + /// Root `CLAUDE.md`'s, spelled again here so the guest's copy is checked + /// against the rule and not against itself. + const USER_AGENT: &str = "toyos-build (https://github.com/ToyOSOrg/ToyOS)"; + const WAIT: Duration = Duration::from_secs(30); + let host: std::net::IpAddr = https::HOST.parse().expect("an address"); + 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 want = format!("{JOB}: ok bytes={} sha256={:x}", body.len(), sha2::Sha256::digest(body.as_slice())); + let fetched = Server::start(trusted.leaf(host), body.clone())?; + let wrong_name = Server::start(trusted.leaf([192, 0, 2, 1].into()), body.clone())?; + let untrusted = Server::start(stranger.leaf(host), body)?; + + let mut roots = toyos_build::build::trust_roots(); + roots.extend_from_slice(trusted.pem().as_bytes()); + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); + let options = BootOptions { + extra_root_files: vec![(toyos_build::build::TRUST_ROOTS.to_string(), roots)], + ..Default::default() + }; + let mut qemu = boot_netcase(&[], &[(JOB.to_string(), bin)], options)?; + let roots = format!("/system/{}", toyos_build::build::TRUST_ROOTS); + let fetches: [(&str, &Server, String, i32); 3] = [ + ("the trusted server", &fetched, want, 0), + ("the server named for another address", &wrong_name, format!("{JOB}: refused not-valid-for-name"), 2), + ("the server no root vouches for", &untrusted, format!("{JOB}: refused unknown-issuer"), 2), + ]; + for (what, server, line, code) in &fetches { + let url = format!("https://{}:{}/body", https::HOST, server.port); + let result = qemu.run_test(&format!("test_rs_{JOB} {url} {roots}"), Duration::from_secs(120)); + if let Some(why) = &result.error { + return Err(format!("{what}: {why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(*code) || !result.stdout.lines().any(|l| l.trim_end() == line) { + return Err(format!( + "{what}: the client ended {:?} and was to end {code} saying {line:?}:\n{}", + result.exit_code, result.stdout + )); + } + } + + // Of a refused handshake the server sees the connection arrive and no + // more: this client sends it no alert, on the host as here, and the close + // after it need not arrive + // (`issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md`). + for (what, server, _, code) in &fetches { + if !matches!(server.next(WAIT).map_err(|e| format!("{what}: {e}"))?, Seen::Accepted) { + return Err(format!("{what} saw something before a connection")); + } + if *code != 0 { + continue; + } + match server.next(WAIT).map_err(|e| format!("{what}: {e}"))? { + Seen::Served { version: Some(rustls::ProtocolVersion::TLSv1_3), path, user_agents } + if path == "/body" && user_agents == [USER_AGENT] => {} + other => return Err(format!("{what} saw {other:?}, not a TLS 1.3 GET of /body as {USER_AGENT:?}")), + } + } + for (what, server, _, _) in &fetches { + if let Some(seen) = server.more().into_iter().find(|seen| !matches!(seen, Seen::Failed(_))) { + return Err(format!("{what} saw more than the one connection it was asked for: {seen:?}")); + } + } + Ok(()) +} + /// Run the machine-shape test, which owns its QEMU: the machine shape *is* the /// test. fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { @@ -3067,6 +3152,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config), "netstack_socket_churn" => netstack_socket_churn(), "libc_sockets" => libc_sockets(), + "https_fetch" => https_fetch(), "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config), "machine_shutdown" => power::machine_shutdown(test_config), "acpi_power_button" => power::acpi_power_button(test_config), diff --git a/userland/doom/Cargo.toml b/userland/doom/Cargo.toml index 49a54583d97..103f8e36ed6 100644 --- a/userland/doom/Cargo.toml +++ b/userland/doom/Cargo.toml @@ -18,9 +18,7 @@ softbuffer = { version = "0.4", default-features = false, features = ["wayland", [build-dependencies] cc = "1.1" -ureq = { version = "3", default-features = false, features = ["rustls-no-provider", "rustls-webpki-roots"] } -rustls-rustcrypto = "0.0.2-alpha" -webpki-roots = "1" +ureq = { version = "3", default-features = false, features = ["rustls"] } flate2 = { version = "1", default-features = false, features = ["rust_backend"] } tar = "0.4" diff --git a/userland/doom/build.rs b/userland/doom/build.rs index d7b494f4a3a..6e33658bece 100644 --- a/userland/doom/build.rs +++ b/userland/doom/build.rs @@ -1,5 +1,4 @@ use std::path::PathBuf; -use std::sync::Arc; use std::{fs, path::Path}; /// The upstream doomgeneric commit `/system/bin/doom` is built from. @@ -158,21 +157,12 @@ fn main() { println!("cargo:rerun-if-changed=doomgeneric"); } -fn http_agent() -> ureq::Agent { - let tls = ureq::tls::TlsConfig::builder() - .provider(ureq::tls::TlsProvider::Rustls) - .root_certs(ureq::tls::RootCerts::WebPki) - .unversioned_rustls_crypto_provider(Arc::new(rustls_rustcrypto::provider())) - .build(); - ureq::Agent::config_builder() - .tls_config(tls) - .build() - .new_agent() -} +/// Root `CLAUDE.md`'s: the only `User-Agent` ToyOS sends. +const USER_AGENT: &str = "toyos-build (https://github.com/ToyOSOrg/ToyOS)"; fn download_doomgeneric(root: &Path) { println!("Downloading doomgeneric {DOOMGENERIC_COMMIT}..."); - let agent = http_agent(); + let agent = ureq::Agent::config_builder().user_agent(USER_AGENT).build().new_agent(); // The commit is the checksum: GitHub's archive of a sha is that sha's tree // and can be nothing else, so pinning the URL needs no second hash of ours. let resp = agent From d63234cb9fb3b4f3d164aaaa54869644a12baad2 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 17:14:09 +0200 Subject: [PATCH 2/8] The HTTPS test server reports a failed handshake without a reason nobody reads clippy denies the unread field. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- tests/common/https.rs | 9 ++++----- tests/toyos.rs | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/common/https.rs b/tests/common/https.rs index d526f25968c..234fa7f7ac9 100644 --- a/tests/common/https.rs +++ b/tests/common/https.rs @@ -74,8 +74,8 @@ pub enum Seen { Accepted, /// A handshake that completed and a request answered with the whole body. Served { version: Option, path: String, user_agents: Vec }, - /// A handshake the server could not complete, and why. - Failed(rustls::Error), + /// A handshake the server could not complete. + Failed, } /// A server on a loopback port of its own, answering each connection in turn @@ -127,9 +127,8 @@ impl Server { fn serve(config: &Arc, mut tcp: TcpStream, body: &[u8]) -> Seen { let mut conn = rustls::ServerConnection::new(config.clone()).expect("a server connection"); while conn.is_handshaking() { - if let Err(e) = conn.complete_io(&mut tcp) { - let why = e.get_ref().and_then(|inner| inner.downcast_ref::()).cloned(); - return Seen::Failed(why.unwrap_or_else(|| rustls::Error::General(e.to_string()))); + if conn.complete_io(&mut tcp).is_err() { + return Seen::Failed; } } let version = conn.protocol_version(); diff --git a/tests/toyos.rs b/tests/toyos.rs index b4d9b72da0e..08ab34a3979 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -3138,7 +3138,7 @@ fn https_fetch() -> Result<(), String> { } } for (what, server, _, _) in &fetches { - if let Some(seen) = server.more().into_iter().find(|seen| !matches!(seen, Seen::Failed(_))) { + if let Some(seen) = server.more().into_iter().find(|seen| !matches!(seen, Seen::Failed)) { return Err(format!("{what} saw more than the one connection it was asked for: {seen:?}")); } } From c86a74113b35630f7ee9f1a32517719eb919abcd Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 17:36:06 +0200 Subject: [PATCH 3/8] https_fetch runs ring_kat on the CPU its fetch ran on, and a silent netcase boot is filed The handshake is rustls on ring at both ends, so it alone would pass a ring wrong at both; ring's known answers are the oracle, and ring picks its code by the CPU's features, so they run on the same guest CPU as the fetch. ring_kat's shared run stays the T14's. The whole suite went red once on netstack_socket_churn: the firmware cleared the screen and said nothing more for 487 s under a load average near 90, and the test was green alone. Filed with the load, as a red under load is. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...othing-past-the-firmwares-screen-clears.md | 31 +++++++++++++++++++ tests/toyos.rs | 18 +++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md diff --git a/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md b/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md new file mode 100644 index 00000000000..1e68b2050e8 --- /dev/null +++ b/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md @@ -0,0 +1,31 @@ +--- +status: open +kind: defect +opened: 2026-10-09 +--- + +# A netcase boot under host load said nothing past the firmware's screen clears + +One whole `cargo test` on the development Mac, at the head of the branch that +added `https_fetch`, went red on `netstack_socket_churn` alone: `[qemu] Boot +timed out waiting for ===READY===; the console carried: nothing at all`, after +487 s. The boot's 16550 file holds 58 bytes, the firmware's +screen-clearing escapes (`ESC[2J ESC[01;01H ESC[=3h`, three times) and +nothing after them: no loader line. The same image booted green in the same +run for `https_fetch` and `libc_sockets`, and `netstack_socket_churn` alone a +minute later was green in 12 s. + +The host's load averages as the suite began were 82.78, 94.69 and 92.31 on +14 cores, other worktrees' suites among them, and 36.29, 47.59 and 67.23 as +it ended; 37.01, 45.47 and 64.77 for the green run alone. The harness had +priced its liveness ceilings at 1.00x. + +Not reduced: whether the firmware is stuck or starved, which a QMP read of a +silent guest's registers would say (`tests/CLAUDE.md`'s method for a death +during boot), and at what rate. + +**Exit**: a rate over repeated boots of this image beside other guests, each +silent one's registers read, that names where the firmware stopped; and the +fix at that cause. + +**Owner**: the orchestrator. diff --git a/tests/toyos.rs b/tests/toyos.rs index 08ab34a3979..d3a2957de21 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -212,6 +212,9 @@ const DRIVEN_AND_SHARED: &[&str] = &[ "fault_gates", "sched_stress", "std_alloc", + // Its shared run is the T14's CPU; `https_fetch` runs it on the guest CPU + // its fetch ran on, since `ring` picks its code by the CPU's features. + "ring_kat", ]; /// What `test-early-panic` panics with (`kernel/src/main.rs`): the last line its @@ -3074,11 +3077,15 @@ fn libc_sockets() -> Result<(), String> { /// run starts on the host: it fetches a body over TLS 1.3 whole, by a hash over /// every byte the host computes with another SHA-256 than the guest's; and it /// refuses a certificate for another address and one from an authority the -/// roots file does not hold, each by its own name, at the handshake. +/// roots file does not hold, each by its own name, at the handshake. First, +/// on the same CPU, `ring_kat` holds `ring` to its specifications' answers: +/// the server is `rustls` on `ring` too, so the handshake alone would pass a +/// `ring` wrong at both ends. fn https_fetch() -> Result<(), String> { use common::https::{self, Authority, Seen, Server}; use sha2::Digest; const JOB: &str = "https_get"; + const KAT: &str = "ring_kat"; /// Root `CLAUDE.md`'s, spelled again here so the guest's copy is checked /// against the rule and not against itself. const USER_AGENT: &str = "toyos-build (https://github.com/ToyOSOrg/ToyOS)"; @@ -3094,12 +3101,17 @@ fn https_fetch() -> Result<(), String> { let mut roots = toyos_build::build::trust_roots(); roots.extend_from_slice(trusted.pem().as_bytes()); - let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); + let crate_path = compile::repo_root().join("tests/toyos-rust-tests"); + let bins = [JOB, KAT].map(|name| (name.to_string(), qemu::build_toyos_bin(qemu::SUITE_ARCH, &crate_path, name))); let options = BootOptions { extra_root_files: vec![(toyos_build::build::TRUST_ROOTS.to_string(), roots)], ..Default::default() }; - let mut qemu = boot_netcase(&[], &[(JOB.to_string(), bin)], options)?; + let mut qemu = boot_netcase(&[], &bins, options)?; + let kat = qemu.run_test("test_rs_ring_kat", Duration::from_secs(120)); + if kat.error.is_some() || kat.exit_code != Some(0) || !kat.stdout.lines().any(|l| l.trim_end() == "ring_kat: ok") { + return Err(format!("{KAT} ended {:?} ({:?}):\n{}", kat.exit_code, kat.error, kat.stdout)); + } let roots = format!("/system/{}", toyos_build::build::TRUST_ROOTS); let fetches: [(&str, &Server, String, i32); 3] = [ ("the trusted server", &fetched, want, 0), From 2f7b0eadaa3c32785970351b56e1c846d44cb1db Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 18:17:23 +0200 Subject: [PATCH 4/8] The trust roots ship with their licence's text under a gate that reads them, and ring_kat holds RSA, P-384 and SHA-384 to published answers The owner allowed CDLA-Permissive-2.0 for the Mozilla roots. The licence gate now judges them: by the licence of the crate the build copies them out of (`build::TRUST_ROOTS_CRATE`), as data, the one place `licence::DATA_ONLY` passes, and refused unless the text the image carries beside them, /system/etc/ssl/CDLA-Permissive-2.0.txt, is that crate's own LICENSE byte for byte. The licence asks for the text to travel with the data; it is committed under licenses/ and NOTICE names it. The tooling issue that tracked the gap is closed. The `font: bool` the gate threaded through `refused`, `judge_licence` and `Report::judge` becomes `Scope`, since a licence allowed for data alone is a third case. `https_fetch` stages the roots and its harness's authority under a name of its own and passes that path, so `build_and_assemble` writes the roots unconditionally and the test-only match and assert go. The C environment is `GuestEnv`'s, applied by `cargo_build` to every guest build, so `build_programs` and `TestBuild` no longer each derive it. `trust_roots` writes PEM with `pem`, already in the lock through rcgen, and the hand-written encoder, `base64` and the test of the encoder's round trip go. `ring_kat` adds SHA-384 of "abc" (FIPS 180-2 D.1), ECDSA P-384 with SHA-384 (RFC 6979 A.2.6), and RSA PKCS#1 v1.5 and PSS with SHA-256 at 2048 bits (the first passing `[mod = 2048]` SHA256 vector of NIST CAVP's FIPS 186-3 SigVer15 and SigVerPSS), each signature refused with a bit flipped. It runs in `https_fetch` alone now: the shared boot ran it on the same QEMU CPU model as the fetch's boot, so the second run bought nothing, and the fetch's run keeps the oracle beside the handshake it vouches for. The host-tools rows name the host `cc` compiling ring for doom's build script and the toolchain's clang compiling a test crate's ring, and the lost-FIN issue says slirp is not ruled out. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- Cargo.lock | 2 +- Cargo.toml | 2 +- NOTICE | 14 ++ ...ed-handshake-can-leave-its-peer-waiting.md | 5 +- ...d-runs-host-tools-outside-rust-and-qemu.md | 4 +- issues/the-internet-clients-work-unchanged.md | 8 +- ...ry-image-and-no-licence-gate-reads-them.md | 25 --- .../CDLA-Permissive-2.0-webpki-root-certs.txt | 61 ++++++++ src/build.rs | 102 +++++-------- src/licence.rs | 142 +++++++++++++++--- tests/toyos-rust-tests/src/bin/ring_kat.rs | 87 ++++++++++- tests/toyos.rs | 19 ++- 12 files changed, 343 insertions(+), 128 deletions(-) delete mode 100644 issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md create mode 100644 licenses/CDLA-Permissive-2.0-webpki-root-certs.txt diff --git a/Cargo.lock b/Cargo.lock index 4b2f9487a5c..3502e872802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5664,7 +5664,6 @@ name = "toyos-build" version = "0.1.0" dependencies = [ "acpiserver-api", - "base64 0.22.1", "bcachefs", "fatfs", "flate2", @@ -5675,6 +5674,7 @@ dependencies = [ "image", "kernel", "libc", + "pem", "rcgen", "rustls", "serde", diff --git a/Cargo.toml b/Cargo.toml index 739417910f2..dfc0b15e1d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -221,7 +221,7 @@ gix = { version = "0.88", default-features = false, features = ["status", "sha1" # The authorities `build::trust_roots` writes to ROOT, as the rustls project # publishes the Mozilla root program, and the PEM it writes them in. webpki-root-certs = "1" -base64 = "0.22" +pem = "4" [dev-dependencies] # `USER_TOP`, so the harness judges a held `rsp` against the bound the kernel diff --git a/NOTICE b/NOTICE index 224294bec76..127a2db08ed 100644 --- a/NOTICE +++ b/NOTICE @@ -244,6 +244,20 @@ rust-lang/llvm-project, Apache-2.0 WITH LLVM-exception like upstream's; the toolchain release carries the clang, `lld` and LLVM tools built from it. +/system/etc/ssl/cert.pem — the Mozilla root program, CDLA-Permissive-2.0 +------------------------------------------------------------------------ + +Every image carries the authorities of Mozilla's root program as the +`webpki-root-certs` crate publishes them, a derived work of the CCADB's data, +written by `src/build.rs`'s `trust_roots`. That crate is under +CDLA-Permissive-2.0, which the owner allowed for these roots: it asks that the +agreement's text travel with the data, and the image carries it beside them as +/system/etc/ssl/CDLA-Permissive-2.0.txt. + + Licence text: licenses/CDLA-Permissive-2.0-webpki-root-certs.txt (the + crate's `LICENSE`, sha256 + e271993808fec50ab29350b39539cdec611a9103f827e0aa26d61da70e2d33f8) + bcachefs/tests/fixtures/ — volumes upstream's tools wrote --------------------------------------------------------- diff --git a/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md index bf107395cea..b88964e9e0a 100644 --- a/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md +++ b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md @@ -30,7 +30,10 @@ unread, nothing sent since the client's first flight), or the second connection of a boot, is unmeasured, and so is whether it reaches the stack the move installs (`issues/toyos-has-its-own-network-stack.md`). `OWNERLESS_LIFE` (`userland/netstack/src/main.rs`) should have reset it at 100 -seconds and no reset arrived either. +seconds and no reset arrived either. Nor is slirp ruled out: no capture was +taken of the guest's side, so whether the guest sent the FIN or the reset and +slirp lost it is unmeasured too; a QEMU `filter-dump` on the netcase NIC +answers it. **Exit condition**: a guest test whose process closes a connection mid- handshake, with the peer's flight unread, and exits, sees the host's peer 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 d45c5a44edf..e2216e5510a 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 @@ -19,8 +19,8 @@ arrives and is not one. M4 and M5 are stages of `issues/toyos-builds-itself.md`. | `sh` running LLVM's `config.guess`, and the POSIX tools and `cc` it runs | LLVM's CMake, whenever this host builds an LLVM, and the C++ runtime's, in every sysroot build, ask it the host's triple, unconditionally (`get_host_triple` in `rust/src/llvm-project/llvm/cmake/modules/GetHostTriple.cmake`, which runs `sh` by name) | refused: a Rust tool does the shell's part, brush 0.4.0: on the development host (macOS, arm64) `config.guess` printed `/bin/sh`'s triple under it, `arm64-apple-darwin27.0.0`, exit 0 each. The script runs `sed`, `uname`, `mktemp`, `grep`, `rm`, `rmdir` and `cc` there under either shell, and that `cc` is the `cc` rows'. Five of the other six are refused, uutils' doing each: under brush with sed 0.2.0, grep 0.2.0 and coreutils 0.12.0's `mktemp`, `rm` and `rmdir`, and nothing else on `PATH` but the host's `uname` and `cc`, it printed that triple, exit 0. `uname` is admitted: coreutils 0.12.0's answers `-p` with `unknown` where macOS's answers `arm`, and `config.guess` reads that as PowerPC, `powerpc-apple-darwin27.0.0`, exit 0 | CMake finds brush as its `sh`, uutils' `sed`, `grep`, `mktemp`, `rm` and `rmdir`, and a Rust `uname` that answers `-p` as the host's does; or M5 runs it in the guest | | `git` for worktrees, submodules, checkouts, fixtures and rustc's bootstrap | adds worktrees (`src/sysroot.rs`); updates submodules (`src/lib.rs`, `src/sysroot.rs`); fetches the fork from the primary's and checks it out (`src/sysroot.rs`); makes the tests' fixture repositories; runs inside rustc's bootstrap | admitted: no Rust tool does the job, gitoxide 0.85 adds, removes and prunes no worktree, updates no submodule, stages, resets and pushes nothing, checks out only a fresh clone and fetches a local path by spawning `git`; a fixture must be what `git` makes, and bootstrap runs `git` itself | M4 runs it in the guest | | `git` for reads, a config write, a commit's paths written out, and clones and fetches over HTTPS | `rev-parse`, `merge-base`, `ls-tree`, `ls-files`, `cat-file`, `config --get-regexp`, `config --blob`, `status`, `diff`, `ls-remote` and `grep`, in the build system and its tests; `config --global --add safe.directory` in CI's containers; `checkout -- ` through an index of its own, which writes the C++ runtime's sources out of the LLVM commit into the stored LLVM (`src/llvm.rs`); every workflow's checkout | refused: a Rust tool does it, gitoxide 0.85, which reads refs, objects, the index, config, worktrees and status, adds a value to a config file and writes it (gix-config 0.58's `File::section_mut_or_create_new`, `SectionMut::push`, `File::write_to`), walks history, diffs, and lists, fetches and clones a remote over HTTPS; `grep` is a search of the files its index names; and gitoxide's CLI 0.59 (gix 0.88) wrote the runtimes' sources of LLVM `849da7d6` into an empty directory, each path's tree through `gix rev parse`, `gix index from-tree` and `gix free index checkout-exclusive`, exit 0 each: the 18759 files `git` writes there, byte for byte and mode for mode | those are gitoxide's | -| `cc`, `c++` and `ar` on a Linux host, `build-essential` in CI's containers | rustc links every host binary through `cc`; `cc` and `c++` compile LLVM, clang, LLD and `rustc_llvm` (`src/llvm.rs` names both to bootstrap); `cc` compiles ring's C and assembly in every build of the build system itself, whose HTTP agent is rustls on ring (`src/release.rs`); `ar` archives what `cc::Build` compiles, ring's included | admitted: no Rust tool compiles C or C++, or takes rustc's host link; ring is the one TLS provider the tree takes (owner, 2026-10-02), over a provider in Rust alone | M5: no host in the loop | -| the toolchain's own `clang`, `llvm-ar`, `rust-lld` and `llvm-config`, built from `ToyOSOrg/llvm-project` | rustc links every guest binary with `rust-lld`; `clang` compiles the C corpus (`tests/common/compile.rs`) and, with `llvm-ar`, doomgeneric through `cc::Build` (`src/clang.rs`); rustc's bootstrap asks `llvm-config` how to link LLVM | admitted: our fork's C++, which ToyOS can one day build and run; no Rust tool compiles C, `cc::Build` archives with an `ar`, bootstrap reads LLVM through `llvm-config`, and `CLAUDE.md` links everything with `rust-lld` | M5: no host in the loop | +| `cc`, `c++` and `ar` on a Linux host, `build-essential` in CI's containers | rustc links every host binary through `cc`; `cc` and `c++` compile LLVM, clang, LLD and `rustc_llvm` (`src/llvm.rs` names both to bootstrap); `cc` compiles ring's C and assembly in every build of the build system itself, whose HTTP agent is rustls on ring (`src/release.rs`), and in every build of doom's build script, whose download of doomgeneric is `ureq` on rustls on ring (`userland/doom/build.rs`); `ar` archives what `cc::Build` compiles, ring's included | admitted: no Rust tool compiles C or C++, or takes rustc's host link; ring is the one TLS provider the tree takes (owner, 2026-10-02), over a provider in Rust alone | M5: no host in the loop | +| the toolchain's own `clang`, `llvm-ar`, `rust-lld` and `llvm-config`, built from `ToyOSOrg/llvm-project` | rustc links every guest binary with `rust-lld`; `clang` compiles the C corpus (`tests/common/compile.rs`) and, with `llvm-ar`, through `cc::Build` the C of every guest crate that has any (`src/clang.rs`, `src/build.rs`'s `GuestEnv`): doomgeneric, and ring's C and assembly in a test crate's `https_get` and `ring_kat`; rustc's bootstrap asks `llvm-config` how to link LLVM | admitted: our fork's C++, which ToyOS can one day build and run; no Rust tool compiles C, `cc::Build` archives with an `ar`, bootstrap reads LLVM through `llvm-config`, and `CLAUDE.md` links everything with `rust-lld` | M5: no host in the loop | | `ovmf-generic`, `qemu-efi-aarch64` | the x86-64 and AArch64 UEFI firmware of CI's guest containers (`src/firmware.rs`), packaged by Debian apart from QEMU | admitted: QEMU's own firmware, and no Rust firmware does its job | the instrument's QEMU carries its own firmware | | `ca-certificates` | the trust store `git` and `curl` verify against in CI's containers | admitted: data both of them need | goes when neither runs there | | the T14's Ubuntu and every tool `src/metal.rs` runs on it over `ssh` | the metal loop, on the T14 and never on a development host | outside the rule: recovery equipment on a test machine, not the build's host | they leave with Ubuntu (`issues/the-machine-updates-itself-without-ubuntu.md`) | diff --git a/issues/the-internet-clients-work-unchanged.md b/issues/the-internet-clients-work-unchanged.md index 7b32241e42c..b0dd28eea41 100644 --- a/issues/the-internet-clients-work-unchanged.md +++ b/issues/the-internet-clients-work-unchanged.md @@ -40,9 +40,11 @@ netd, `toyos::net` and std's ToyOS networking in the `rust/` fork. off, leaving nothing to call `__assert_fail`), and `toyos` in `LINUX_ABI` and in `src/rand.rs`'s `getrandom` list. The build writes the Mozilla roots of `webpki-root-certs` to ROOT as `/system/etc/ssl/cert.pem` - (`build::TRUST_ROOTS`), and a test image stages that file with its - harness's authority after them. In an x86-64 QEMU guest `ring_kat` - passes known answers, and `https_fetch` has an unchanged `ureq` 3 on + (`build::TRUST_ROOTS`), with the text of their licence, + CDLA-Permissive-2.0, beside them, which the owner allowed for these roots + and the licence gate holds them to; `https_fetch` stages a file of its own + holding them and its harness's authority. In an x86-64 QEMU guest + `ring_kat` passes known answers, RSA and P-384 among them, and `https_fetch` has an unchanged `ureq` 3 on `rustls` 0.23 fetch a body over TLS 1.3 byte-exact, send the project's `User-Agent`, and refuse a wrong name and an untrusted root. Not run: AArch64, the T14. diff --git a/issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md b/issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md deleted file mode 100644 index e3a1b827ff8..00000000000 --- a/issues/the-trust-roots-ship-in-every-image-and-no-licence-gate-reads-them.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-10-09 ---- - -# The trust roots ship in every image and no licence gate reads them - -`build::trust_roots` (`src/build.rs`) writes the Mozilla root program's -authorities, as the `webpki-root-certs` crate publishes them, to every ROOT as -`/system/etc/ssl/cert.pem`. That crate's licence is `CDLA-Permissive-2.0`, -which `licence::ALLOWED` does not name, and the gate (`src/licence.rs`) never -sees it: it judges the crates an image's programs link and the files the tree -commits, and the roots are neither — a dependency of the build system whose -data the build copies into the image. The image carries no copy of that -licence's text. - -**Exit**: the licence gate judges data the build writes into an image from a -third-party crate, and the roots file passes it: under a licence `ALLOWED` -names, or an `EXCEPTIONS` row, with whatever notice its licence asks of a -redistribution in the image. - -**Owner**: the internet-clients track -(`issues/the-internet-clients-work-unchanged.md`), whose stage 3 put the file -there. diff --git a/licenses/CDLA-Permissive-2.0-webpki-root-certs.txt b/licenses/CDLA-Permissive-2.0-webpki-root-certs.txt new file mode 100644 index 00000000000..8e84169cf7b --- /dev/null +++ b/licenses/CDLA-Permissive-2.0-webpki-root-certs.txt @@ -0,0 +1,61 @@ +# Community Data License Agreement - Permissive - Version 2.0 + +This is the Community Data License Agreement - Permissive, Version +2.0 (the "agreement"). Data Provider(s) and Data Recipient(s) agree +as follows: + +## 1. Provision of the Data + +1.1. A Data Recipient may use, modify, and share the Data made +available by Data Provider(s) under this agreement if that Data +Recipient follows the terms of this agreement. + +1.2. This agreement does not impose any restriction on a Data +Recipient's use, modification, or sharing of any portions of the +Data that are in the public domain or that may be used, modified, +or shared under any other legal exception or limitation. + +## 2. Conditions for Sharing Data + +2.1. A Data Recipient may share Data, with or without modifications, so +long as the Data Recipient makes available the text of this agreement +with the shared Data. + +## 3. No Restrictions on Results + +3.1. This agreement does not impose any restriction or obligations +with respect to the use, modification, or sharing of Results. + +## 4. No Warranty; Limitation of Liability + +4.1. All Data Recipients receive the Data subject to the following +terms: + +THE DATA IS PROVIDED ON AN "AS IS" BASIS, WITHOUT REPRESENTATIONS, +WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED +INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +NO DATA PROVIDER SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE DATA OR RESULTS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## 5. Definitions + +5.1. "Data" means the material received by a Data Recipient under +this agreement. + +5.2. "Data Provider" means any person who is the source of Data +provided under this agreement and in reliance on a Data Recipient's +agreement to its terms. + +5.3. "Data Recipient" means any person who receives Data directly +or indirectly from a Data Provider and agrees to the terms of this +agreement. + +5.4. "Results" means any outcome obtained by computational analysis +of Data, including for example machine learning models and models' +insights. diff --git a/src/build.rs b/src/build.rs index 0e6586d6367..c96861e52a3 100644 --- a/src/build.rs +++ b/src/build.rs @@ -337,11 +337,16 @@ struct GuestEnv { image_key: String, /// Whose floor the loader keeps (`signing::FLOOR_ENV`), which follows the key. floor_scope: &'static str, + /// `CSysroot::cc_env` for the architecture's userland: every guest crate + /// that compiles C compiles it with the toolchain's clang against libc's C + /// sysroot, never with the host's `cc`. + cc: Vec<(String, String)>, } impl GuestEnv { - fn new(sysroot: Sysroot) -> Self { + fn new(sysroot: Sysroot, arch: Arch) -> Self { Self { + cc: crate::clang::CSysroot::of(sysroot.dir(), arch).cc_env(), sysroot, image_key: crate::signing::key().public_hex(), floor_scope: crate::signing::key().floor_scope().word(), @@ -371,7 +376,8 @@ fn cargo_build( .env_remove("RUSTFLAGS") .env(crate::signing::KEY_ENV, &env.image_key) .env(crate::signing::FLOOR_ENV, env.floor_scope) - .env_remove("RUSTC"); + .env_remove("RUSTC") + .envs(env.cc.iter().map(|(k, v)| (k, v))); for (k, v) in extra_env { cmd.env(k, v); } @@ -714,21 +720,23 @@ fn release(root: &Path, toolchain: &crate::keystore::Key, arch: Arch) -> toyos_o /// `/system/etc/ssl/cert.pem`. pub const TRUST_ROOTS: &str = "etc/ssl/cert.pem"; +/// The crate [`trust_roots`] reads them out of: `licence::judge` holds them to +/// its licence and [`TRUST_ROOTS_LICENCE`] to its licence text. +pub const TRUST_ROOTS_CRATE: &str = "webpki-root-certs"; + +/// Where on ROOT their licence's text is, and the text: CDLA-Permissive-2.0 +/// lets the roots be shared only with it. +pub const TRUST_ROOTS_LICENCE: (&str, &[u8]) = ( + "etc/ssl/CDLA-Permissive-2.0.txt", + include_bytes!("../licenses/CDLA-Permissive-2.0-webpki-root-certs.txt"), +); + /// The Mozilla root program's authorities as `webpki-root-certs` publishes /// them, each certificate a PEM block in the published order. pub fn trust_roots() -> Vec { - use base64::Engine; - let mut pem = String::new(); - for cert in webpki_root_certs::TLS_SERVER_ROOT_CERTS { - pem.push_str("-----BEGIN CERTIFICATE-----\n"); - let body = base64::engine::general_purpose::STANDARD.encode(cert.as_ref()); - for line in body.as_bytes().chunks(64) { - pem.push_str(std::str::from_utf8(line).expect("base64 is ASCII")); - pem.push('\n'); - } - pem.push_str("-----END CERTIFICATE-----\n"); - } - pem.into_bytes() + let config = pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF); + let roots = webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter(); + roots.map(|cert| pem::encode_config(&pem::Pem::new("CERTIFICATE", cert.to_vec()), config)).collect::().into_bytes() } fn build_and_assemble( @@ -752,16 +760,8 @@ fn build_and_assemble( root_files.extend(assets::collect(&config.assets, &programs)); } - // A test image's roots are these and its harness's CA after them, staged - // whole under the same name, so the client reads one file either way. - let roots = trust_roots(); - match extra_files.iter().find(|(name, _)| name == TRUST_ROOTS) { - Some((_, staged)) => assert!( - staged.starts_with(&roots), - "a test image staged {TRUST_ROOTS} with other than the build's roots at its head" - ), - None => root_files.push((TRUST_ROOTS.to_string(), roots)), - } + root_files.push((TRUST_ROOTS.to_string(), trust_roots())); + root_files.push((TRUST_ROOTS_LICENCE.0.to_string(), TRUST_ROOTS_LICENCE.1.to_vec())); // Extra files (test binaries, shared libs) for (name, data) in extra_files { @@ -813,11 +813,6 @@ fn build_programs( let ws_target = root.join(format!("target/{target}/{PROFILE}")); - // Every userland crate that compiles C compiles it with the toolchain's - // clang against libc's C sysroot. - let cc_env = crate::clang::CSysroot::of(env.sysroot.dir(), arch).cc_env(); - let cc_env: Vec<(&str, &str)> = cc_env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); - // Build and read under one hold, exactly as `build_toyos_bins` does and for // the same reason: a program's path is keyed on (crate, target, profile) // alone, so every config in this run writes and reads the same @@ -832,7 +827,7 @@ fn build_programs( extra.push("-p"); extra.push(pkg); } - cargo_build(root, target, &extra, env, &cc_env, quiet); + cargo_build(root, target, &extra, env, &[], quiet); } for c in &programs { @@ -1733,7 +1728,7 @@ fn shipped_parts(root: &Path, boot: &Boot, plan: &Plan) -> (Vec, Vec, Ve // this worktree's crate targets can land inside this build. let mut lock = buildlock::shared(root, "build"); let config = parse_config(&boot.config); - let env = GuestEnv::new(toolchain::ensure(root, &mut lock)); + let env = GuestEnv::new(toolchain::ensure(root, &mut lock), arch); invalidate_stale(&mut lock, &env.sysroot.identity, &[root.to_path_buf()]); @@ -1947,7 +1942,7 @@ pub fn build_test_parts( // back after the userland build, and a clean landing in between is the // same defect as one landing mid-compile. let mut lock = buildlock::shared(root, "test image"); - let env = GuestEnv::new(toolchain::ensure(root, &mut lock)); + let env = GuestEnv::new(toolchain::ensure(root, &mut lock), arch); invalidate_stale(&mut lock, &env.sysroot.identity, &[root.to_path_buf()]); @@ -2023,7 +2018,7 @@ pub fn build_toyos_bins(root: &Path, arch: Arch, crate_path: &Path, quiet: bool) if !quiet { eprintln!("[build] Building cdylib subcrate: {lib_name}"); } - cargo_build(&sub_path, target, &[], env, &build.cc_env(), quiet); + cargo_build(&sub_path, target, &[], env, &[], quiet); let lib_out = sub_path.join(format!("target/{target}/{PROFILE}")); lib_search_dirs.push(lib_out.clone()); @@ -2045,10 +2040,11 @@ pub fn build_toyos_bins(root: &Path, arch: Arch, crate_path: &Path, quiet: bool) for dir in &lib_search_dirs { link_flags.push_str(&format!("-L {} ", dir.display())); } - let mut extra_env = build.cc_env(); - if !link_flags.is_empty() { - extra_env.push(("RUSTFLAGS", link_flags.trim_end())); - } + let extra_env: Vec<(&str, &str)> = if link_flags.is_empty() { + vec![] + } else { + vec![("RUSTFLAGS", link_flags.trim_end())] + }; cargo_build(crate_path, target, &["--bins"], env, &extra_env, quiet); let bin_dir = crate_path.join(format!("target/{target}/{PROFILE}")); @@ -2086,9 +2082,6 @@ pub fn build_toyos_bins(root: &Path, arch: Arch, crate_path: &Path, quiet: bool) struct TestBuild { target: &'static str, env: GuestEnv, - /// `CSysroot::cc_env` for the target: a test crate's C is compiled as a - /// program's is (`build_programs`), never by the host's `cc`. - cc_env: Vec<(String, String)>, _lock: buildlock::Held, _artifact: buildlock::Guard, } @@ -2096,15 +2089,10 @@ struct TestBuild { impl TestBuild { fn begin(root: &Path, arch: Arch, what: &str, stale_targets: &[PathBuf]) -> Self { let mut lock = buildlock::shared(root, what); - let env = GuestEnv::new(toolchain::ensure(root, &mut lock)); + let env = GuestEnv::new(toolchain::ensure(root, &mut lock), arch); invalidate_stale(&mut lock, &env.sysroot.identity, stale_targets); let artifact = buildlock::artifact(root); - let cc_env = crate::clang::CSysroot::of(env.sysroot.dir(), arch).cc_env(); - TestBuild { target: arch.userland(), env, cc_env, _lock: lock, _artifact: artifact } - } - - fn cc_env(&self) -> Vec<(&str, &str)> { - self.cc_env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect() + TestBuild { target: arch.userland(), env, _lock: lock, _artifact: artifact } } } @@ -2114,7 +2102,7 @@ pub fn build_toyos_bin(root: &Path, arch: Arch, crate_path: &Path, name: &str, q let _building = Building::start(format!("{} {name} of {}", arch.name(), hostws::rel(root, crate_path))); let build = TestBuild::begin(root, arch, "a test binary", &[crate_path.to_path_buf()]); let (target, env) = (build.target, &build.env); - cargo_build(crate_path, target, &["--bin", name], env, &build.cc_env(), quiet); + cargo_build(crate_path, target, &["--bin", name], env, &[], quiet); let binary = crate_path.join(format!("target/{target}/{PROFILE}/{name}")); fs::read(&binary).unwrap_or_else(|e| panic!("read the test binary {}: {e}", binary.display())) } @@ -2125,24 +2113,6 @@ pub fn build_toyos_bin(root: &Path, arch: Arch, crate_path: &Path, name: &str, q mod tests { use super::*; - /// **The roots file reads back, by the reader a client reads it with, as - /// every published root in the published order and nothing else**: a - /// line break or a padding byte the writer got wrong is a root no client - /// trusts, or a file no client reads. - #[test] - fn the_trust_roots_read_back_as_the_published_set() { - let read: Vec> = ureq::tls::parse_pem(&trust_roots()) - .map(|item| match item.expect("every block of the roots file parses") { - ureq::tls::PemItem::Certificate(cert) => cert.der().to_vec(), - _ => panic!("the roots file holds a PEM block that is not a certificate"), - }) - .collect(); - let published: Vec> = - webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().map(|cert| cert.to_vec()).collect(); - assert!(!published.is_empty()); - assert_eq!(read, published); - } - /// **The release a tree records is its commit, at that commit's own time, /// and dirty from the first file that is not that commit's**: an /// untracked file counts, and so does an edit to a tracked one, staged or diff --git a/src/licence.rs b/src/licence.rs index 6bfe3e1e1fd..d109a93f9cc 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -13,6 +13,12 @@ //! superset of what one image links, and a crate only another platform pulls in //! can red here. Git and path packages are judged exactly as registry ones. //! +//! **The trust roots are judged by the crate the build copies them out of**, +//! [`crate::build::TRUST_ROOTS_CRATE`], which no shipped package reaches: by +//! its `license` as data, the one place [`DATA_ONLY`] passes, and refused +//! unless the licence text the image carries beside them is that crate's own +//! `LICENSE`, byte for byte. +//! //! **A committed file is judged by its [`COMMITTED_FILES`] row**, //! whose [`Terms`] column is its licence. A file ships when it is under a //! shipped asset directory or a shipped path package's directory, or when such @@ -77,6 +83,31 @@ const ALLOWED_WITH: &[(&str, &str)] = &[("Apache-2.0", "LLVM-exception")]; /// Allowed for a [`Terms::Font`] row, and nowhere else. const FONTS_ONLY: &[&str] = &["OFL-1.1"]; +/// Allowed for the data the build writes into an image out of +/// [`crate::build::TRUST_ROOTS_CRATE`], and nowhere else: the owner allowed +/// CDLA-Permissive-2.0 for the Mozilla roots. +const DATA_ONLY: &[&str] = &["CDLA-Permissive-2.0"]; + +/// What a licence is judged as: [`FONTS_ONLY`] passes only for a font and +/// [`DATA_ONLY`] only for the trust roots. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Scope { + Code, + Font, + Data, +} + +impl Scope { + fn allows(self, id: &str) -> bool { + ALLOWED.contains(&id) + || match self { + Scope::Code => false, + Scope::Font => FONTS_ONLY.contains(&id), + Scope::Data => DATA_ONLY.contains(&id), + } + } +} + /// Surfaced by name in every verdict, not refused. const NAMED: &[&str] = &["MPL-2.0"]; @@ -519,10 +550,16 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ ]; /// `NOTICE` sections that name no files, and why. -const PROSE: &[(&str, &str)] = &[( - "Rust crates and the forks", - "every crate, fork or not, is judged by the crate half of this gate", -)]; +const PROSE: &[(&str, &str)] = &[ + ( + "Rust crates and the forks", + "every crate, fork or not, is judged by the crate half of this gate", + ), + ( + "/system/etc/ssl/cert.pem — the Mozilla root program, CDLA-Permissive-2.0", + "the build writes it out of a crate, judged with the text beside it by `judge_trust_roots`", + ), +]; // --- Expressions ------------------------------------------------------------- @@ -629,11 +666,10 @@ fn parse_with(tokens: &[String], at: &mut usize) -> Result { } /// The leaves that keep `expr` from passing; empty when it passes. -fn refused(expr: &Expr, font: bool) -> Vec { +fn refused(expr: &Expr, scope: Scope) -> Vec { match expr { Expr::Id(id) => { - let ok = ALLOWED.contains(&id.as_str()) || (font && FONTS_ONLY.contains(&id.as_str())); - if ok { + if scope.allows(id) { vec![] } else { vec![id.clone()] @@ -646,9 +682,9 @@ fn refused(expr: &Expr, font: bool) -> Vec { vec![format!("{id} WITH {exception}")] } } - Expr::And(parts) => parts.iter().flat_map(|p| refused(p, font)).collect(), + Expr::And(parts) => parts.iter().flat_map(|p| refused(p, scope)).collect(), Expr::Or(parts) => { - let each: Vec> = parts.iter().map(|p| refused(p, font)).collect(); + let each: Vec> = parts.iter().map(|p| refused(p, scope)).collect(); if each.iter().any(Vec::is_empty) { vec![] } else { @@ -666,17 +702,17 @@ fn names(expr: &Expr) -> bool { Expr::And(parts) => parts.iter().any(names), Expr::Or(parts) => parts .iter() - .filter(|p| refused(p, false).is_empty()) + .filter(|p| refused(p, Scope::Code).is_empty()) .all(names), } } /// Why `licence` does not pass, or `None` when it does. -fn judge_licence(licence: &str, font: bool) -> Option { +fn judge_licence(licence: &str, scope: Scope) -> Option { match parse(licence) { Err(why) => Some(format!("not an SPDX expression: {why}")), Ok(expr) => { - let leaves = refused(&expr, font); + let leaves = refused(&expr, scope); (!leaves.is_empty()).then(|| format!("not allowed: {}", leaves.join(", "))) } } @@ -755,8 +791,8 @@ impl Report { } /// Judge `finding`'s licence, and record it as a refusal or an MPL name. - fn judge(&mut self, finding: Finding, font: bool) { - match judge_licence(&finding.licence, font) { + fn judge(&mut self, finding: Finding, scope: Scope) { + match judge_licence(&finding.licence, scope) { Some(why) => self.find(Finding { why, ..finding }), None if parse(&finding.licence).is_ok_and(|e| names(&e)) => { let (subject, licence, via) = (finding.subject, finding.licence, finding.via); @@ -938,7 +974,7 @@ fn judge_crates(metadata: &Value, roots: &[PathBuf], report: &mut Report) -> Res into: into[dep].clone(), }; match (str_of(package, "license"), str_of(package, "license_file")) { - (Some(licence), _) => report.judge(finding(licence, String::new()), false), + (Some(licence), _) => report.judge(finding(licence, String::new()), Scope::Code), (None, Some(file)) => report.find(finding( "", format!("declares only a licence file, {file}, which no gate reads"), @@ -949,6 +985,46 @@ fn judge_crates(metadata: &Value, roots: &[PathBuf], report: &mut Report) -> Res Ok(local) } +// --- The trust roots --------------------------------------------------------- + +/// Judge the roots out of `package`, whose `LICENSE` is `text`, beside which an +/// image ships `shipped`. +fn judge_trust_roots(package: &Value, text: &[u8], shipped: &[u8], report: &mut Report) { + let name = str_of(package, "name").unwrap_or("?"); + let version = str_of(package, "version").unwrap_or("?"); + let finding = Finding { + subject: format!("crate {name} {version}, the data of {}", crate::build::TRUST_ROOTS), + kind: Kind::Crate, + key: name.to_string(), + licence: str_of(package, "license").unwrap_or("").to_string(), + why: String::new(), + via: vec!["build::trust_roots".to_string()], + into: BTreeSet::from([None]), + }; + if text == shipped { + report.judge(finding, Scope::Data); + } else { + let why = format!("ships {} beside them, which is not its LICENSE", crate::build::TRUST_ROOTS_LICENCE.0); + report.find(Finding { why, ..finding }); + } +} + +/// [`judge_trust_roots`] on the build's own workspace. +fn judge_trust_roots_of(root: &Path, report: &mut Report) -> Result<(), String> { + let name = crate::build::TRUST_ROOTS_CRATE; + let metadata = metadata(root, &root.join("Cargo.toml"), &["--locked"], &[])?; + let graph = Graph::new(&metadata)?; + let found: Vec<&Value> = graph.packages.values().copied().filter(|p| str_of(p, "name") == Some(name)).collect(); + let [package] = found[..] else { + return Err(format!("the build's workspace resolves {} packages named {name}, not one", found.len())); + }; + let manifest = str_of(package, "manifest_path").ok_or_else(|| format!("{name} has no manifest path"))?; + let licence = Path::new(manifest).with_file_name("LICENSE"); + let text = std::fs::read(&licence).map_err(|e| format!("read {}: {e}", licence.display()))?; + judge_trust_roots(package, &text, crate::build::TRUST_ROOTS_LICENCE.1, report); + Ok(()) +} + // --- Committed files --------------------------------------------------------- /// Where committed files ship from, as root-relative paths. @@ -1004,8 +1080,11 @@ fn judge_files(ledger: &[Row], tracked: &[String], shipping: &Shipping, report: } for (path, _, _, terms) in ledger { if shipping.ships(path) { - let font = matches!(terms, Terms::Font(_)); - report.judge(Finding::at(Kind::File, path, terms.expr(), String::new()), font); + let scope = match terms { + Terms::Spdx(_) => Scope::Code, + Terms::Font(_) => Scope::Font, + }; + report.judge(Finding::at(Kind::File, path, terms.expr(), String::new()), scope); } } } @@ -1101,7 +1180,7 @@ fn judge_notice( report.notes.push(format!("NOTICE section {path} ({expr}) is not shipped")); continue; } - report.judge(Finding::at(Kind::Notice, path, expr, String::new()), false); + report.judge(Finding::at(Kind::Notice, path, expr, String::new()), Scope::Code); } } @@ -1284,6 +1363,8 @@ pub fn judge(root: &Path) -> Result { roots: vec![library.join("std")], }); + judge_trust_roots_of(root, &mut report)?; + let mut local = Local::default(); for doc in &docs { let found = judge_crates(&doc.metadata, &doc.roots, &mut report)?; @@ -1329,7 +1410,7 @@ mod tests { use serde_json::json; fn verdict_of(licence: &str) -> Option { - judge_licence(licence, false) + judge_licence(licence, Scope::Code) } #[test] @@ -1343,7 +1424,11 @@ mod tests { "an unrecognised licence passed" ); assert!(verdict_of("OFL-1.1").is_some(), "OFL passed outside a font"); - assert_eq!(judge_licence("OFL-1.1", true), None); + assert_eq!(judge_licence("OFL-1.1", Scope::Font), None); + assert!(judge_licence("OFL-1.1", Scope::Data).is_some(), "OFL passed as data"); + assert!(verdict_of("CDLA-Permissive-2.0").is_some(), "CDLA passed outside data"); + assert!(judge_licence("CDLA-Permissive-2.0", Scope::Font).is_some(), "CDLA passed for a font"); + assert_eq!(judge_licence("CDLA-Permissive-2.0", Scope::Data), None); assert_eq!(verdict_of("MPL-2.0"), None); } @@ -1694,6 +1779,23 @@ ub_checks"#; assert!(crates(&d).findings.is_empty()); } + /// **The roots pass under their crate's licence as data, and only with + /// that crate's own licence text beside them.** + #[test] + fn the_trust_roots_pass_only_with_their_crates_licence_text() { + let judged = |licence: &str, shipped: &[u8]| { + let package = json!({"name": "roots", "version": "1.0.0", "license": licence}); + let mut report = Report::default(); + judge_trust_roots(&package, b"the text", shipped, &mut report); + report.findings + }; + assert_eq!(judged("CDLA-Permissive-2.0", b"the text"), []); + let other = judged("CDLA-Permissive-2.0", b"another text"); + assert!(other.len() == 1 && other[0].why.contains("not its LICENSE"), "{other:?}"); + let sharing = judged("CDLA-Sharing-1.0", b"the text"); + assert!(sharing.len() == 1 && sharing[0].why.contains("CDLA-Sharing-1.0"), "{sharing:?}"); + } + #[test] fn a_root_the_metadata_does_not_hold_is_refused() { let d = doc(&[("other", Some("MIT"), None, None)], &[]); diff --git a/tests/toyos-rust-tests/src/bin/ring_kat.rs b/tests/toyos-rust-tests/src/bin/ring_kat.rs index bcbeb8c4d9b..03cd43f430d 100644 --- a/tests/toyos-rust-tests/src/bin/ring_kat.rs +++ b/tests/toyos-rust-tests/src/bin/ring_kat.rs @@ -18,9 +18,12 @@ fn check(what: &str, got: &[u8], want: &str) { } fn digests() { - // FIPS 180-2, appendix B.1 and C.1, and B.3. + // FIPS 180-2, appendix B.1, D.1 and C.1, and B.3. check("sha256(abc)", digest::digest(&digest::SHA256, b"abc").as_ref(), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + check("sha384(abc)", digest::digest(&digest::SHA384, b"abc").as_ref(), + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed\ + 8086072ba1e7cc2358baeca134c825a7"); check("sha512(abc)", digest::digest(&digest::SHA512, b"abc").as_ref(), "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a\ 2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"); @@ -143,6 +146,88 @@ fn signatures() { public.verify(b"sample", &sig).expect("ecdsa p-256 rfc6979 a.2.5 verifies"); assert!(public.verify(b"samplf", &sig).is_err(), "ecdsa p-256 verifies another message"); println!("ring_kat: ecdsa p-256 rfc6979 a.2.5 verified, and refused for another message"); + + // RFC 6979, appendix A.2.6: P-384, SHA-384, the message "sample". + let public = signature::UnparsedPublicKey::new( + &signature::ECDSA_P384_SHA384_FIXED, + hex("04ec3a4e415b4e19a4568618029f427fa5da9a8bc4ae92e02e06aae5286b300c64\ + def8f0ea9055866064a254515480bc13\ + 8015d9b72d7d57244ea8ef9ac0c621896708a59367f9dfb9f54ca84b3f1c9db1\ + 288b231c3ae0d4fe7344fd2533264720"), + ); + verifies("ecdsa p-384 rfc6979 a.2.6", b"sample", hex( + "94edbb92a5ecb8aad4736e56c691916b3f88140666ce9fa73d64c4ea95ad133c\ + 81a648152e44acf96e36dd1e80fabe46\ + 99ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94f\ + a329c145786e679e7b82c71a38628ac8", + ), |m, s| public.verify(m, s)); + + // NIST CAVP's FIPS 186-3 RSA SigVer files, `[mod = 2048]`, SHA256: the + // first vector of each whose result is P. + let pkcs1 = signature::RsaPublicKeyComponents { + n: hex("c47abacc2a84d56f3614d92fd62ed36ddde459664b9301dcd1d61781cfcc026b\ + cb2399bee7e75681a80b7bf500e2d08ceae1c42ec0b707927f2b2fe92ae85208\ + 7d25f1d260cc74905ee5f9b254ed05494a9fe06732c3680992dd6f0dc634568d\ + 11542a705f83ae96d2a49763d5fbb24398edf3702bc94bc168190166492b8671\ + de874bb9cecb058c6c8344aa8c93754d6effcd44a41ed7de0a9dcd9144437f21\ + 2b18881d042d331a4618a9e630ef9bb66305e4fdf8f0391b3b2313fe549f0189\ + ff968b92f33c266a4bc2cffc897d1937eeb9e406f5d0eaa7a14782e76af3fce9\ + 8f54ed237b4a04a4159a5f6250a296a902880204e61d891c4da29f2d65f34cbb"), + e: hex("49d2a1"), + }; + verifies("rsa pkcs#1 v1.5 sha-256 2048 cavp SigVer15", &hex( + "95123c8d1b236540b86976a11cea31f8bd4e6c54c235147d20ce722b03a6ad75\ + 6fbd918c27df8ea9ce3104444c0bbe877305bc02e35535a02a58dcda306e632a\ + d30b3dc3ce0ba97fdf46ec192965dd9cd7f4a71b02b8cba3d442646eeec4af59\ + 0824ca98d74fbca934d0b6867aa1991f3040b707e806de6e66b5934f05509bea", + ), hex( + "51265d96f11ab338762891cb29bf3f1d2b3305107063f5f3245af376dfcc7027\ + d39365de70a31db05e9e10eb6148cb7f6425f0c93c4fb0e2291adbd22c77656a\ + fc196858a11e1c670d9eeb592613e69eb4f3aa501730743ac4464486c7ae68fd\ + 509e896f63884e9424f69c1c5397959f1e52a368667a598a1fc90125273d9341\ + 295d2f8e1cc4969bf228c860e07a3546be2eeda1cde48ee94d062801fe666e4a\ + 7ae8cb9cd79262c017b081af874ff00453ca43e34efdb43fffb0bb42a4e2d32a\ + 5e5cc9e8546a221fe930250e5f5333e0efe58ffebf19369a3b8ae5a67f6a048b\ + c9ef915bda25160729b508667ada84a0c27e7e26cf2abca413e5e4693f4a9405", + ), |m, s| pkcs1.verify(&signature::RSA_PKCS1_2048_8192_SHA256, m, s)); + + // Its salt is 32 bytes, the one length `ring` verifies with SHA-256. + let pss = signature::RsaPublicKeyComponents { + n: hex("a47d04e7cacdba4ea26eca8a4c6e14563c2ce03b623b768c0d49868a57121301\ + dbf783d82f4c055e73960e70550187d0af62ac3496f0a3d9103c2eb7919a7275\ + 2fa7ce8c688d81e3aee99468887a15288afbb7acb845b7c522b5c64e678fcd3d\ + 22feb84b44272700be527d2b2025a3f83c2383bf6a39cf5b4e48b3cf2f56eef0\ + dfff18555e31037b915248694876f3047814415164f2c660881e694b58c28038\ + a032ad25634aad7b39171dee368e3d59bfb7299e4601d4587e68caaf8db457b7\ + 5af42fc0cf1ae7caced286d77fac6cedb03ad94f1433d2c94d08e60bc1fdef05\ + 43cd2951e765b38230fdd18de5d2ca627ddc032fe05bbd2ff21e2db1c2f94d8b"), + e: hex("10e43f"), + }; + verifies("rsa-pss sha-256 2048 cavp SigVerPSS", &hex( + "e002377affb04f0fe4598de9d92d31d6c786040d5776976556a2cfc55e54a1dc\ + b3cb1b126bd6a4bed2a184990ccea773fcc79d246553e6c64f686d21ad415267\ + 3cafec22aeb40f6a084e8a5b4991f4c64cf8a927effd0fd775e71e8329e41fdd\ + 4457b3911173187b4f09a817d79ea2397fc12dfe3d9c9a0290c8ead31b6690a6", + ), hex( + "4f9b425c2058460e4ab2f5c96384da2327fd29150f01955a76b4efe956af06dc\ + 08779a374ee4607eab61a93adc5608f4ec36e47f2a0f754e8ff839a8a19b1db1\ + e884ea4cf348cd455069eb87afd53645b44e28a0a56808f5031da5ba9112768d\ + fbfca44ebe63a0c0572b731d66122fb71609be1480faa4e4f75e43955159d70f\ + 081e2a32fbb19a48b9f162cf6b2fb445d2d6994bc58910a26b5943477803cdaa\ + a1bd74b0da0a5d053d8b1dc593091db5388383c26079f344e2aea600d0e32416\ + 4b450f7b9b465111b7265f3b1b063089ae7e2623fc0fda8052cf4bf3379102fb\ + f71d7c98e8258664ceed637d20f95ff0111881e650ce61f251d9c3a629ef222d", + ), |m, s| pss.verify(&signature::RSA_PSS_2048_8192_SHA256, m, s)); +} + +/// Verify `sig` over `message`, then refuse it with one bit of the signature +/// flipped. +fn verifies(what: &str, message: &[u8], sig: Vec, verify: impl Fn(&[u8], &[u8]) -> Result<(), ring::error::Unspecified>) { + verify(message, &sig).unwrap_or_else(|_| panic!("{what} does not verify")); + let mut forged = sig; + forged[0] ^= 1; + assert!(verify(message, &forged).is_err(), "{what} verifies with a flipped signature bit"); + println!("ring_kat: {what} verified, and refused with a flipped signature bit"); } fn system_random() { diff --git a/tests/toyos.rs b/tests/toyos.rs index d1eb9c204f0..cb377f0cc52 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -140,6 +140,9 @@ const RUST_SKIP: &[&str] = &[ // It needs a NIC in front of netstack and HTTPS servers behind it: // `https_fetch` runs it on `tests/netcase`. "https_get", + // `https_fetch` runs it on the boot its fetches run on, so that test + // carries its own oracle; a shared run would be the same QEMU CPU twice. + "ring_kat", // It asserts nothing at all: it holds `dump_nmi_probe`'s boot open for // twenty seconds. On a shared boot it would be twenty seconds of nothing. "lan_hold", @@ -3181,9 +3184,9 @@ fn libc_sockets() -> Result<(), String> { } /// An unchanged HTTPS client — `ureq` on `rustls` on `ring`, as published but -/// for `ring`'s ToyOS arms — trusting `build::TRUST_ROOTS` and sending the -/// project's `User-Agent`, on one boot of `tests/netcase` against servers this -/// run starts on the host: it fetches a body over TLS 1.3 whole, by a hash over +/// for `ring`'s ToyOS arms — trusting `build::trust_roots` and this run's +/// authority, and sending the project's `User-Agent`, on one boot of +/// `tests/netcase` against servers this run starts on the host: it fetches a body over TLS 1.3 whole, by a hash over /// every byte the host computes with another SHA-256 than the guest's; and it /// refuses a certificate for another address and one from an authority the /// roots file does not hold, each by its own name, at the handshake. First, @@ -3208,20 +3211,20 @@ fn https_fetch() -> Result<(), String> { let wrong_name = Server::start(trusted.leaf([192, 0, 2, 1].into()), body.clone())?; let untrusted = Server::start(stranger.leaf(host), body)?; + // The shipped roots and the harness's authority, under a name of the + // test's own: the client reads the file its argument names. + const ROOTS: &str = "etc/ssl/harness-cert.pem"; let mut roots = toyos_build::build::trust_roots(); roots.extend_from_slice(trusted.pem().as_bytes()); let crate_path = compile::repo_root().join("tests/toyos-rust-tests"); let bins = [JOB, KAT].map(|name| (name.to_string(), qemu::build_toyos_bin(qemu::SUITE_ARCH, &crate_path, name))); - let options = BootOptions { - extra_root_files: vec![(toyos_build::build::TRUST_ROOTS.to_string(), roots)], - ..Default::default() - }; + let options = BootOptions { extra_root_files: vec![(ROOTS.to_string(), roots)], ..Default::default() }; let mut qemu = boot_netcase(&[], &bins, options)?; let kat = qemu.run_test("test_rs_ring_kat", Duration::from_secs(120)); if kat.error.is_some() || kat.exit_code != Some(0) || !kat.stdout.lines().any(|l| l.trim_end() == "ring_kat: ok") { return Err(format!("{KAT} ended {:?} ({:?}):\n{}", kat.exit_code, kat.error, kat.stdout)); } - let roots = format!("/system/{}", toyos_build::build::TRUST_ROOTS); + let roots = format!("/system/{ROOTS}"); let fetches: [(&str, &Server, String, i32); 3] = [ ("the trusted server", &fetched, want, 0), ("the server named for another address", &wrong_name, format!("{JOB}: refused not-valid-for-name"), 2), From 216f33b3536475b4807424860620d21660351ba8 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 18:38:49 +0200 Subject: [PATCH 5/8] NOTICE heads the trust roots' section by a tracked path The gate globs every section's path through git before it reads a section as prose, and /system/etc/ssl/cert.pem is a path on ROOT, not in the tree: the licence step of `cargo run -- --ci host` was red on it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- NOTICE | 4 ++-- src/licence.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NOTICE b/NOTICE index 127a2db08ed..dae60737d72 100644 --- a/NOTICE +++ b/NOTICE @@ -244,8 +244,8 @@ rust-lang/llvm-project, Apache-2.0 WITH LLVM-exception like upstream's; the toolchain release carries the clang, `lld` and LLVM tools built from it. -/system/etc/ssl/cert.pem — the Mozilla root program, CDLA-Permissive-2.0 ------------------------------------------------------------------------- +licenses/CDLA-Permissive-2.0-webpki-root-certs.txt — the Mozilla roots, CDLA-Permissive-2.0 +------------------------------------------------------------------------------------------- Every image carries the authorities of Mozilla's root program as the `webpki-root-certs` crate publishes them, a derived work of the CCADB's data, diff --git a/src/licence.rs b/src/licence.rs index d109a93f9cc..edb80a238bd 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -556,8 +556,8 @@ const PROSE: &[(&str, &str)] = &[ "every crate, fork or not, is judged by the crate half of this gate", ), ( - "/system/etc/ssl/cert.pem — the Mozilla root program, CDLA-Permissive-2.0", - "the build writes it out of a crate, judged with the text beside it by `judge_trust_roots`", + "licenses/CDLA-Permissive-2.0-webpki-root-certs.txt — the Mozilla roots, CDLA-Permissive-2.0", + "the roots the build writes out of a crate are judged, with this text beside them, by `judge_trust_roots`", ), ]; From e3b11331e5a71ee48cb21cb26e8ae39205c76b74 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 18:50:12 +0200 Subject: [PATCH 6/8] A second netcase boot under load said nothing past the firmware's screen clears `https_fetch` alone at 216f33b35, at load 89 to 99, timed out waiting for ===READY=== after 64 s with the same 58 bytes of firmware escapes on its 16550 and nothing after them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...st-load-said-nothing-past-the-firmwares-screen-clears.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md b/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md index 1e68b2050e8..a57c0a02f4b 100644 --- a/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md +++ b/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md @@ -20,6 +20,12 @@ The host's load averages as the suite began were 82.78, 94.69 and 92.31 on it ended; 37.01, 45.47 and 64.77 for the green run alone. The harness had priced its liveness ceilings at 1.00x. +A second, at `216f33b35` on the same branch: `cargo test --test toyos-build +-- https_fetch` alone, beside this tree's own `cargo run -- --ci host` and +other worktrees' work, went red the same way after 64 s, its 16550 file the +same 58 bytes. Load averages 89.36, 87.83 and 82.56 as it began and 98.94, +91.36 and 84.48 as it ended. + Not reduced: whether the firmware is stuck or starved, which a QMP read of a silent guest's registers would say (`tests/CLAUDE.md`'s method for a death during boot), and at what rate. From 88d5c7c0f50949f313a0cf4a0454d2cd0ede67f7 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 19:37:41 +0200 Subject: [PATCH 7/8] The licence gate finds the trust roots' crate in the root workspace's document it already holds `judge_trust_roots_of` ran a fourth `cargo metadata --locked` over the root workspace and built a `Graph` over it only to find one package by name. `judge` already holds a document of that workspace: the one whose `members` holds the root `Cargo.toml`. `webpki-root-certs` is a non-optional dependency of the root package, so whichever feature set that document was resolved with, it resolves the crate, and the gate reads it from there. A shipped tree with no crate in the root workspace is refused by name rather than guessed at. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- src/licence.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/licence.rs b/src/licence.rs index edb80a238bd..33433164732 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -1009,12 +1009,13 @@ fn judge_trust_roots(package: &Value, text: &[u8], shipped: &[u8], report: &mut } } -/// [`judge_trust_roots`] on the build's own workspace. -fn judge_trust_roots_of(root: &Path, report: &mut Report) -> Result<(), String> { +/// [`judge_trust_roots`] on the build's own workspace, out of `metadata`, a +/// document of it: the crate is a non-optional dependency of the root +/// package, so every feature set resolves it. +fn judge_trust_roots_of(metadata: &Value, report: &mut Report) -> Result<(), String> { let name = crate::build::TRUST_ROOTS_CRATE; - let metadata = metadata(root, &root.join("Cargo.toml"), &["--locked"], &[])?; - let graph = Graph::new(&metadata)?; - let found: Vec<&Value> = graph.packages.values().copied().filter(|p| str_of(p, "name") == Some(name)).collect(); + let packages = metadata["packages"].as_array().ok_or("cargo metadata printed no packages")?; + let found: Vec<&Value> = packages.iter().filter(|p| str_of(p, "name") == Some(name)).collect(); let [package] = found[..] else { return Err(format!("the build's workspace resolves {} packages named {name}, not one", found.len())); }; @@ -1363,7 +1364,12 @@ pub fn judge(root: &Path) -> Result { roots: vec![library.join("std")], }); - judge_trust_roots_of(root, &mut report)?; + let manifest = root.join("Cargo.toml"); + let build = docs + .iter() + .find(|d| d.members.contains(&manifest)) + .ok_or_else(|| format!("no shipped crate is in the workspace of {}", manifest.display()))?; + judge_trust_roots_of(&build.metadata, &mut report)?; let mut local = Local::default(); for doc in &docs { From 8a84bc258183d9223593f476f5f31d7a3a7ede84 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 08:22:52 +0200 Subject: [PATCH 8/8] The refused handshake's lost close is recorded as smoltcp's, and as not seen on ToyOS's own stack in two boots The issue named smoltcp as the stack main ships, and OWNERLESS_LIFE in a file #801 deleted. A probe patch on https_fetch, waiting up to 150 s for each refused connection's end at the host's server, found all four ends already arrived in two boots at the merge of main after #801. The issue stays open: two boots are no rate, and the test asserts nothing of them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...ed-handshake-can-leave-its-peer-waiting.md | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md index b88964e9e0a..eb8a700df05 100644 --- a/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md +++ b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md @@ -8,32 +8,31 @@ opened: 2026-10-09 `https_fetch` (`tests/toyos.rs`) runs `https_get` three times on one boot of `tests/netcase`, each a new process and one TCP connection through -`/system/bin/netstack` (smoltcp, the stack `main` ships) to a `rustls` server -on the host. The second and third runs refuse the server's certificate during -the handshake, print their refusal, drop the connection and exit 2. On the -host, the server's read of the handshake's next record is answered by -nothing: in one boot its peer's end of stream arrived and in the other it did -not, 180 seconds after the process exited, no FIN and no reset. netstack -logged nothing about either connection. +`/system/bin/netstack` to a `rustls` server on the host. The second and third +runs refuse the server's certificate during the handshake, print their +refusal, drop the connection and exit 2. On smoltcp, the stack `netstack` ran +before ToyOS's own replaced it, the server's read of the handshake's next +record was answered by nothing: in one boot its peer's end of stream arrived +and in the other it did not, 180 seconds after the process exited, no FIN and +no reset. netstack logged nothing about either connection. The same client and server both on the host (macOS) see each refused connection end at once with an end of stream and no alert: the client sends -no alert, so the close is all the peer is owed, and it is what is missing. +no alert, so the close is all the peer is owed, and it is what was missing. Measured at `a944746d5` plus the branch that added `https_fetch`, under QEMU with slirp, once (logs on that branch's pull request): the second connection of the boot never ended at the peer; the third did, by the end of the 180 seconds. Two earlier boots of the same test reported the -second connection silent after 30 seconds. Not reduced: whether it is the -connection's state when its owner exits mid-handshake (the server's flight -unread, nothing sent since the client's first flight), or the second -connection of a boot, is unmeasured, and so is whether it reaches the stack -the move installs (`issues/toyos-has-its-own-network-stack.md`). -`OWNERLESS_LIFE` (`userland/netstack/src/main.rs`) should have reset it at 100 -seconds and no reset arrived either. Nor is slirp ruled out: no capture was -taken of the guest's side, so whether the guest sent the FIN or the reset and -slirp lost it is unmeasured too; a QEMU `filter-dump` on the netcase NIC -answers it. +second connection silent after 30 seconds. Not reduced, and no capture was +taken of the guest's side, so slirp losing the guest's FIN was not ruled out. + +On ToyOS's own stack, measured at that branch's merge of `main` after #801 +by a probe patch on `https_fetch` that waits up to 150 seconds for each +refused connection's end (patch and logs on that branch's pull request): in +two boots, all four refused connections had already ended at the peer when +the harness looked, after the third run exited. Two boots are no rate, and +`https_fetch` still asserts nothing of these ends. **Exit condition**: a guest test whose process closes a connection mid- handshake, with the peer's flight unread, and exits, sees the host's peer