Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/scripts/ci-gate-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Fail unless every job handed to us succeeded.
#
# Reads the `needs` context as JSON on stdin:
# {"build": {"result": "success"}, "test": {"result": "skipped"}}
#
# Any result other than "success" fails, `skipped` and `cancelled` included: a
# job that did not run did not verify anything, and a required check that
# passes on "did not run" is the fail-open shape this repository has spent #38,
# #59 and #82 removing from the boot path.
#
# This lives in a file rather than inline in the workflow so that the rule can
# be executed by a test with real inputs, instead of a test grepping the YAML
# for the string it expects to find there.
set -euo pipefail

results=$(cat)

if [ -z "$results" ] || [ "$results" = "null" ]; then
echo "::error::CI Gate received no job results; refusing to pass." >&2
exit 1
fi

printf '%s\n' "$results"

bad=$(printf '%s' "$results" | jq -r '
to_entries[]
| select(.value.result != "success")
| " \(.key): \(.value.result)"')

if [ -n "$bad" ]; then
echo "::error::CI Gate failed. These jobs did not succeed:"
printf '%s\n' "$bad"
exit 1
fi

echo "All jobs succeeded."
37 changes: 30 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,7 @@ jobs:
cmake ninja-build gcc-12 g++-12 \
gcc-arm-none-eabi binutils-arm-none-eabi \
lcov gcovr python3-pip
# Install from requirements.txt rather than a hand-maintained list.
# tests/unit/test_sign_image.py guards itself with
# pytest.importorskip("cryptography"), so a dependency that is
# declared nowhere does not fail the job -- it skips the 14 cases
# that pin the signed header format and the run still goes green.
# pytest-cov stays separate: it is a CI-only coverage plugin, not a
# dependency of anything in the repository.
# pyyaml: tests/unit/test_ci_gate.py parses .github/workflows/ci.yml
pip3 install -r requirements.txt pytest-cov

- name: Configure (host)
Expand Down Expand Up @@ -182,3 +176,32 @@ jobs:
generate_release_notes: true
draft: false
prerelease: false

# ── Required check ────────────────────────────────────────────────────────
# One job that succeeds only if every other job in this workflow did, so
# branch protection has a single stable name to require. `required_status_
# checks` is null on master here, the same gap eos#92 and ebuild#87 track:
# nothing builds the merge result before it becomes master.
#
# `release` is deliberately outside the gate: it only runs on tags, so on a
# pull request it is skipped, and a required check that is skipped never
# reports -- the pull request would wait for a status that never arrives.
#
# `if: always()` for the same reason in reverse: without it the gate is
# skipped whenever an earlier job fails, so a real failure would present as a
# pull request that hangs rather than one that goes red.
#
# A non-success result of any kind fails the gate, `skipped` included. A job
# that did not run did not verify anything, and treating that as a pass is
# the fail-open shape #38, #59 and #82 removed from the boot path.
ci-gate:
name: CI Gate
runs-on: ubuntu-22.04
needs: [test, build-arm, static-analysis]
if: always()
steps:
- uses: actions/checkout@v4
- name: Every job in this workflow must have succeeded
env:
RESULTS: ${{ toJSON(needs) }}
run: printf '%s' "$RESULTS" | .github/scripts/ci-gate-check.sh
24 changes: 12 additions & 12 deletions .github/workflows/simulation-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ jobs:
needs: [simulate, cross-platform]
runs-on: ubuntu-latest
steps:
- name: Results
run: |
echo "════════════════════════════════════════════"
echo " EoSim Simulation Sanity Results"
echo "════════════════════════════════════════════"
echo "Simulation (11 platforms): ${{ needs.simulate.result }}"
echo "Cross-Platform (Win/Lin/Mac): ${{ needs.cross-platform.result }}"
echo "════════════════════════════════════════════"
if [ "${{ needs.simulate.result }}" != "success" ]; then
echo "❌ Simulation failed"; exit 1
fi
echo "✅ All simulation checks passed (EoSim steps skipped — no published release)"
# Iterates toJSON(needs) rather than naming each dependency. The
# previous body printed both results and branched on `simulate` alone,
# so a red or skipped `cross-platform` -- three OS legs -- passed the
# gate and it still printed "All simulation checks passed". That is the
# fail-open shape ci.yml's gate was written to remove, in a job whose
# name a maintainer would plausibly require. This form cannot fall out
# of step with `needs:`.
- uses: actions/checkout@v4
- name: Every job in this workflow must have succeeded
env:
RESULTS: ${{ toJSON(needs) }}
run: printf '%s' "$RESULTS" | .github/scripts/ci-gate-check.sh
56 changes: 12 additions & 44 deletions core/ed25519_verify.c
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ static int point_is_identity(gf p[4])
return diff == 0;
}

