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: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    name: build & test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo build --all-targets
      - run: cargo test

  # Zero warnings, kept at zero. Default features plus `component` (the WIT
  # world's adapter, which ships). `puf` is research code outside the charter
  # (R-04 in docs/DEVIATIONS.md) and is left out on purpose.
  lint:
    name: clippy + rustdoc (-D warnings)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - name: cargo clippy --all-targets -- -D warnings
        run: cargo clippy --all-targets --locked -- -D warnings
      - name: cargo clippy --lib --features component -- -D warnings
        run: cargo clippy --lib --locked --features component -- -D warnings
      - name: cargo doc --no-deps (-D warnings)
        run: cargo doc --no-deps --locked
        env:
          RUSTDOCFLAGS: -D warnings

  # Declared rust-version in Cargo.toml, pinned and actually built rather than only stated.
  # 1.75 was the original declared MSRV but was never tested and does not build: the locked
  # zeroize_derive already requires the `edition2024` Cargo feature, which needs rustc 1.85+.
  # 1.85 also matches the MSRV declared by pqc-kem/pqc-sig, so this keeps all three repos in
  # the Identity Release initiative on the same floor.
  msrv:
    name: MSRV (1.85) build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@1.85.0
        with:
          targets: wasm32-unknown-unknown
      # MSRV is a promise about what a *consumer* compiles: the library and the
      # dependencies it pulls in. It is not a promise about our own test tooling.
      # `--all-targets` pulls dev-dependencies, and wasmtime, used only by the
      # execution proof and never shipped, requires rustc 1.95. Building it here
      # would raise the floor for every consumer in order to satisfy a test.
      #
      # The library is built in both configurations a consumer can select, so
      # the floor is still checked against everything that actually ships.
      - run: cargo build --locked
      # The component is what actually ships as L1, so the floor is checked
      # against it too. `--no-default-features` is only meaningful for wasm32:
      # on a host target a no_std build has no allocator or panic handler and
      # cannot link, which is why it was never built that way.
      - run: cargo build --locked --no-default-features --features component --target wasm32-unknown-unknown

  # P3-02: proves identity generation works fully offline by denying the
  # capability at the boundary (a real network namespace with no interface),
  # not by an in-process "am I offline?" assertion. An in-process check can
  # only observe the paths it knows to instrument and would pass even if a
  # dependency three levels down still had a route out.
  #
  # `sudo unshare --net` puts the step's process tree in a fresh network
  # namespace with no configured interface (loopback only, and unused here)
  # — equivalent to the `--network none` a container runtime would give you,
  # without needing to solve "how does an offline container fetch crates".
  # GitHub-hosted ubuntu-latest runners have passwordless sudo, so this needs
  # no extra setup.
  #
  # NOTE: an earlier version of this job used the unprivileged
  # `unshare --net --map-root-user` (no sudo) specifically to avoid depending
  # on passwordless sudo, and that was verified working in WSL2/Ubuntu. It
  # does NOT work on GitHub-hosted runners: Ubuntu 23.10+ restricts
  # unprivileged user-namespace creation via AppArmor
  # (kernel.apparmor_restrict_unprivileged_userns=1), so the job died with
  # "unshare: write failed /proc/self/uid_map: Operation not permitted"
  # before either isolated step ran at all — the first real run of this
  # workflow produced zero evidence in either direction. `sudo unshare --net`
  # sidesteps the user-namespace restriction entirely (root doesn't need
  # CAP_SYS_ADMIN granted via userns) and is what's actually confirmed
  # working on ubuntu-latest.
  #
  # Dependencies and test binaries are built in the earlier, networked step;
  # the isolated steps only execute already-built code with `--offline`, so
  # cargo itself never needs a route either.
  offline-generation:
    name: offline generation (network-isolated)
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable

      - name: Prefetch deps and build test binaries (network available here)
        run: |

          cargo build --all-targets
          cargo test --no-run --lib --test plp_tests
          cargo test --no-run --test network_isolation_negative_control

      # rustup's directories are named explicitly. sudo resets HOME to /root,
      # and once the runner stopped exporting RUSTUP_HOME, rustup under sudo
      # looked in /root/.rustup, found no toolchain, and exited before cargo
      # ran. That failed the generation step, and it let the negative control
      # below "fail as expected" without testing anything. The negative control
      # now also requires that its test actually ran and failed.
      - name: Generation suite MUST succeed with no network interface available
        run: |

          sudo -E env "PATH=$PATH" "RUSTUP_HOME=${RUSTUP_HOME:-$HOME/.rustup}" "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" \
            unshare --net -- \
            cargo test --offline --lib --test plp_tests

      - name: Negative control MUST fail with no network interface available
        run: |

          if sudo -E env "PATH=$PATH" "RUSTUP_HOME=${RUSTUP_HOME:-$HOME/.rustup}" "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" \
               unshare --net -- \
               cargo test --offline --test network_isolation_negative_control > negative-control.log 2>&1; then
            cat negative-control.log
            echo "::error::negative-control network call succeeded inside the network-isolated step — isolation is NOT enforced, the offline-generation claim is unproven"
            exit 1
          fi
          cat negative-control.log
          # A failure only counts if the test ran and failed. Anything earlier
          # (no toolchain, a build error) says nothing about isolation.
          grep -q "^test network_call_succeeds_when_a_route_exists \.\.\. FAILED" negative-control.log || {
            echo "::error::the negative control did not run its test, so its failure is not evidence of isolation"
            exit 1
          }
          echo "Negative control failed as expected under network isolation — isolation confirmed real."

  # P3-08 follow-up: zeroize_wipes_master_identity_secret_wasm (src/plp.rs)
  # proves MasterIdentity's secret is actually zeroized in WASM linear
  # memory, not just on native — but a test nobody runs isn't proof (same
  # failure shape as the earlier `unshare` incident on this repo, where the
  # isolation job silently didn't execute what it claimed to). This job runs
  # it for real, under wasm32-unknown-unknown via wasm-pack + Node.
  #
  # The `wasm` feature it used to build no longer exists: P3-13 (0X3-81)
  # retired the wasm-bindgen export surface so there is one WebAssembly
  # artifact, the component. `wasm-bindgen-test` survives as a TEST HARNESS,
  # which is a different thing from those exports — the test calls the native
  # Rust API and asserts the secret is wiped, and running it under wasm32 is
  # what shows the wasm codegen path does not elide that write. So this job
  # keeps doing its job, on the default feature set.
  wasm-test:
    name: wasm test (zeroization + full suite, node)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
      - name: Install wasm-pack
        run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
      - name: Run wasm test suite
        run: wasm-pack test --node -- --no-default-features
      # A passing run above is not by itself proof the zeroization test ran:
      # if it were filtered out or silently skipped, the job would still be
      # green. Name it explicitly and require it.
      - name: The zeroization test specifically must have run in WASM
        run: |

          out=$(wasm-pack test --node -- --no-default-features 2>&1)
          echo "$out"
          echo "$out" | grep -q "zeroize_wipes_master_identity_secret_wasm" || {
            echo "::error::zeroize_wipes_master_identity_secret_wasm did not run under wasm32 - the zeroization-in-linear-memory claim is unproven"
            exit 1
          }