diff --git a/.cargo/config.toml b/.cargo/config.toml index 5584339528f..f9edee2286c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -30,19 +30,15 @@ rustflags = ["-Dwarnings", "-Cforce-frame-pointers=yes"] [target.aarch64-unknown-none-softfloat] rustflags = ["-Dwarnings", "-Cforce-frame-pointers=yes", "-Adead_code", "-Aunfulfilled_lint_expectations", "-Clink-arg=-pie", "-Clink-arg=-znotext"] -# The loader's two tables are one list: -# - `ed25519-dalek`'s field arithmetic in plain 64-bit integers: its default on -# x86-64 is the AVX2 backend, which this soft-float target has no registers -# for. -# - `/Brepro` stamps the image with a hash of itself rather than the clock, and -# `/DEBUG:NONE` writes no PDB, whose GUID the image would otherwise carry and -# whose contents name the target directory: without both, two builds of one -# tree are two images. +# The loader's two tables are one: `/Brepro` stamps the image with a hash of +# itself rather than the clock, and `/DEBUG:NONE` writes no PDB, whose GUID the +# image would otherwise carry and whose contents name the target directory: +# without both, two builds of one tree are two images. [target.x86_64-unknown-uefi] -rustflags = ["-Dwarnings", "-Cforce-frame-pointers=yes", "--cfg=curve25519_dalek_backend=\"serial\"", "-Clink-arg=/Brepro", "-Clink-arg=/DEBUG:NONE"] +rustflags = ["-Dwarnings", "-Cforce-frame-pointers=yes", "-Clink-arg=/Brepro", "-Clink-arg=/DEBUG:NONE"] [target.aarch64-unknown-uefi] -rustflags = ["-Dwarnings", "-Cforce-frame-pointers=yes", "--cfg=curve25519_dalek_backend=\"serial\"", "-Clink-arg=/Brepro", "-Clink-arg=/DEBUG:NONE"] +rustflags = ["-Dwarnings", "-Cforce-frame-pointers=yes", "-Clink-arg=/Brepro", "-Clink-arg=/DEBUG:NONE"] [target.x86_64-unknown-toyos] rustflags = ["-Dwarnings", "--cfg=p384_backend=\"bignum\""] diff --git a/Cargo.lock b/Cargo.lock index 23508649e0d..ead84e08d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,7 +347,6 @@ name = "bootloader" version = "0.1.0" dependencies = [ "bcachefs", - "sha2 0.10.9", "toyos-abi", "toyos-acpi", "toyos-blackbox", @@ -4882,6 +4881,7 @@ name = "toyos-update" version = "0.1.0" dependencies = [ "ed25519-dalek 2.2.0", + "ring", "toyos-crypto", "toyos-wallclock", ] diff --git a/NOTICE b/NOTICE index 750b6dacfa6..9ea46447e6e 100644 --- a/NOTICE +++ b/NOTICE @@ -474,6 +474,22 @@ image carries them. These are every file of that directory for an interface the crate offers. +lib/crypto/tests/data/ed25519_speccheck_cases.json — "Taming the many EdDSAs" cases, Apache-2.0 +----------------------------------------------------------------------------------------------- + + ed25519_speccheck_cases.json sha256 08e47a36d9aead288664930505584f353fff113ab854f2800db1e4f5b3540450 + Upstream: https://github.com/novifinancial/ed25519-speccheck, `cases.json` + at commit 65519336fda78a3d016e947df6d82848aca0c9da, byte for byte + Licence text: lib/crypto/tests/data/speccheck-LICENSE (upstream's `LICENSE`, + sha256 c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4) + SPDX-License-Identifier: Apache-2.0 + +The twelve edge cases of Chalkias, Garillot, Kondi and Nikolaenko, "Taming the +many EdDSAs" (2020): small-order and non-canonical keys and points, and S +past the group order. `toyos-crypto`'s host tests read them +(`lib/crypto/tests/differential.rs`); no image carries them. + + tests/acvp/nist-*.json — NIST ACVP HMAC and ML-KEM vectors, NIST-Software ------------------------------------------------------------------------- diff --git a/build/licence.rs b/build/licence.rs index 2cc6faf9aa2..465ee826784 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -900,6 +900,12 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "NOTICE", Terms::Spdx("Apache-2.0"), ), + ( + "lib/crypto/tests/data/ed25519_speccheck_cases.json", + "08e47a36d9aead288664930505584f353fff113ab854f2800db1e4f5b3540450", + "NOTICE", + Terms::Spdx("Apache-2.0"), + ), ( "tests/acvp/nist-hmac-sha1.json", "6e96eb5f53065219c437a99fedec7299c916c76e170d6b74507dc5409ed9cd6b", diff --git a/build/ssh/Cargo.toml b/build/ssh/Cargo.toml index c0f389a3d27..64da143ee7c 100644 --- a/build/ssh/Cargo.toml +++ b/build/ssh/Cargo.toml @@ -16,5 +16,5 @@ publish = false toyos-crypto = { path = "../../lib/crypto", features = ["sign"] } # The one `SHA256:` fingerprint a key is named by, the one the package # repository's keys are named by too; and the public key of a seed, which the -# image-signing key read through `hostkey` is derived with. -toyos-update = { path = "../../lib/update" } +# image-signing key read through `hostkey` is derived with, which is `sign`'s. +toyos-update = { path = "../../lib/update", features = ["sign"] } diff --git a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md index 0abb535a765..9d82023bb73 100644 --- a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md +++ b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md @@ -60,9 +60,9 @@ review has been done, and passing vectors does not show it is safer than ring. | Consumer | Today | Needs from the crate | |---|---|---| | `toyos-ssh`'s SSH protocol (`build/ssh`: `kex`, the server, the client). Host-only until `issues/the-ssh-server-is-toyos-own-and-russh-is-gone.md` stage 2 | the crate, since stage 1 | X25519, ML-KEM-768 and its hybrid, Ed25519 sign/verify, `chacha20-poly1305@openssh.com`, SHA-256/512, and a caller-supplied `Random` for replays | -| `toyos-ssh`'s `hostkey` on the signing path. `build/signing.rs` reads the owner's image-signing key through `openssh_seed` and writes a minted one through `openssh_private`; both derive the public key from the seed | `toyos_update::sig::public_of`, since stage 1 | nothing: this derivation is the root of trust's; the verifier itself switches in stage 6 | +| `toyos-ssh`'s `hostkey` on the signing path. `build/signing.rs` reads the owner's image-signing key through `openssh_seed` and writes a minted one through `openssh_private`; both derive the public key from the seed | `toyos_update::sig::public_of`, the crate's since #1019 | nothing | | `system/sshserver` | russh with the RustCrypto stack, including `rsa` (RUSTSEC-2023-0071) | none directly; it reaches the crate through `toyos-ssh` | -| Image and package signing: `lib/update` (the loader, `/system/bin/update`, `build/signing.rs`, `build/root`, #892's repository keys) | `ed25519-dalek` `verify_strict`, which hashes with the loader's `sha2` under `force-soft`; the crate's SHA-512 over the signed header, since #1016 | strict Ed25519 verification in the soft-float UEFI loader, and signing behind `sign` on the host. The track's last stage, stage 6 (owner, 2026-10-13) | +| Image and package signing: `lib/update` (the loader, `/system/bin/update`, `build/signing.rs`, `build/root`, #892's repository keys) | the crate's strict Ed25519 and SHA-2, since #1019; signing and a seed's public key behind `sign`, on the host | stage 6's bench readings | | Every SHA-256: the build, `pkg`, `lib/swap`, `system/update`, `lib/update` and the loader | the crate, since #1016: SHA-NI on x86-64, the UEFI loader's in a block that saves the XMM registers it uses; the Armv8 SHA instructions where the target promises them. No slower than `toyos-sha2-hw`, which it replaces, on either bench: on the T14 4.6 times as fast at 64 B and 9 and 6 percent faster at 16 KiB and 1 MiB, on the AMD bench 2.8 times at 64 B, 2 percent at 16 KiB and equal at 1 MiB (#1016's evidence) | nothing | | ToyOS's own HTTPS clients. On the host: the build's `ureq` (`build/release.rs`) and doom's build script (`apps/doom/build.rs`). In a guest, none today: `https_tls13` and `pkg install ` are stages 3 and 5 of `issues/the-internet-clients-work-unchanged.md` | rustls on `ring`, which is `ToyOSOrg/ring` in a guest | the rustls provider through ureq's `rustls-no-provider` feature. Stage 5, once parity and security hold (owner, 2026-10-13) | | Wi-Fi's `wlan` (`issues/there-is-no-wifi.md` P6, W2) | nothing yet | PBKDF2-HMAC-SHA1, the 802.11i PRF and RFC 3394 unwrap, which the crate has. AES-CMAC-128, SAE over P-256 by hash-to-element, a public constant-time comparison and a public wiping secret, which it lacks | @@ -325,7 +325,7 @@ Not consumers: `cfg(curve25519_dalek_backend = "fiat")`, which nothing in the tree sets, so no build compiles it. It is reached through `toyos-crypto`'s own oracles `x25519-dalek` 2.0.1 and `ed25519-dalek` 2.2.0, and - through `lib/update`'s `ed25519-dalek`. + `lib/update`'s `ed25519-dalek` oracle: test-only since #1019. - Under russh (`system/sshserver`), `curve25519-dalek` 5.0.0-pre.6 names `fiat-crypto` 0.3.0 behind the same unset `cfg`. - Under russh, `p384` 0.14.0-rc.8 names `fiat-crypto` 0.3.0 unless @@ -489,20 +489,40 @@ Not consumers: - Wi-Fi's P6 is this crate's AES-CMAC, SAE by hash-to-element, public constant-time comparison and public wiping secret, each against its RFC or Annex J vectors. -6. **The root of trust, last.** The loader and `lib/update` verify, and the - host signs, with the crate. The owner ruled on 2026-10-13 that this is the - track's last stage, taken only after every stage 4 exit holds. +6. **The root of trust.** The loader and `lib/update` verify, and the host + signs, with the crate. The owner ruled on 2026-10-13 that this is the + track's last stage, taken only after every stage 4 exit holds, and on + 2026-10-22 that the tree uses the crate now and improves it in place + ("use our own crypto where possible"); #1019 takes it. **Exit**: - Ring stays a host differential oracle of the verifier. - The edge corpus is Wycheproof's `ed25519` and the vectors of Chalkias et al., "Taming the many EdDSAs" (2020). On both corpora the crate agrees with `ed25519-dalek`'s `verify_strict` and with ring. + - **Awaiting the owner, as this exit cannot hold as written:** on the + twelve cases every verdict of the crate's is `verify_strict`'s, and + ring accepts four that `verify_strict` refuses: the small-order key of + cases 0 and 1, the small-order R of case 2 and case 11's + non-canonical R. No verifier agrees with both. #1019 pins ring's + verdicts (`lib/crypto/tests/differential.rs`, + `speccheck_cases_against_dalek_strict_and_ring`). - The loader verifies a signed image on the T14 and the AMD bench, with - its verification time measured on each. - - An AArch64 QEMU loader boot verifies a signed image, because the crate's - UEFI path has never run. + its verification time measured on each. Done (#1019's probe image, + #1016's evidence): 253,975 counter ticks on the T14 at its stated + 2.4192 GHz, about 105 µs, and 122,724 on the AMD bench, whose CPU states + no counter rate. + - An AArch64 QEMU loader boot verifies a signed image. Done: + `a_bent_supervisor_is_refused_and_the_other_slot_boots` boots QEMU's + AArch64 machine too. - `ed25519-dalek` and the loader's `sha2` leave `Cargo.lock`'s non-dev - packages. + packages. Done (#1019). + - **One verdict differs from `verify_strict`, for the owner to accept or + refuse:** the crate refuses a public key whose y is p or more, as + RFC 8032 section 5.1.3 requires, where `verify_strict` accepts it when + the point is not small-order. Every key the tree verifies against is one + its build minted, which is canonical, so no verdict on a key the tree + holds differs (`lib/update/tests/ed25519_oracles.rs` names the + encodings). ## Weaknesses until the exits hold diff --git a/lib/crypto/tests/data/ed25519_speccheck_cases.json b/lib/crypto/tests/data/ed25519_speccheck_cases.json new file mode 100644 index 00000000000..8686dcb7eef --- /dev/null +++ b/lib/crypto/tests/data/ed25519_speccheck_cases.json @@ -0,0 +1 @@ +[{"message":"8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6","pub_key":"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa","signature":"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a0000000000000000000000000000000000000000000000000000000000000000"},{"message":"9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79","pub_key":"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa","signature":"f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43a5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04"},{"message":"aebf3f2601a0c8c5d39cc7d8911642f740b78168218da8471772b35f9d35b9ab","pub_key":"f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43","signature":"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa8c4bd45aecaca5b24fb97bc10ac27ac8751a7dfe1baff8b953ec9f5833ca260e"},{"message":"9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79","pub_key":"cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d","signature":"9046a64750444938de19f227bb80485e92b83fdb4b6506c160484c016cc1852f87909e14428a7a1d62e9f22f3d3ad7802db02eb2e688b6c52fcd6648a98bd009"},{"message":"e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c","pub_key":"cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d","signature":"160a1cb0dc9c0258cd0a7d23e94d8fa878bcb1925f2c64246b2dee1796bed5125ec6bc982a269b723e0668e540911a9a6a58921d6925e434ab10aa7940551a09"},{"message":"e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c","pub_key":"cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d","signature":"21122a84e0b5fca4052f5b1235c80a537878b38f3142356b2c2384ebad4668b7e40bc836dac0f71076f9abe3a53f9c03c1ceeeddb658d0030494ace586687405"},{"message":"85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40","pub_key":"442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623","signature":"e96f66be976d82e60150baecff9906684aebb1ef181f67a7189ac78ea23b6c0e547f7690a0e2ddcd04d87dbc3490dc19b3b3052f7ff0538cb68afb369ba3a514"},{"message":"85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40","pub_key":"442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623","signature":"8ce5b96c8f26d0ab6c47958c9e68b937104cd36e13c33566acd2fe8d38aa19427e71f98a473474f2f13f06f97c20d58cc3f54b8bd0d272f42b695dd7e89a8c22"},{"message":"9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41","pub_key":"f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43","signature":"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03be9678ac102edcd92b0210bb34d7428d12ffc5df5f37e359941266a4e35f0f"},{"message":"9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41","pub_key":"f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43","signature":"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffca8c5b64cd208982aa38d4936621a4775aa233aa0505711d8fdcfdaa943d4908"},{"message":"e96b7021eb39c1a163b6da4e3093dcd3f21387da4cc4572be588fafae23c155b","pub_key":"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","signature":"a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04"},{"message":"39a591f5321bbe07fd5a23dc2f39d025d74526615746727ceefd6e82ae65c06f","pub_key":"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","signature":"a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04"}] \ No newline at end of file diff --git a/lib/crypto/tests/data/speccheck-LICENSE b/lib/crypto/tests/data/speccheck-LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/lib/crypto/tests/data/speccheck-LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT 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. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/crypto/tests/differential.rs b/lib/crypto/tests/differential.rs index 3cd9c512ddc..563a7c01c29 100644 --- a/lib/crypto/tests/differential.rs +++ b/lib/crypto/tests/differential.rs @@ -500,6 +500,32 @@ fn seeded_ed25519_against_ring_and_dalek() { }); } +/// The twelve cases of "Taming the many EdDSAs" (Chalkias et al., 2020): +/// every verdict is `verify_strict`'s, a refused key counting as a refused +/// signature. ring accepts four that strict verification refuses: the +/// small-order key of cases 0 and 1, the small-order R of case 2, and case +/// 11's non-canonical R, so its verdicts are pinned, not followed. +#[test] +#[cfg_attr(not(feature = "portable"), ignore = "the same code in every form: run in the portable one")] +fn speccheck_cases_against_dalek_strict_and_ring() { + let cases = support::data("ed25519_speccheck_cases.json"); + let cases = cases.as_array().unwrap(); + assert_eq!(cases.len(), 12); + for (i, case) in cases.iter().enumerate() { + let field = |name: &str| support::hex(case[name].as_str().unwrap()); + let (message, public, signature) = (field("message"), field("pub_key"), field("signature")); + let public: [u8; 32] = public.try_into().unwrap(); + let ours = toyos_crypto::ed25519::PublicKey::from_bytes(public).is_ok_and(|key| key.verify(&message, &signature).is_ok()); + let strict = ed25519_dalek::VerifyingKey::from_bytes(&public).is_ok_and(|key| { + key.verify_strict(&message, &ed25519_dalek::Signature::from_bytes(&signature[..].try_into().unwrap())).is_ok() + }); + let ring = ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, public).verify(&message, &signature).is_ok(); + assert_eq!(ours, strict, "case {i}: verify_strict"); + assert_eq!(ours, i == 3, "case {i}: only case 3 is a signature strict verification takes"); + assert_eq!(ring, [0, 1, 2, 3, 11].contains(&i), "case {i}: ring's verdict moved"); + } +} + /// One case of key derivation and agreement beside RustCrypto's, and /// verification of its deterministic signature, changed or not, beside /// ring's verifier. diff --git a/lib/update/Cargo.toml b/lib/update/Cargo.toml index 5d1317ea646..98d86682ee4 100644 --- a/lib/update/Cargo.toml +++ b/lib/update/Cargo.toml @@ -14,22 +14,26 @@ license = "MIT OR Apache-2.0" publish = false [features] -# Signing, for the host that holds the key. Off in the loader and the updater, -# so no binary on the machine carries a path that makes a signature. -sign = [] +# Signing and deriving a seed's public key, for the host that holds the key. +# Off in the loader and the updater, so no binary on the machine carries a +# path that makes a signature. +sign = ["toyos-crypto/sign"] [dependencies] -# **The one verifier, and why this one.** `ed25519-dalek` is the Ed25519 the -# Rust ecosystem ships (RustCrypto's `ssh-key` and `russh`, which this tree's -# sshserver already runs, verify with it), it is `no_std` without `alloc`, and it -# is already resolved in the userland lockfile at this version. No default -# features: no `std`, no precomputed tables (a verify is one per boot), no -# `zeroize` (the machine holds no secret key). Verified with `verify_strict`, -# which refuses the small-order and non-canonical encodings RFC 8032 §5.1.7 -# leaves to the implementation. -ed25519-dalek = { version = "2.2", default-features = false } -# SHA-512 over the signed header, as SSHSIG names it, and SHA-256 over the -# sections and a package archive. +# Strict Ed25519 verification (RFC 8032, small-order and non-canonical +# encodings refused), SHA-512 over the signed header as SSHSIG names it, and +# SHA-256 over the sections and a package archive. `no_std`. toyos-crypto = { path = "../crypto" } # The calendar a repository document's `expires` is written in, the tree's one. toyos-wallclock = { path = "../wallclock" } + +# The other verifiers the image signature is held against: RustCrypto's +# `verify_strict` and ring's. +[[test]] +name = "ed25519_oracles" +required-features = ["sign"] + +[dev-dependencies] +toyos-crypto = { path = "../crypto", features = ["sign"] } +ed25519-dalek = "2.2" +ring = "0.17.14" diff --git a/lib/update/src/sig.rs b/lib/update/src/sig.rs index cf209b208d2..2099783b06e 100644 --- a/lib/update/src/sig.rs +++ b/lib/update/src/sig.rs @@ -16,7 +16,7 @@ //! anything else — a git commit, a package repository's targets, an image — //! from verifying as this. -use ed25519_dalek::{Signature, VerifyingKey}; +use toyos_crypto::ed25519::PublicKey; use toyos_crypto::hash::Sha512; use crate::image::{HEADER_BYTES, SIGNATURE_BYTES}; @@ -98,26 +98,26 @@ impl core::fmt::Display for Refused { /// Whether `signature` is `key`'s over `signed`, in its namespace. /// -/// `verify_strict`: a signature with a non-canonical `S`, or a key of small -/// order, is refused rather than accepted by one implementation and not -/// another. +/// Strict RFC 8032: a signature with a non-canonical `S` or a small-order +/// `R`, and a key that is small-order or not canonically encoded, are refused +/// rather than accepted by one implementation and not another. pub fn verify(key: &[u8; 32], signed: &S, signature: &[u8; SIGNATURE_BYTES]) -> Result<(), Refused> { - let key = VerifyingKey::from_bytes(key).map_err(|_| Refused::Key)?; - key.verify_strict(message(signed).as_bytes(), &Signature::from_bytes(signature)) - .map_err(|_| Refused::Signature) + let key = PublicKey::from_bytes(*key).map_err(|_| Refused::Key)?; + key.verify(message(signed).as_bytes(), signature).map_err(|_| Refused::Signature) } -/// The public key of the seed `seed`. +/// The public key of the seed `seed`: the host's, like [`sign`]. +#[cfg(any(test, feature = "sign"))] pub fn public_of(seed: &[u8; 32]) -> [u8; 32] { - ed25519_dalek::SigningKey::from_bytes(seed).verifying_key().to_bytes() + *toyos_crypto::ed25519::SigningKey::from_seed(*seed).public_key().as_bytes() } /// `seed`'s signature over `signed`: the host's half, and no binary on the /// machine is built with it. #[cfg(any(test, feature = "sign"))] pub fn sign(seed: &[u8; 32], signed: &S) -> [u8; SIGNATURE_BYTES] { - use ed25519_dalek::Signer as _; - ed25519_dalek::SigningKey::from_bytes(seed).sign(message(signed).as_bytes()).to_bytes() + // The message is SSHSIG's few hundred bytes, far below SHA-512's bound. + toyos_crypto::ed25519::SigningKey::from_seed(*seed).sign(message(signed).as_bytes()).expect(crate::SHA_BOUND) } /// A seed as a key file holds it: 64 lowercase hex digits and a newline. @@ -218,16 +218,16 @@ mod tests { fn rfc_8032_vectors_verify_and_a_flipped_bit_does_not() { for (seed, public, msg, sig) in [RFC_1, RFC_2, RFC_3] { assert_eq!(public_of(&seed), public, "the public key derived from the RFC's seed"); - let key = VerifyingKey::from_bytes(&public).expect("the RFC's key"); - key.verify_strict(msg, &Signature::from_bytes(&sig)).expect("the RFC's signature"); + let key = PublicKey::from_bytes(public).expect("the RFC's key"); + key.verify(msg, &sig).expect("the RFC's signature"); for bit in [0, 255, 256, 511] { let mut bent = sig; bent[bit / 8] ^= 1 << (bit % 8); - assert!(key.verify_strict(msg, &Signature::from_bytes(&bent)).is_err(), "bit {bit}"); + assert!(key.verify(msg, &bent).is_err(), "bit {bit}"); } let mut longer = msg.to_vec(); longer.push(0); - assert!(key.verify_strict(&longer, &Signature::from_bytes(&sig)).is_err()); + assert!(key.verify(&longer, &sig).is_err()); } } diff --git a/lib/update/tests/ed25519_oracles.rs b/lib/update/tests/ed25519_oracles.rs new file mode 100644 index 00000000000..12ef0a5acde --- /dev/null +++ b/lib/update/tests/ed25519_oracles.rs @@ -0,0 +1,108 @@ +//! The image signature beside RustCrypto's `verify_strict` and ring: the same +//! signature bytes over SSHSIG's message from every seed, the same verdict on +//! every changed signature, message and key, and the one place the verifiers +//! part, named. + +use ring::signature::{self, KeyPair}; +use toyos_update::image::{HEADER_BYTES, SIGNATURE_BYTES}; +use toyos_update::sig::{self, Refused}; + +const CASES: usize = 2_000; + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } + fn bytes(&mut self) -> [u8; N] { + core::array::from_fn(|_| self.next() as u8) + } +} + +fn dalek_strict(public: &[u8; 32], message: &[u8], signature: &[u8; SIGNATURE_BYTES]) -> bool { + ed25519_dalek::VerifyingKey::from_bytes(public) + .is_ok_and(|key| key.verify_strict(message, &ed25519_dalek::Signature::from_bytes(signature)).is_ok()) +} + +fn ring(public: &[u8; 32], message: &[u8], signature: &[u8; SIGNATURE_BYTES]) -> bool { + signature::UnparsedPublicKey::new(&signature::ED25519, public).verify(message, signature).is_ok() +} + +#[test] +fn signatures_and_verdicts_match_dalek_and_ring() { + let mut rng = Rng(0x5eed_ed25); + for case in 0..CASES { + let seed: [u8; 32] = rng.bytes(); + let header: [u8; HEADER_BYTES] = rng.bytes(); + let message = sig::message(&header); + let public = sig::public_of(&seed); + let dalek = ed25519_dalek::SigningKey::from_bytes(&seed); + assert_eq!(dalek.verifying_key().to_bytes(), public, "case {case}: the public key"); + let ours = sig::sign(&seed, &header); + use ed25519_dalek::Signer as _; + assert_eq!(dalek.sign(message.as_bytes()).to_bytes(), ours, "case {case}: the signature"); + let pair = signature::Ed25519KeyPair::from_seed_unchecked(&seed).unwrap(); + assert_eq!(pair.public_key().as_ref(), public, "case {case}: ring's public key"); + + let (mut signature, mut signed) = (ours, header); + match case % 4 { + 0 => {} + 1 => signature[rng.below(SIGNATURE_BYTES)] ^= 1 << rng.below(8), + 2 => signed[rng.below(HEADER_BYTES)] ^= 1 << rng.below(8), + _ => { + // S + L: the same scalar, not canonically encoded. + let mut carry = 0u16; + const L: [u8; 32] = [ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, + ]; + for (byte, l) in signature[32..].iter_mut().zip(L) { + let sum = u16::from(*byte) + u16::from(l) + carry; + *byte = sum as u8; + carry = sum >> 8; + } + } + } + let verdict = sig::verify(&public, &signed, &signature).is_ok(); + let message = sig::message(&signed); + assert_eq!(verdict, dalek_strict(&public, message.as_bytes(), &signature), "case {case}: verify_strict"); + assert_eq!(verdict, ring(&public, message.as_bytes(), &signature), "case {case}: ring"); + assert_eq!(verdict, case % 4 == 0, "case {case}"); + } +} + +/// Every key encoding `verify_strict` decodes and finds not weak, this +/// verifier decodes too, but for the ones RFC 8032 section 5.1.3 refuses: a +/// y of p or more. (Its other refusal, x = 0 with the sign bit set, is only +/// at y = 1 or -1, both small-order, which both refuse.) ToyOS verifies only +/// against keys its build minted, which are canonical, so the two never part +/// on a key the tree holds; the test names where they would. +#[test] +fn keys_match_dalek_but_for_the_encodings_rfc_8032_refuses() { + let mut rng = Rng(0xdec0_de25); + let mut parted = 0; + let header = [0u8; HEADER_BYTES]; + for case in 0..CASES { + let mut encoding: [u8; 32] = rng.bytes(); + if case % 2 == 0 { + // y = p + k, k < 19: the only non-canonical encodings there are. + encoding = [0xff; 32]; + encoding[0] = 0xed + rng.below(19) as u8; + encoding[31] = 0x7f | (rng.below(2) as u8) << 7; + } + let ours = sig::verify(&encoding, &header, &[0; SIGNATURE_BYTES]) != Err(Refused::Key); + let dalek = ed25519_dalek::VerifyingKey::from_bytes(&encoding).is_ok_and(|key| !key.is_weak()); + // y < p, p = 2^255 - 19: anything but ed..ff, ff.., 7f in its 255 bits. + let canonical = !(encoding[0] >= 0xed && encoding[1..31].iter().all(|&b| b == 0xff) && encoding[31] & 0x7f == 0x7f); + assert_eq!(ours, dalek && canonical, "case {case}: {encoding:02x?}"); + parted += usize::from(dalek && !canonical); + } + assert!(parted > 0, "no encoding where verify_strict and RFC 8032 part was drawn"); +} diff --git a/loader/Cargo.toml b/loader/Cargo.toml index dce0d6e0405..dead85d156f 100644 --- a/loader/Cargo.toml +++ b/loader/Cargo.toml @@ -37,8 +37,3 @@ toyos-tsc = { path = "../lib/tsc" } toyos-update = { path = "../lib/update" } # Where on ROOT the patch for this CPU is, as the build names it. toyos-microcode = { path = "../lib/microcode" } -# Not hashed with here: `force-soft`, unified into the `sha2` that -# `ed25519-dalek` takes its SHA-512 from inside a verification. This target is -# soft-float, and the x86 backend reaches for SSE and SHA-NI registers a UEFI -# application may not assume are its own. -sha2 = { version = "0.10", default-features = false, features = ["force-soft"] } diff --git a/loader/src/slot.rs b/loader/src/slot.rs index 1f396773216..566cf25654f 100644 --- a/loader/src/slot.rs +++ b/loader/src/slot.rs @@ -227,14 +227,16 @@ fn signed_header(bs: &BootServices, which: Which, slot: &Slot) -> Result<(Header let header_bytes: &[u8; toyos_update::image::HEADER_BYTES] = signed[..toyos_update::image::HEADER_BYTES].try_into().expect("a signed header begins with its header"); let signature: [u8; SIGNATURE_BYTES] = toyos_update::image::signature_of(&signed); + let began = crate::arch::counter(); sig::verify(&KEY, header_bytes, &signature).map_err(|why| { println!("{HEAD} {letter}: {why}"); Refusal::Signature })?; + let ticks = crate::arch::counter().wrapping_sub(began); let digest = toyos_update::sha256(&signed); let mut hex = [0u8; 64]; println!( - "{HEAD} {letter}: signed header {} verifies under this loader's key, version {}", + "{HEAD} {letter}: signed header {} verifies under this loader's key, version {}, in {ticks} counter ticks", toyos_update::hex(&digest, &mut hex), header.version );