Repository navigation
fix(pkg): give eos_pkg a real trust anchor, or refuse to verify - #120
Merged
srpatcha merged 6 commits intoSep 1, 2026
Merged
Conversation
Two independent breakages, both in "CI — eos" on master (9861a3b). net/src/net_posix.c did not compile in the default configuration. net.h wraps its declarations in `#if EOS_ENABLE_NET`, and nothing sets that flag without a product profile or a test build. CMake, however, adds net_posix.c to eos_net whenever the build is not cross-compiling — the flag never enters that decision. So the header expanded to nothing while the file still defined the API against it: net/src/net_posix.c:43:30: error: unknown type name 'eos_net_addr_t' net.c already guards its body the same way and survives; the POSIX mapping was the half that did not. Guarded so the two translation units appear and disappear together. This is the exact gap the "Default configuration" job was added to catch, and it caught it. tests/test_net.c failed to link: undefined reference to `assert' The file defines CHECK() precisely because CI builds Release, NDEBUG deletes assert() along with the expression inside it, and half these checks wrap the call under test — the comment above the macro says so. Ten call sites in the POSIX section still used bare assert(), including assert(eos_net_connect(...) == 0), the exact pattern named there. They were invisible until embeddedos-org#104 added -UNDEBUG and stopped assert() being a no-op; the file never includes <assert.h>, so it resolved to an implicit function and failed at link. Converted to CHECK(), which the file already establishes as the right macro here. Verified locally on macOS, all three configurations CI builds: cmake -B build/host -DCMAKE_BUILD_TYPE=Release builds (fails on master) -DEOS_PRODUCT=medical / aerospace both build -DEOS_BUILD_TESTS=ON -DEOS_PRODUCT=vbox_test + ctest 34/34 passed Negative control: the same default-configuration build against unpatched origin/master reproduces the unknown-type errors verbatim.
Kartikey1306
force-pushed
the
fix/pkg-trust-anchor
branch
from
September 1, 2026 15:24
a6171fd to
ad756fb
Compare
Contributor
Author
|
Rebased onto #114 (
Both are in the stack now, so this branch builds on its own. Local after the rebase: 39/39 ctest, on macOS, Release. The stack fast-forwards from One thing this rebase made tidier than I expected: #114 is "test suites that |
…rd it Nine test_*.c files in tests/ are named by no add_executable(), so they are compiled by nothing. Nothing reports that: the file stays in the tree, ctest counts one fewer test, and the run is green. test_mpu_validate.c is the one that matters. It landed with the MPU fix it guards (embeddedos-org#109) and a later merge kept the file while dropping the target that built it, so its six cases -- regions the MPU cannot program, and the divide-before-check that preceded them -- have not run since. Four of the nine build and pass as they are: test_mpu_validate 6 cases, registration restored test_hal_stm32f4 21 cases; it needed only the board header directory on the include path, which is the whole reason it never ran test_e2e kernel boot-to-task path test_net_mock eNet socket API against a mock transport ctest goes from 34 to 38. The other five stay out, each for a reason recorded next to its name in the new guard: one hangs because it asserts stub semantics against the real POSIX implementation; one cannot link because backends/ is in no add_subdirectory(); and three would pass without testing anything -- test_profiles reports 0/0 because its directory allowlist admits only "products", which holds no *.yaml; test_performance_benchmarks asserts a wall-clock threshold; test_emulation_simulation asserts a literal against itself one line after assigning it. tests/unit/test_cmake_test_registration.py makes the omission fail. It parses tests/CMakeLists.txt statically -- no cmake, no compiler -- and asserts every suite is either built or listed with a reason, that every built target is registered with ctest, that the exclusion list has no stale or non-existent entries, and, by name, that test_mpu_validate is built. Dropping that registration again fails two of the five. The eBoot repo has carried the same guard since a merge dropped test_fw_transport there; this is the same file adapted to this layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
services/pkg/eos_pkg.c checked package signatures against
static const uint8_t eos_pkg_public_key[32] = {0};
embeddedos-org#99 stopped that key accepting every package. It did not make package
signing work: the anchor is still all zeros, so eos_pkg now rejects every
package, including correctly signed ones, and reports "signature
verification failed" while doing it -- blaming the package for a key that
was never provisioned.
The key comes from the platform now. eos_pkg_set_trust_anchor() is the
seam a keystore or a provisioning step fills, EOS_PKG_TRUST_ANCHOR_HEX is
the build-time form, and with neither configured verification refuses and
names the reason. EOS_ALLOW_UNSIGNED_PKG is the opt-in for tests and
bring-up, matching EOS_ALLOW_STUB_CRYPTO in services/crypto and
EOS_ALLOW_UNSIGNED_OTA in services/ota.
An anchor is validated when it is installed rather than once per package,
so an unusable key is reported as an unusable key. That needed embeddedos-org#99's
checks to be reachable, so ed25519_public_key_is_usable() exports them
rather than duplicating the constants.
services/pkg was in no CMakeLists.txt, so nothing compiled this file and
no test could reach it -- which is why the all-zero anchor survived. It
is now the eos_eapp library (pkg/ already owns the name eos_pkg for the
build-recipe registry).
Closes the second half of embeddedos-org#98.
Adding services/pkg to the build put it in the ARM Cortex-M4 leg too, where it does not belong and does not compile: eos_pkg.c lists directories, spawns processes and waits on them, so it needs <dirent.h>, <unistd.h> and <sys/wait.h>. Bare-metal newlib turns the first of those into a hard #error. Gated on CMAKE_CROSSCOMPILING, which is the switch net_posix.c already uses for the same reason, and for the same stated reason: EOS_PLATFORM defaults to "linux" and a cross toolchain file does not change it, so that condition is true even for a Cortex-M build. Verified with a toolchain file that sets CMAKE_SYSTEM_NAME while keeping the host compiler, so the CMake logic can be checked without arm-none-eabi installed: eos_eapp and test_pkg_trust_anchor are not generated under it, and both are present in the host build. Host ctest 39/39.
CodeQL flags this PR with one new high-severity alert:
tests/test_pkg_trust_anchor.c:128
File created without restricting permissions
A file may be created here with mode 0666, which would make it
world-writable.
fopen(path, "wb") creates with 0666 & ~umask, so on a permissive umask the
fixture is writable by anyone on the machine. These fixtures are the signed
packages the verifier is about to trust, so a world-writable one lets
anything on the box rewrite the payload between the write here and the read
under test -- which is the very substitution the suite exists to detect.
Create through open(O_CREAT|O_WRONLY|O_TRUNC, S_IRUSR|S_IWUSR) and fdopen
the descriptor. Verified the call yields 0600.
39/39.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kartikey1306
force-pushed
the
fix/pkg-trust-anchor
branch
from
September 1, 2026 15:52
c8cac16 to
f73eb92
Compare
srpatcha
approved these changes
Sep 1, 2026
Kartikey1306
added a commit
to Kartikey1306/eos
that referenced
this pull request
Sep 3, 2026
…ry directly Answers the review on embeddedos-org#122. The malleability fix itself was found correct; these are the findings around it. Finding 1 (High) -- the digest was printed and never compared, so the shared contract could not fail. EOS_ED25519_CONTRACT_DIGEST came from the header this repo's own generator had just written, a hash taken over its own output, so editing the generator here and not in eBoot produced a self-consistent corpus with a new digest, a green test, and a divergence visible only to a human reading two CI logs. Now pinned as a literal in committed source, together with the vector count and accept count, which are generated for the same reason and were equally unfalsifiable. The eBoot twin (embeddedos-org#89) pins the identical value. Finding 4 (Medium) -- sc_is_canonical()'s accept side had no committed test. Through the public API the boundary is unreachable: S == L and a failed group equation are both simply 0, so an off-by-one refusing a valid S = L - 1 would satisfy every vector in the corpus and reject real signatures only in the field. It is a pure function of 32 bytes, so tests/test_ed25519_canonical_s.c tests it directly -- 0, 1, L-1, L, L+1, 2^252, 2^252+2^251 and all-ones -- with no signing key and no `cryptography` module. ed25519_verify.c is compiled a second time with EOS_ED25519_TEST_HOOKS to give the function external linkage; the shipped build is unchanged. This is the case the finding was about, reproduced. With `S == L` accepted: test_ed25519_canonical_s [FAIL] at the S = L case test_ed25519_contract [PASS] all 76 vectors The corpus cannot see it. The direct test can. Also corrected while writing it: 2^252 is BELOW L (L = 2^252 + 2.77e37), so it is in range. The value that is both non-canonical and passed by ref10's `signature[63] & 224` test is 2^252 + 2^251 -- 0x18 & 224 == 0. Both are now asserted, the second alongside the mask check that shows why the partial test misses it. Finding 2 (Medium) -- tests/vectors/ed25519_contract_vectors.h is now committed rather than generated. CMAKE_CURRENT_SOURCE_DIR preceded CMAKE_CURRENT_BINARY_DIR on the include path, so a stale source-tree copy would silently win over the generated one -- and the PR body's own re-derivation command wrote to exactly that path. Committing removes the shadow, makes the corpus auditable from a tag, and drops find_package(Python3 ... REQUIRED) from a pure-C suite. NOT fixed here, and stated rather than left implied: finding 3, the corpus omitting the y >= p (non-canonical public key) family. That is the class most likely to diverge between the two verifiers and it is worth adding -- but adding vectors changes the digest, which both repos now pin, so it has to land in eos and eBoot together in one coordinated change. That is the mechanism working as intended, and it is a follow-up rather than something to slip into this PR. Verified: ctest 41/41 PASS test_ed25519_contract 76 vectors, 3 accept, 73 refuse test_ed25519_canonical_s PASS rebased onto master; conflicts in tests/CMakeLists.txt and services/crypto/src/ed25519_verify.c resolved additively -- master's ed25519_public_key_is_usable() (embeddedos-org#120) and this branch's sc_is_canonical() both kept. Refs embeddedos-org#122
Kartikey1306
added a commit
to Kartikey1306/eos
that referenced
this pull request
Sep 3, 2026
…ry directly Answers the review on embeddedos-org#122. The malleability fix itself was found correct; these are the findings around it. Finding 1 (High) -- the digest was printed and never compared, so the shared contract could not fail. EOS_ED25519_CONTRACT_DIGEST came from the header this repo's own generator had just written, a hash taken over its own output, so editing the generator here and not in eBoot produced a self-consistent corpus with a new digest, a green test, and a divergence visible only to a human reading two CI logs. Now pinned as a literal in committed source, together with the vector count and accept count, which are generated for the same reason and were equally unfalsifiable. The eBoot twin (embeddedos-org#89) pins the identical value. Finding 4 (Medium) -- sc_is_canonical()'s accept side had no committed test. Through the public API the boundary is unreachable: S == L and a failed group equation are both simply 0, so an off-by-one refusing a valid S = L - 1 would satisfy every vector in the corpus and reject real signatures only in the field. It is a pure function of 32 bytes, so tests/test_ed25519_canonical_s.c tests it directly -- 0, 1, L-1, L, L+1, 2^252, 2^252+2^251 and all-ones -- with no signing key and no `cryptography` module. ed25519_verify.c is compiled a second time with EOS_ED25519_TEST_HOOKS to give the function external linkage; the shipped build is unchanged. This is the case the finding was about, reproduced. With `S == L` accepted: test_ed25519_canonical_s [FAIL] at the S = L case test_ed25519_contract [PASS] all 76 vectors The corpus cannot see it. The direct test can. Also corrected while writing it: 2^252 is BELOW L (L = 2^252 + 2.77e37), so it is in range. The value that is both non-canonical and passed by ref10's `signature[63] & 224` test is 2^252 + 2^251 -- 0x18 & 224 == 0. Both are now asserted, the second alongside the mask check that shows why the partial test misses it. Finding 2 (Medium) -- tests/vectors/ed25519_contract_vectors.h is now committed rather than generated. CMAKE_CURRENT_SOURCE_DIR preceded CMAKE_CURRENT_BINARY_DIR on the include path, so a stale source-tree copy would silently win over the generated one -- and the PR body's own re-derivation command wrote to exactly that path. Committing removes the shadow, makes the corpus auditable from a tag, and drops find_package(Python3 ... REQUIRED) from a pure-C suite. NOT fixed here, and stated rather than left implied: finding 3, the corpus omitting the y >= p (non-canonical public key) family. That is the class most likely to diverge between the two verifiers and it is worth adding -- but adding vectors changes the digest, which both repos now pin, so it has to land in eos and eBoot together in one coordinated change. That is the mechanism working as intended, and it is a follow-up rather than something to slip into this PR. Verified: ctest 41/41 PASS test_ed25519_contract 76 vectors, 3 accept, 73 refuse test_ed25519_canonical_s PASS rebased onto master; conflicts in tests/CMakeLists.txt and services/crypto/src/ed25519_verify.c resolved additively -- master's ed25519_public_key_is_usable() (embeddedos-org#120) and this branch's sc_is_canonical() both kept. Refs embeddedos-org#122
Kartikey1306
added a commit
to Kartikey1306/eos
that referenced
this pull request
Sep 3, 2026
Retitles and reduces this PR to what it still does, per the review. Both repairs it was opened for have landed on master by other routes: net/src/net_posix.c is byte-identical to origin/master, and master's default configuration builds clean. Against current master the branch's only real content was this one line -- and merging it as it stood would have *reverted* 646 lines that landed since, including embeddedos-org#120's pkg trust anchor, tests/test_pkg_trust_anchor.c and tests/unit/test_cmake_test_registration.py. Reset to master and re-applied the one surviving change rather than leave a stale branch that still looks mergeable. The include is genuinely unused: tests/test_net.c has zero assert() calls and uses its own CHECK() macro at 22 call sites. Worth correcting the record, per finding 2: embeddedos-org#124 added it one commit ago as "the missing assert.h include that broke master", and the file had no assert() at embeddedos-org#124's parent either. What fixed that link error was the assert() -> CHECK() conversion already on master; the include was never load-bearing, so removing it changes nothing except the next reader's understanding. Stacked on embeddedos-org#127, which restores the test_crypto_ed25519_loworder registration master is currently red on. Verified: pytest tests/ 15 passed ctest 39/39 PASS Refs embeddedos-org#116, embeddedos-org#124
Kartikey1306
added a commit
to Kartikey1306/eos
that referenced
this pull request
Sep 3, 2026
… the guard checks Reduces this PR to what it still contributes, and answers the review. The four registrations it opened with have landed on master by other routes -- test_mpu_validate, test_e2e, test_hal_stm32f4 and the registration guard are all there. Merging the branch as it stood would have reverted 505 lines that landed since, including embeddedos-org#120's pkg trust anchor. Reset onto embeddedos-org#127 and kept only what is still true. Finding 1 (Medium) -- test_net_mock was registered with no target_link_libraries and includes no EmbeddedOS header. Its mock_net_* functions are called directly rather than standing in for eos_net_*, so no change to net/ can make it fail: 614 lines asserting a mock against itself. Registering it added a green ctest entry covering nothing, which is precisely the failure mode the guard in this same commit exists to surface -- and the guard's own NOT_BUILT policy is where it belongs. Listed there with the reason, including what would make it worth building: driving the real eos_net_* API against the mock transport, which is what its docblock already claims it does. Finding 2 (Low) -- documented the guard's limit where the policy is defined. It checks that every suite is built and registered with ctest. It does not check that a suite reaches the code it is named after, and a file that links nothing satisfies it completely. Worth stating next to NOT_BUILT, because that is the list someone reaches for when the guard fires. Findings 3 and 4 are moot at this head: the test_mpu_validate comment and test_net_mock's vestigial assert.h include both belonged to hunks that are either upstream or removed here. Verified: pytest tests/ 15 passed ctest 37/37 PASS (was 38 with test_net_mock, which tested nothing) Refs embeddedos-org#114
Kartikey1306
added a commit
to Kartikey1306/eos
that referenced
this pull request
Sep 3, 2026
… the guard checks Reduces this PR to what it still contributes, and answers the review. The four registrations it opened with have landed on master by other routes -- test_mpu_validate, test_e2e, test_hal_stm32f4 and the registration guard are all there. Merging the branch as it stood would have reverted 505 lines that landed since, including embeddedos-org#120's pkg trust anchor. Reset onto embeddedos-org#127 and kept only what is still true. Finding 1 (Medium) -- test_net_mock was registered with no target_link_libraries and includes no EmbeddedOS header. Its mock_net_* functions are called directly rather than standing in for eos_net_*, so no change to net/ can make it fail: 614 lines asserting a mock against itself. Registering it added a green ctest entry covering nothing, which is precisely the failure mode the guard in this same commit exists to surface -- and the guard's own NOT_BUILT policy is where it belongs. Listed there with the reason, including what would make it worth building: driving the real eos_net_* API against the mock transport, which is what its docblock already claims it does. Finding 2 (Low) -- documented the guard's limit where the policy is defined. It checks that every suite is built and registered with ctest. It does not check that a suite reaches the code it is named after, and a file that links nothing satisfies it completely. Worth stating next to NOT_BUILT, because that is the list someone reaches for when the guard fires. Findings 3 and 4 are moot at this head: the test_mpu_validate comment and test_net_mock's vestigial assert.h include both belonged to hunks that are either upstream or removed here. Verified: pytest tests/ 15 passed ctest 38/38 PASS (test_net_mock is no longer among them; the count is unchanged from master because embeddedos-org#127, which this is stacked on, adds test_crypto_ed25519_loworder -- a suite that does exercise real code.) Refs embeddedos-org#114
This was referenced Sep 3, 2026
Kartikey1306
added a commit
to Kartikey1306/eos
that referenced
this pull request
Sep 4, 2026
…ry directly Answers the review on embeddedos-org#122. The malleability fix itself was found correct; these are the findings around it. Finding 1 (High) -- the digest was printed and never compared, so the shared contract could not fail. EOS_ED25519_CONTRACT_DIGEST came from the header this repo's own generator had just written, a hash taken over its own output, so editing the generator here and not in eBoot produced a self-consistent corpus with a new digest, a green test, and a divergence visible only to a human reading two CI logs. Now pinned as a literal in committed source, together with the vector count and accept count, which are generated for the same reason and were equally unfalsifiable. The eBoot twin (embeddedos-org#89) pins the identical value. Finding 4 (Medium) -- sc_is_canonical()'s accept side had no committed test. Through the public API the boundary is unreachable: S == L and a failed group equation are both simply 0, so an off-by-one refusing a valid S = L - 1 would satisfy every vector in the corpus and reject real signatures only in the field. It is a pure function of 32 bytes, so tests/test_ed25519_canonical_s.c tests it directly -- 0, 1, L-1, L, L+1, 2^252, 2^252+2^251 and all-ones -- with no signing key and no `cryptography` module. ed25519_verify.c is compiled a second time with EOS_ED25519_TEST_HOOKS to give the function external linkage; the shipped build is unchanged. This is the case the finding was about, reproduced. With `S == L` accepted: test_ed25519_canonical_s [FAIL] at the S = L case test_ed25519_contract [PASS] all 76 vectors The corpus cannot see it. The direct test can. Also corrected while writing it: 2^252 is BELOW L (L = 2^252 + 2.77e37), so it is in range. The value that is both non-canonical and passed by ref10's `signature[63] & 224` test is 2^252 + 2^251 -- 0x18 & 224 == 0. Both are now asserted, the second alongside the mask check that shows why the partial test misses it. Finding 2 (Medium) -- tests/vectors/ed25519_contract_vectors.h is now committed rather than generated. CMAKE_CURRENT_SOURCE_DIR preceded CMAKE_CURRENT_BINARY_DIR on the include path, so a stale source-tree copy would silently win over the generated one -- and the PR body's own re-derivation command wrote to exactly that path. Committing removes the shadow, makes the corpus auditable from a tag, and drops find_package(Python3 ... REQUIRED) from a pure-C suite. NOT fixed here, and stated rather than left implied: finding 3, the corpus omitting the y >= p (non-canonical public key) family. That is the class most likely to diverge between the two verifiers and it is worth adding -- but adding vectors changes the digest, which both repos now pin, so it has to land in eos and eBoot together in one coordinated change. That is the mechanism working as intended, and it is a follow-up rather than something to slip into this PR. Verified: ctest 41/41 PASS test_ed25519_contract 76 vectors, 3 accept, 73 refuse test_ed25519_canonical_s PASS rebased onto master; conflicts in tests/CMakeLists.txt and services/crypto/src/ed25519_verify.c resolved additively -- master's ed25519_public_key_is_usable() (embeddedos-org#120) and this branch's sc_is_canonical() both kept. Refs embeddedos-org#122
srpatcha
pushed a commit
that referenced
this pull request
Sep 8, 2026
* fix(build): restore the low-order Ed25519 suite that #93 removed `CI — eos` and `Python Unit Tests` are red on master (769191a), and both fail for one reason: these suites exist in tests/ but no add_executable() in tests/CMakeLists.txt builds them, so they never run: ['test_crypto_ed25519_loworder.c'] 3aa8644 (#93, "remove duplicate test targets from tests/CMakeLists.txt") removed all four lines that build and register test_crypto_ed25519_loworder. It was not a duplicate — there was exactly one registration before that commit, and it took it. What stopped running is the suite that guards #101: Ed25519 rejecting low-order public keys. A low-order key makes every term of the verification equation collapse to the identity regardless of the message, so a signature of all zeros verifies against any content at all. The fix is still in services/crypto/src/ed25519_verify.c and still correct; nothing has been checking it since #93 merged, and nothing would have noticed if a later change had removed it too. Restored verbatim from 3aa8644^, in its original position before test_crypto_sha512. No other file changes. The guard that caught this is test_cmake_test_registration.py, added recently. It did exactly its job — this is the first thing it found. Verified: test_cmake_test_registration.py -> 5 passed (1 failed on master) pytest tests/ -> 15 passed (14 passed 1 failed on master) test_crypto_ed25519_loworder -> 5/5 tests passed ctest -> 39/39 passed (master builds 38) * chore(tests): drop the unused assert.h include from test_net.c Retitles and reduces this PR to what it still does, per the review. Both repairs it was opened for have landed on master by other routes: net/src/net_posix.c is byte-identical to origin/master, and master's default configuration builds clean. Against current master the branch's only real content was this one line -- and merging it as it stood would have *reverted* 646 lines that landed since, including #120's pkg trust anchor, tests/test_pkg_trust_anchor.c and tests/unit/test_cmake_test_registration.py. Reset to master and re-applied the one surviving change rather than leave a stale branch that still looks mergeable. The include is genuinely unused: tests/test_net.c has zero assert() calls and uses its own CHECK() macro at 22 call sites. Worth correcting the record, per finding 2: #124 added it one commit ago as "the missing assert.h include that broke master", and the file had no assert() at #124's parent either. What fixed that link error was the assert() -> CHECK() conversion already on master; the include was never load-bearing, so removing it changes nothing except the next reader's understanding. Stacked on #127, which restores the test_crypto_ed25519_loworder registration master is currently red on. Verified: pytest tests/ 15 passed ctest 39/39 PASS Refs #116, #124 * ci: pin the eBoot and ebuild checkouts instead of floating on master Every open eos pull request is currently red on `EoS Full-Stack Simulation`, and none of them caused it. The kernel job checks out embeddedos-org/eBoot at `ref: master`, and eBoot master does not compile: include/eos_image.h:135 and :142 static-assert offsetof(eos_image_header_t, reserved) for a member that became tlv_len + tlv_hash, and core/ed25519_verify.c redefines point_is_identity -- two pairs of PRs that each merged clean and broke the build together. eBoot #94 repairs it and waits on review; until it lands, no change to any eos PR can make this job pass, and after it lands the next broken eBoot master does the same thing again. A floating `ref: master` makes every eos PR's CI depend on the moment-to- moment state of another repository's default branch. Pinning the two compiled-against checkouts makes the job a function of this repository plus two recorded SHAs -- red means the PR broke something, green means it did not, and a cross-repo bump is a reviewable one-line diff with CI on it. EBOOT_COMMIT a172a6d6e1e6 the newest eBoot master ancestor that compiles; found by walking master back and building each candidate. Its child 8a015b2 already carries the ed25519 redefinition. EBUILD_COMMIT e5d8052f3e2c ebuild's current master, frozen as-is. Both of its checkouts feed steps that are commented out (eFab does not exist), so this pin only removes the float. The middleware matrix (10 repos) still floats; it runs those repos' own tests rather than compiling eos against them, and 10 more pins deserve their own decision. When eBoot #94 merges, bump EBOOT_COMMIT to the merge SHA. Verified before pushing, master vs pin under the job's own configuration (-DEBLDR_BOARD=qemu_arm64 -DEBLDR_VERIFY_STAGE1=OFF -DCMAKE_BUILD_TYPE=Release, cross-compile mode): eBoot@master fails at the eos_image.h asserts, the exact error in the job log; eBoot@a172a6d builds every target, rc=0. Both pinned SHAs verified reachable from their repos' master. Workflow parses; the three `ref:` edits and the env block are the whole diff. This PR's own simulation run is the live test: it uses this branch's workflow and should go green while every master-based PR stays red. Carried onto this branch from #135 (001128d) so this PR's own simulation job runs against a buildable eBoot: each pull request executes its own copy of the workflow, so the fix greens a PR only once its branch contains it. Byte-identical to #135's commit -- git drops it as already-upstream the moment #135 merges. The pin itself is #135's to review.
srpatcha
pushed a commit
that referenced
this pull request
Sep 8, 2026
…rd it (#114) * fix(build): restore the low-order Ed25519 suite that #93 removed `CI — eos` and `Python Unit Tests` are red on master (769191a), and both fail for one reason: these suites exist in tests/ but no add_executable() in tests/CMakeLists.txt builds them, so they never run: ['test_crypto_ed25519_loworder.c'] 3aa8644 (#93, "remove duplicate test targets from tests/CMakeLists.txt") removed all four lines that build and register test_crypto_ed25519_loworder. It was not a duplicate — there was exactly one registration before that commit, and it took it. What stopped running is the suite that guards #101: Ed25519 rejecting low-order public keys. A low-order key makes every term of the verification equation collapse to the identity regardless of the message, so a signature of all zeros verifies against any content at all. The fix is still in services/crypto/src/ed25519_verify.c and still correct; nothing has been checking it since #93 merged, and nothing would have noticed if a later change had removed it too. Restored verbatim from 3aa8644^, in its original position before test_crypto_sha512. No other file changes. The guard that caught this is test_cmake_test_registration.py, added recently. It did exactly its job — this is the first thing it found. Verified: test_cmake_test_registration.py -> 5 passed (1 failed on master) pytest tests/ -> 15 passed (14 passed 1 failed on master) test_crypto_ed25519_loworder -> 5/5 tests passed ctest -> 39/39 passed (master builds 38) * test: stop building test_net_mock, which covers nothing, and say what the guard checks Reduces this PR to what it still contributes, and answers the review. The four registrations it opened with have landed on master by other routes -- test_mpu_validate, test_e2e, test_hal_stm32f4 and the registration guard are all there. Merging the branch as it stood would have reverted 505 lines that landed since, including #120's pkg trust anchor. Reset onto #127 and kept only what is still true. Finding 1 (Medium) -- test_net_mock was registered with no target_link_libraries and includes no EmbeddedOS header. Its mock_net_* functions are called directly rather than standing in for eos_net_*, so no change to net/ can make it fail: 614 lines asserting a mock against itself. Registering it added a green ctest entry covering nothing, which is precisely the failure mode the guard in this same commit exists to surface -- and the guard's own NOT_BUILT policy is where it belongs. Listed there with the reason, including what would make it worth building: driving the real eos_net_* API against the mock transport, which is what its docblock already claims it does. Finding 2 (Low) -- documented the guard's limit where the policy is defined. It checks that every suite is built and registered with ctest. It does not check that a suite reaches the code it is named after, and a file that links nothing satisfies it completely. Worth stating next to NOT_BUILT, because that is the list someone reaches for when the guard fires. Findings 3 and 4 are moot at this head: the test_mpu_validate comment and test_net_mock's vestigial assert.h include both belonged to hunks that are either upstream or removed here. Verified: pytest tests/ 15 passed ctest 38/38 PASS (test_net_mock is no longer among them; the count is unchanged from master because #127, which this is stacked on, adds test_crypto_ed25519_loworder -- a suite that does exercise real code.) Refs #114 * ci: pin the eBoot and ebuild checkouts instead of floating on master Every open eos pull request is currently red on `EoS Full-Stack Simulation`, and none of them caused it. The kernel job checks out embeddedos-org/eBoot at `ref: master`, and eBoot master does not compile: include/eos_image.h:135 and :142 static-assert offsetof(eos_image_header_t, reserved) for a member that became tlv_len + tlv_hash, and core/ed25519_verify.c redefines point_is_identity -- two pairs of PRs that each merged clean and broke the build together. eBoot #94 repairs it and waits on review; until it lands, no change to any eos PR can make this job pass, and after it lands the next broken eBoot master does the same thing again. A floating `ref: master` makes every eos PR's CI depend on the moment-to- moment state of another repository's default branch. Pinning the two compiled-against checkouts makes the job a function of this repository plus two recorded SHAs -- red means the PR broke something, green means it did not, and a cross-repo bump is a reviewable one-line diff with CI on it. EBOOT_COMMIT a172a6d6e1e6 the newest eBoot master ancestor that compiles; found by walking master back and building each candidate. Its child 8a015b2 already carries the ed25519 redefinition. EBUILD_COMMIT e5d8052f3e2c ebuild's current master, frozen as-is. Both of its checkouts feed steps that are commented out (eFab does not exist), so this pin only removes the float. The middleware matrix (10 repos) still floats; it runs those repos' own tests rather than compiling eos against them, and 10 more pins deserve their own decision. When eBoot #94 merges, bump EBOOT_COMMIT to the merge SHA. Verified before pushing, master vs pin under the job's own configuration (-DEBLDR_BOARD=qemu_arm64 -DEBLDR_VERIFY_STAGE1=OFF -DCMAKE_BUILD_TYPE=Release, cross-compile mode): eBoot@master fails at the eos_image.h asserts, the exact error in the job log; eBoot@a172a6d builds every target, rc=0. Both pinned SHAs verified reachable from their repos' master. Workflow parses; the three `ref:` edits and the env block are the whole diff. This PR's own simulation run is the live test: it uses this branch's workflow and should go green while every master-based PR stays red. Carried onto this branch from #135 (001128d) so this PR's own simulation job runs against a buildable eBoot: each pull request executes its own copy of the workflow, so the fix greens a PR only once its branch contains it. Byte-identical to #135's commit -- git drops it as already-upstream the moment #135 merges. The pin itself is #135's to review.
srpatcha
added a commit
that referenced
this pull request
Sep 8, 2026
…Boot (#122) * fix(build): restore the low-order Ed25519 suite that #93 removed `CI — eos` and `Python Unit Tests` are red on master (769191a), and both fail for one reason: these suites exist in tests/ but no add_executable() in tests/CMakeLists.txt builds them, so they never run: ['test_crypto_ed25519_loworder.c'] 3aa8644 (#93, "remove duplicate test targets from tests/CMakeLists.txt") removed all four lines that build and register test_crypto_ed25519_loworder. It was not a duplicate — there was exactly one registration before that commit, and it took it. What stopped running is the suite that guards #101: Ed25519 rejecting low-order public keys. A low-order key makes every term of the verification equation collapse to the identity regardless of the message, so a signature of all zeros verifies against any content at all. The fix is still in services/crypto/src/ed25519_verify.c and still correct; nothing has been checking it since #93 merged, and nothing would have noticed if a later change had removed it too. Restored verbatim from 3aa8644^, in its original position before test_crypto_sha512. No other file changes. The guard that caught this is test_cmake_test_registration.py, added recently. It did exactly its job — this is the first thing it found. Verified: test_cmake_test_registration.py -> 5 passed (1 failed on master) pytest tests/ -> 15 passed (14 passed 1 failed on master) test_crypto_ed25519_loworder -> 5/5 tests passed ctest -> 39/39 passed (master builds 38) * fix(ed25519): reject a non-canonical S, and share the contract with eBoot Two things, and the second found the first. eos and eBoot each carry their own Ed25519 verifier. Different code, opposite return conventions — this one returns 1 to accept, eBoot's returns EOS_OK — doing the same job on the same wire format. When only eos rejected low-order public keys, nothing in either repo could notice: there is no shared build, and the two are far too different for a source diff to say anything. That divergence is why eBoot #73 survived here after this side had already fixed it. tests/vectors/ed25519_contract_vectors.h is the part that can be shared: 76 vectors of pure data with a SHA-256 over them, byte-identical to eBoot's copy (eBoot #89). Each side runs its own verifier and prints the digest, so a change to one copy that does not reach the other shows up as two different digests. On its first run it failed here, on three vectors eBoot passes. ed25519_verify() had only ref10's partial check, `signature[63] & 224`. That rejects any S with a bit set above 2^252, but L = 2^252 + 27742317777372353535851937790883648493 so every S in [L, 2^253) passes it while being non-canonical. Adding L to a valid S lands squarely in that band, and (R, S) and (R, S + L) both verified — signature malleability. Anything treating a signature as a unique identifier (deduplication, replay caches, logging by signature hash) sees two distinct signatures over one message and one key. RFC 8032 5.1.7 requires rejecting S outside [0, L). eBoot's core/ed25519_verify.c has done this all along via s_is_canonical(); this side did not. Added sc_is_canonical(), compared most-significant byte first against a public constant — S is public, so this leaks nothing. Verified: contract vectors, before the fix -> 3 of 76 wrong (all three S+L) contract vectors, after -> 76/76, 3 accepted, 73 refused test_crypto_ed25519_loworder -> 5/5, unchanged ctest -> 35/35 passed digest 1059febedae2b3e3dfaa1ef5b419fb37ed7f0d53b377a3250b53b95a546282a2 identical to eBoot #89's copy Stacked on #116; eos master does not build without it. * build(tests): generate the Ed25519 contract vectors, do not commit them tests/vectors/ed25519_contract_vectors.h is 54 KB of generated data, and the identical file was checked in to BOTH eos and eBoot, along with the generator that produces it -- 61 KB duplicated verbatim across two repositories and kept in step by hand. The generator is pure stdlib and deterministic, so each repo can produce the header into its own build tree instead. tools/gen_ed25519_contract_vectors.py grows an -o/--output flag (stdout stays the default, so the regeneration command in its docstring still works), and tests/CMakeLists.txt drives it from add_custom_command. Only the 6.8 KB generator is now carried twice, and it is source rather than generated output. Verified: the generated header is byte-identical to the file it replaces; both repos still report digest 1059febedae2b3e3dfaa1ef5b419fb37ed7f0d53b377a3250b53b95a546282a2, which is the thing that actually holds the two verifiers to one contract. Suites pass (eos 35/35, eBoot 22/22). Breaking the generator fails the build rather than silently reusing a stale header. The cross-compile legs configure with tests OFF so find_package(Python3) is never reached there; confirmed by configuring under a cross toolchain. * test(ed25519): assert the contract digest, and test the [0, L) boundary directly Answers the review on #122. The malleability fix itself was found correct; these are the findings around it. Finding 1 (High) -- the digest was printed and never compared, so the shared contract could not fail. EOS_ED25519_CONTRACT_DIGEST came from the header this repo's own generator had just written, a hash taken over its own output, so editing the generator here and not in eBoot produced a self-consistent corpus with a new digest, a green test, and a divergence visible only to a human reading two CI logs. Now pinned as a literal in committed source, together with the vector count and accept count, which are generated for the same reason and were equally unfalsifiable. The eBoot twin (#89) pins the identical value. Finding 4 (Medium) -- sc_is_canonical()'s accept side had no committed test. Through the public API the boundary is unreachable: S == L and a failed group equation are both simply 0, so an off-by-one refusing a valid S = L - 1 would satisfy every vector in the corpus and reject real signatures only in the field. It is a pure function of 32 bytes, so tests/test_ed25519_canonical_s.c tests it directly -- 0, 1, L-1, L, L+1, 2^252, 2^252+2^251 and all-ones -- with no signing key and no `cryptography` module. ed25519_verify.c is compiled a second time with EOS_ED25519_TEST_HOOKS to give the function external linkage; the shipped build is unchanged. This is the case the finding was about, reproduced. With `S == L` accepted: test_ed25519_canonical_s [FAIL] at the S = L case test_ed25519_contract [PASS] all 76 vectors The corpus cannot see it. The direct test can. Also corrected while writing it: 2^252 is BELOW L (L = 2^252 + 2.77e37), so it is in range. The value that is both non-canonical and passed by ref10's `signature[63] & 224` test is 2^252 + 2^251 -- 0x18 & 224 == 0. Both are now asserted, the second alongside the mask check that shows why the partial test misses it. Finding 2 (Medium) -- tests/vectors/ed25519_contract_vectors.h is now committed rather than generated. CMAKE_CURRENT_SOURCE_DIR preceded CMAKE_CURRENT_BINARY_DIR on the include path, so a stale source-tree copy would silently win over the generated one -- and the PR body's own re-derivation command wrote to exactly that path. Committing removes the shadow, makes the corpus auditable from a tag, and drops find_package(Python3 ... REQUIRED) from a pure-C suite. NOT fixed here, and stated rather than left implied: finding 3, the corpus omitting the y >= p (non-canonical public key) family. That is the class most likely to diverge between the two verifiers and it is worth adding -- but adding vectors changes the digest, which both repos now pin, so it has to land in eos and eBoot together in one coordinated change. That is the mechanism working as intended, and it is a follow-up rather than something to slip into this PR. Verified: ctest 41/41 PASS test_ed25519_contract 76 vectors, 3 accept, 73 refuse test_ed25519_canonical_s PASS rebased onto master; conflicts in tests/CMakeLists.txt and services/crypto/src/ed25519_verify.c resolved additively -- master's ed25519_public_key_is_usable() (#120) and this branch's sc_is_canonical() both kept. Refs #122 * test(ed25519): hash the vectors, and drop the linkage macro CodeQL flagged Answers the second round on #122. Both findings are defects in my previous commit. The digest pin did not cover the corpus. EOS_ED25519_CONTRACT_DIGEST is a #define inside the generated header; EOS_ED25519_CONTRACT_EXPECTED is a #define in the driver. The test compared two string literals and never hashed a vector, so editing a committed vector changed the data and left the value it was compared against untouched. Raised on the eBoot twin (#89) and stated there to apply here too; it did. contract_digest() now recomputes SHA-256 over the vectors at run time in the generator's serialisation -- public_key[32] || signature[64] || message[message_len] || (uint8_t)expect_accept -- via eos_sha256_* from eos/crypto.h. It compares the result to the pinned literal and to the header's own claim, so a corpus and a header from different generator runs are caught. Verified with the probe from the eBoot review. Changing one byte of the order-1 identity public key: [FAIL] corpus digest changed expected 1059febe...282a2 got 55370deb...cae9 exit 1 That `got` value is byte-identical to the one eBoot's driver produces for the same edit, which is independent confirmation that the two committed corpora really are the same file. Worth recording because it nearly cost me the finding: the first probe run reported PASS. The build had not picked up the header change, so the binary was stale. `touch` on the driver and a rebuild gave the failure above. A probe that does not rebuild proves nothing -- check that the link step ran. CodeQL alert 405, "Static function sc_is_canonical is unreachable" -- caused by the EOS_ED25519_TEST_HOOKS macro I added, which gave the symbol a linkage that varied by build. Removed: the function is static again, and tests/test_ed25519_canonical_s.c includes the translation unit to reach it, which is the ordinary way to test a static and needs no conditional compilation in a crypto source file. eos_crypto is still linked for the ge_*/sha512 helpers the included unit calls -- a static archive contributes only objects needed for undefined symbols, and ed25519_verify.c.o is not one, because the test object already defines everything in it. Verified: ctest 41/41 PASS test_ed25519_canonical_s PASS, links with no duplicate symbols test_ed25519_contract 76 vectors, 3 accept, 73 refuse byte-flip probe FAILS (after a real rebuild) off-by-one probe, `S == L` accepted: test_ed25519_canonical_s FAILS test_ed25519_contract still exit 0 so the direct boundary test still discriminates where the corpus cannot. Refs #122 * docs(ed25519): make the two copies actually byte-identical, drop stale comments Addresses srpatcha's review findings 1 and 3 on #122. Text and comments only; no behaviour change, and the 76 vectors and their digest are untouched. Finding 1 -- the body claimed the corpus was byte-identical to eBoot#89's copy and it was not: the two generators had drifted in their docstrings and in the header comment they emit, so the files differed by three comment lines while all 76 vectors and the digest matched. Adopted eBoot#89's wording verbatim -- it is the more accurate description, since each side recomputes the digest and compares it to a pinned literal rather than merely printing it -- and regenerated. Both repos' tools/gen_ed25519_contract_vectors.py and tests/vectors/ed25519_contract_vectors.h are now byte-identical: sha256 489b1fc0662187841c58e12c650ca1a25a974e4fafe4d4318f4fad62b7e4b5cf which is the value the review measured for eBoot's copy, so the spot-check the finding describes -- hash both files -- now passes. Finding 3 -- three comment blocks described an approach that was abandoned, two of them contradicting the code beside them: (a) ed25519_verify.c opened "Linked, not static, when the test hook is on" six lines above "It stays static", and the function is static. (b) tests/CMakeLists.txt described compiling ed25519_verify.c a second time with EOS_ED25519_TEST_HOOKS. That macro occurred exactly once in the tree -- in that sentence. The next line already describes what the test really does (#include the translation unit). (c) the paragraph saying the vector table "is produced into the build tree rather than committed" sat directly above one saying it is committed. It is committed; the generate-at-build-time paragraph was mine, left behind when that approach was reverted, and it is the half that was false. In each case the surviving text already explained the real design, so this is subtraction. Verified: cmake+ctest 41/41 (the count finding 4 asks for, not the 35/35 still in the body), test_ed25519_contract reports digest 1059feb... over 76 vectors with 3 accepted and 0 wrong, and test_ed25519_canonical_s still passes. --------- Co-authored-by: Srikanth Patchava <srikanth.patchava@outlook.com>
srpatcha
added a commit
to embeddedos-org/eSec
that referenced
this pull request
Oct 6, 2026
…acts The status line said eSec is Implemented at services/crypto/ with "one part Experimental" without naming it, so a reader could take one of the seven crypto suites for the unfinished part. Name package signature verification (services/pkg/eos_pkg.c) in the status line itself. Also correct the paragraph behind it: it described eos_pkg verifying against an all-zero key. embeddedos-org/eos#120 removed that key; eos_pkg now verifies only against a platform-supplied trust anchor (eos_pkg_set_trust_anchor() or EOS_PKG_TRUST_ANCHOR_HEX) and refuses otherwise unless EOS_ALLOW_UNSIGNED_PKG is set. It stays Experimental: eos ships no key and the v2 signature envelope landed on 2026-10-04. Still tracked as embeddedos-org/eos#98.
srpatcha
added a commit
to embeddedos-org/eSec
that referenced
this pull request
Oct 6, 2026
* docs: eSec is Implemented in eos, and one part is not
The README said "Status: Planned. There is no implementation yet." The
crypto services are Implemented under §28 — 15 source files and seven test
suites passing in eos, at services/crypto/.
But flatly upgrading the status would overstate it. Package signature
verification is Experimental at best: services/pkg/eos_pkg.c verifies
against eos_pkg_public_key[32] = {0}, and the low-order-key rejection that
makes an all-zero anchor fail closed rather than open only just landed
(eos#99). A verifier with no real key authenticates nothing, and a README
claiming a working security framework should say so. Tracked as eos#98.
Also replaces the old rationale with v2.0 §10 and §21.1, adds the component
manifest as the way to depend on this, and records why splitting early is
costly — two Ed25519 implementations across eos and eBoot carried the same
bypass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: name the Experimental part in the status line, and update its facts
The status line said eSec is Implemented at services/crypto/ with "one part Experimental" without naming it, so a reader could take one of the seven crypto suites for the unfinished part. Name package signature verification (services/pkg/eos_pkg.c) in the status line itself.
Also correct the paragraph behind it: it described eos_pkg verifying against an all-zero key. embeddedos-org/eos#120 removed that key; eos_pkg now verifies only against a platform-supplied trust anchor (eos_pkg_set_trust_anchor() or EOS_PKG_TRUST_ANCHOR_HEX) and refuses otherwise unless EOS_ALLOW_UNSIGNED_PKG is set. It stays Experimental: eos ships no key and the v2 signature envelope landed on 2026-10-04. Still tracked as embeddedos-org/eos#98.
* docs: recount eos services/crypto/src (16 files at eos dc13c59)
Signed-off-by: Srikanth Patchava <srpatcha@users.noreply.github.com>
---------
Signed-off-by: Srikanth Patchava <srpatcha@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Srikanth Patchava <srpatcha@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the second half of #98 — the trust anchor, which @srpatcha called
"still the more important half" after #99 landed the verifier half.
The state #99 left behind
#99 stopped
services/cryptoaccepting low-order keys, which closed the halfwhere
eos_pkgaccepted every package. The anchor itself is untouched onmaster:So
eos_pkgwent from accepting everything to rejecting everything. Verified —a package whose payload is RFC 8032 §7.1 Test 2's message, carrying Test 2's real
signature, built against
masterunmodified:The package is fine. The key was never provisioned. The message says the
opposite, so the one diagnostic an operator gets points away from the cause.
Why nobody could have caught it
Verified —
services/pkg/eos_pkg.cappears in noCMakeLists.txt:Nothing compiled the file.
tests/test_package.ctestspkg/src/package.c—the build-recipe registry, a different module that happens to own the target
name
eos_pkg. So the anchor was unreachable by any test, which is the reasonit survived four rounds of review.
It compiles clean as it stands (
-Wall -Wextra -Wpedantic, 0 warnings); it wassimply never wired in. It is now the
eos_eapplibrary.The fix
The key comes from the platform, in the shape this repository already uses:
eos_pkg_set_trust_anchor()— where a keystore or provisioning step installs iteos_ota_set_authenticator()(#75)EOS_PKG_TRUST_ANCHOR_HEX— 64 hex characters from the buildEOS_ALLOW_STUB_CRYPTO(#84),EOS_ALLOW_UNSIGNED_OTA(#75)EOS_ALLOW_UNSIGNED_PKGis the opt-in, for tests and bring-up.An anchor is validated when it is installed, not once per package:
That needed #99's checks to be reachable from outside
ed25519_verify(), sothey are exported as
ed25519_public_key_is_usable()rather than duplicated —one implementation of the rule, one set of constants.
The three failures are reported apart because they call for different actions:
fix the provisioning, fix the build, or reject the package.
Tests fail on the unfixed code
tests/test_pkg_trust_anchor.c, 8 tests. Verified they discriminate — builtagainst
master'seos_pkg.cwith only the new symbols shimmed in so it links:and with the anchor-validation half also shimmed in, so the run reaches the
verification path:
Both halves of the change are load-bearing and independently proven.
Coverage: all eight low-order encodings refused as anchors — the all-zero value
this file shipped is only one of them; a real key accepted; a genuinely signed
package accepted under its own key, so a verifier that rejects everything
does not pass; a different real key rejected; a tampered payload rejected with
the header hash recomputed over it, so the SHA-256 check passes and only the
signature can reject it; and a rejected anchor leaving the one in force intact,
so a failed provisioning attempt cannot disarm a working installation.
Verification
cmake -B build/host -DEOS_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Releasetest_pkg_trust_anchoreos_pkg.cunder-Wall -Wextra -Wpedanticmastersame machine, same flagsVerified — the single failure is
test_net, identical on both sides andpre-existing. It is macOS-only:
tests/test_net.ccallsassert()without#include <assert.h>, which glibc admits transitively and clang does not. Nottouched here; worth its own one-line fix.
Left open deliberately
eos_pkg.crunsrm -rf "%s"/rmdir /s /q "%s"through
system()with an interpolated path (eos_rmdir_recursive). Now thatthe file is compiled, that is reachable code. It is a separate change and a
separate argument; flagging it rather than folding it in.
.eapppayload is signed, the header is not —entry-style fields areoutside the signature, the same shape as eBoot fix: reject invalid dependency graph inputs #40. Worth deciding on; not
part of closing eos_pkg accepts any package — signature verified against an all-zero key with no low-order rejection #98.