static void scalarbase(gf r[4], const uint8_t *s)
{
gf q[4];
fe_copy16(q[0], BX);
fe_copy16(q[1], BY);
fe_copy16(q[2], gf1);
fe_mul(q[3], BX, BY);
scalarmult(r, q, s);
}

/* Reject a public key outside the prime-order subgroup.
*
* Decoding a point is not enough. Ed25519 has eight points of low order, and
Expand All @@ -304,51 +314,9 @@ static int point_is_identity(gf p[4])
* so there is no separate constant to transcribe wrongly: a mistyped L would
* reject valid keys, and only in the field.
*
* A arrives negated from unpackneg(). [L](-A) = -[L]A and the identity is its
* own negation, so neither condition is affected by the sign.
*
* Formulation taken from eBoot#57 by @muhammadburhandevv-hub, which reached
* this before I did and states both conditions in one expression.
* The key arrives negated from unpackneg(). [L](-A) = -[L]A and the identity
* is its own negation, so neither condition is affected by the sign.
*/
static int key_has_prime_order(gf A[4])
{
uint8_t order_l[32];
gf q[4], multiple[4];
int i;

for (i = 0; i < 32; i++)
order_l[i] = (uint8_t)ORDER_L[i];
for (i = 0; i < 4; i++)
fe_copy16(q[i], A[i]);

scalarmult(multiple, q, order_l);
return point_is_identity(multiple) && !point_is_identity(A);
}

static void scalarbase(gf r[4], const uint8_t *s)
{
gf q[4];
fe_copy16(q[0], BX);
fe_copy16(q[1], BY);
fe_copy16(q[2], gf1);
fe_mul(q[3], BX, BY);
scalarmult(r, q, s);
}

static int point_is_identity(gf p[4])
{
uint8_t encoded[32];
point_pack(encoded, p);

uint8_t diff = (uint8_t)(encoded[0] ^ 1U);
for (int i = 1; i < 32; i++)
diff |= encoded[i];
return diff == 0;
}

/* Public keys must be non-identity points in Ed25519's prime-order subgroup.
* Merely decoding a point is insufficient: an identity or torsion key can
* make the verification equation true without knowledge of a private key. */
static int public_key_is_valid_subgroup(gf public_key[4])
{
uint8_t order_l[32];
Expand Down
16 changes: 10 additions & 6 deletions include/eos_image.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, tlv_hash) +

/* Every remaining field, pinned.
*
* Four of the fourteen fields were asserted. Transposing two adjacent
* Three of the thirteen field offsets were asserted (the fourth pre-existing
* assert is sizeof, which is not a field). Transposing two adjacent
* same-width fields moves neither sizeof nor any of those four offsets, so it
* compiled clean: with load_addr and entry_addr swapped, all four existing
* asserts still passed and the bootloader would load an image at its entry
Expand All @@ -132,15 +133,18 @@ EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, flags) == 24,
"flags must stay at offset 24");
EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, sig_len) == 61,
"sig_len must stay at offset 61");
EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, reserved) == 62,
"reserved[] must stay at offset 62");
/* tlv_len and tlv_hash are asserted above, where #93 introduced them; the
* 30 bytes they occupy are the ones this block used to pin as reserved[]. */

/* Field widths. An offset assert cannot see a field growing into padding that
* happens to keep every later offset -- reserved[] absorbs exactly that. */
* happens to keep every later offset -- the 30 bytes at 62 absorb exactly
* that, which is why both halves of that span carry a width assert. */
EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->hash) == 32,
"hash[] is 32 bytes on the wire");
EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->reserved) == 30,
"reserved[] is 30 bytes on the wire");
EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->tlv_len) == 2,
"tlv_len is 2 bytes on the wire");
EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->tlv_hash) == 28,
"tlv_hash is 28 bytes on the wire");
EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->signature) == 64,
"signature[] is 64 bytes on the wire");

Expand Down
Loading
Loading