aethel-core 0.7.5

Post-quantum anonymous identity for Rust and WASM. A no_std alternative to static W3C DIDs: per-context identifiers, no reusable public key, context-bound ML-DSA signing. Selective disclosure ships and is being hardened; see SECURITY.md.
Documentation
name: component

# P5-03: the L1 boundary is one artifact embedded by every language. Shipping a
# compiled binary inside a source package is only defensible if anyone can
# rebuild it and get the same bytes, so this job builds the component twice from
# a clean target directory and fails if the two differ.
#
# It also fails if the built hash does not match the one checked into
# `component.sha256`, which is what makes that file a claim rather than a note.
#
# The hash is platform-specific: this runner is the canonical build platform.
# A build of the same commit on Windows or macOS produces different bytes, because
# rustc embeds platform paths and links a different std. That was caught by this
# job's first run - the checked-in hash had come from a Windows build - and is
# documented in the README rather than papered over.

permissions:
  contents: read

on:
  # Scoped to main deliberately. A bare `push:` fires on every branch, so a push
  # to a PR branch ran this job twice, once per trigger, for identical output.
  # At roughly six minutes it is the slowest job in the matrix, so that was half
  # its runner cost wasted on every push. `pull_request` already covers branches.
  push:
    branches: [main]
  pull_request:

jobs:
  # The export check is only worth its name if it can be seen failing. This job
  # builds a component that lacks exactly one export the WIT declares and
  # requires the checker to reject it for that reason alone (see
  # scripts/component-negative-control.py). It runs beside the main job rather
  # than inside it, so its cold build does not lengthen the critical path.
  negative-control:
    name: the export check must reject a component missing an export
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: dtolnay/rust-toolchain@1.97.0
        with:
          targets: wasm32-unknown-unknown

      - name: Install wasm-tools
        run: cargo install wasm-tools --version 1.258.0 --locked

      - name: The checker must test itself
        run: python3 scripts/test_check_component_exports.py

      - name: Build a component with credential.present removed
        run: |

          scratch=$(mktemp -d)
          rmdir "$scratch"
          git worktree add --detach "$scratch" HEAD
          trap 'git worktree remove --force "$scratch" || rm -rf "$scratch"' EXIT
          python3 scripts/component-negative-control.py mutate "$scratch" identity.credential.present
          # A failure here is the scratch build breaking, not the checker
          # rejecting anything, and is reported as such.
          if ! (
            cd "$scratch"
            cargo build --release --target wasm32-unknown-unknown \
              --no-default-features --features component --locked
            wasm-tools component new \
              target/wasm32-unknown-unknown/release/aethel_core.wasm \
              -o "$GITHUB_WORKSPACE/broken.component.wasm"
          ); then
            echo "CONTROL BUILD FAILED: the scratch component did not build."
            echo "This says nothing about the export check; fix the build first."
            exit 1
          fi

      - name: The checker must reject it, because that export is missing
        run: |

          python3 scripts/component-negative-control.py verify \
            wit/aethel-core.wit broken.component.wasm identity.credential.present

  # component.rev is what aethel_core::GIT_REVISION reports. Nothing else checks
  # it: build.rs passes the file through, and the unit tests compare the constant
  # to the file, so a file containing a plausible-looking but wrong sha would
  # satisfy all of them. This job is the one place the value is checked against
  # the repository it claims to describe.
  provenance:
    name: the recorded revision must be a real ancestor
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          # Ancestry is unanswerable on the default shallow clone.
          fetch-depth: 0

      - name: component.rev must name an ancestor of this commit
        run: |

          rev=$(tr -d '[:space:]' < component.rev)

          if ! printf '%s' "$rev" | grep -qE '^[0-9a-f]{40}$'; then
            echo "component.rev is not a full lowercase commit sha: '$rev'"
            exit 1
          fi

          if ! git cat-file -e "${rev}^{commit}" 2>/dev/null; then
            echo "component.rev names $rev, which is not a commit in this repository."
            exit 1
          fi

          # Ancestry, not mere existence: a sha copied from an unrelated branch
          # would pass `cat-file`. It is an ancestor rather than HEAD itself
          # because a file cannot contain the hash of the commit containing it,
          # which is why GIT_REVISION documents itself as one commit behind.
          if ! git merge-base --is-ancestor "$rev" HEAD; then
            echo "component.rev names $rev, which is not an ancestor of HEAD."
            echo "It must be the commit the recorded component was built from."
            exit 1
          fi

          echo "component.rev names $rev, an ancestor of $(git rev-parse HEAD)"

  component:
    name: build, validate and reproduce the component
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: dtolnay/rust-toolchain@1.97.0
        with:
          targets: wasm32-unknown-unknown

      - name: Install wasm-tools
        run: cargo install wasm-tools --version 1.258.0 --locked

      - name: Build the component
        run: |

          cargo build --release --target wasm32-unknown-unknown \
            --no-default-features --features component --locked
          wasm-tools component new \
            target/wasm32-unknown-unknown/release/aethel_core.wasm \
            -o build1.component.wasm

      - name: The component must validate and match the declared world exactly
        run: |

          wasm-tools validate build1.component.wasm
          python3 scripts/check-component-exports.py \
            wit/aethel-core.wit build1.component.wasm

      # Everything above this point is structural: it builds, it validates, it
      # exposes the declared world. None of that is "you can instantiate it and
      # call it", which is the gap this step closes.
      - name: The component must execute and agree with the native API
        run: |

          cp build1.component.wasm aethel_core.component.wasm
          cargo test --features component-tests --test component_execution --locked

      - name: Two builds of the same commit must be byte-identical
        run: |

          rm -rf target/wasm32-unknown-unknown/release
          cargo build --release --target wasm32-unknown-unknown \
            --no-default-features --features component --locked
          wasm-tools component new \
            target/wasm32-unknown-unknown/release/aethel_core.wasm \
            -o build2.component.wasm
          if ! cmp -s build1.component.wasm build2.component.wasm; then
            echo "NOT REPRODUCIBLE: two builds of the same source differ"
            sha256sum build1.component.wasm build2.component.wasm
            exit 1
          fi
          echo "reproducible: $(sha256sum build1.component.wasm)"

      - name: The built hash must match the one checked into the repo
        run: |

          built=$(sha256sum build1.component.wasm | cut -d' ' -f1)
          declared=$(cut -d' ' -f1 component.sha256)
          if [ "$built" != "$declared" ]; then
            echo "hash mismatch"
            echo "  built:    $built"
            echo "  declared: $declared (component.sha256)"
            echo "If this change is intended, update component.sha256."
            exit 1
          fi
          echo "hash matches component.sha256: $built"