Skip to content

fix(auth): match the Bearer scheme case-insensitively (RFC 6750)#929

Merged
ruvnet merged 1 commit into
mainfrom
fix/bearer-scheme-case-insensitive
Jun 3, 2026
Merged

fix(auth): match the Bearer scheme case-insensitively (RFC 6750)#929
ruvnet merged 1 commit into
mainfrom
fix/bearer-scheme-case-insensitive

Conversation

@ruvnet
Copy link
Copy Markdown
Owner

@ruvnet ruvnet commented Jun 3, 2026

What

require_bearer parsed the Authorization header with strip_prefix("Bearer ")case-sensitive. Per RFC 6750 §2.1 / RFC 7235 §2.1 the auth-scheme is case-insensitive, so a correct token sent as Authorization: bearer <token> (or BEARER, or with extra whitespace) was rejected with a confusing invalid bearer token 401.

That's needless friction in the active auth-setup theme (#864 secure-by-default, #924 ngrok demo) — someone configuring RUVIEW_API_TOKEN whose client lowercases the scheme would see a 401 with a correct token.

How

Match the scheme with eq_ignore_ascii_case("Bearer") and trim leading token whitespace. The token comparison is unchanged — still exact and constant-time (ct_eq). So this does not weaken auth:

bearer  s3cr3t  → 200   (case-insensitive scheme, extra space)
BEARER  s3cr3t  → 200
bearer  nope    → 401   (wrong token still rejected)
Basic   s3cr3t  → 401   (non-Bearer scheme still rejected)

Tests

New accepts_case_insensitive_bearer_scheme asserts all of the above. Full bearer_auth suite: 9 passed.

Audited the surrounding auth code while here — ct_eq correctly rejects length mismatch (no prefix-auth bypass) and is constant-time; the middleware fails closed. No security bug; this is purely an RFC-compliance/robustness fix.

🤖 Generated with claude-flow

`require_bearer` parsed the Authorization header with
`strip_prefix("Bearer ")`, which is case-sensitive. Per RFC 6750 §2.1 /
RFC 7235 §2.1 the auth-scheme is case-insensitive, so a correct token sent
as `Authorization: bearer <token>` (or `BEARER`, or with extra whitespace)
was rejected with a confusing "invalid bearer token" 401 — needless friction
when setting up `RUVIEW_API_TOKEN` (the active #864/#924 theme).

Now the scheme is matched with `eq_ignore_ascii_case` and leading token
whitespace trimmed. The token comparison itself is unchanged — still exact
and constant-time (`ct_eq`) — so this does not weaken auth: a wrong token or
a non-Bearer scheme (`Basic …`) still returns 401.

New test `accepts_case_insensitive_bearer_scheme` covers `bearer`/`BEARER`/
extra-space (accept) and wrong-token/`Basic` (still reject). bearer_auth
suite: 9 passed.

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet ruvnet merged commit be48143 into main Jun 3, 2026
27 of 28 checks passed
@ruvnet ruvnet deleted the fix/bearer-scheme-case-insensitive branch June 3, 2026 09:07
ruvnet added a commit that referenced this pull request Jun 3, 2026
… action (#930)

Two real problems in the Static Application Security Testing job:

1. **It scanned a path that no longer exists.** `bandit -r src/` and
   `semgrep … src/` pointed at the repo-root `src/`, but the Python code
   moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten
   in Rust. So the SAST scan matched nothing — a silent no-op (this is also
   why `bandit-results.sarif` was "Path does not exist" on recent runs).
   Fixed both to `archive/v1/src/`.

2. **Deprecated + redundant + flaky semgrep step.** The
   `returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1`
   from Docker Hub every run (intermittently timing out → red check, e.g. on
   #929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what
   feeds GitHub Security; the action only pushed to the Semgrep cloud app via
   SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes`
   rulesets into the pip semgrep command, so coverage is preserved with no
   Docker pull.

The job stays `continue-on-error: true` (non-gating). YAML validated.
ruvnet added a commit that referenced this pull request Jun 3, 2026
Per the CLAUDE.md pre-merge checklist (item 5, "Add entry under
[Unreleased]"), several recently-merged PRs landed without CHANGELOG
entries. Backfilling the user/operator-facing ones — most importantly the
MAT triage safety fix:

- #926 (Security/safety): survivor with a heartbeat never triaged Deceased
- #918: per-node HA devices report each node's own presence/motion
- #919: actionable --model load diagnostic (refs #894)
- #920: --export-rvf no longer silently produces a placeholder model
- #929 (Security): bearer scheme matched case-insensitively (RFC 6750)

CI-internal fixes (#925 rust-cache, #930 SAST) are intentionally omitted —
they don't change product behavior. Docs-only.
ruvnet added a commit that referenced this pull request Jun 3, 2026
* fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action

Two real problems in the Static Application Security Testing job:

1. **It scanned a path that no longer exists.** `bandit -r src/` and
   `semgrep … src/` pointed at the repo-root `src/`, but the Python code
   moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten
   in Rust. So the SAST scan matched nothing — a silent no-op (this is also
   why `bandit-results.sarif` was "Path does not exist" on recent runs).
   Fixed both to `archive/v1/src/`.

2. **Deprecated + redundant + flaky semgrep step.** The
   `returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1`
   from Docker Hub every run (intermittently timing out → red check, e.g. on
   #929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what
   feeds GitHub Security; the action only pushed to the Semgrep cloud app via
   SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes`
   rulesets into the pip semgrep command, so coverage is preserved with no
   Docker pull.

The job stays `continue-on-error: true` (non-gating). YAML validated.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(protocol): resolve 0xC511_0004 magic collision (closes #928)

Background

`0xC511_0004` was assigned to two different packet formats in firmware
— `EDGE_FUSED_MAGIC` (ADR-063, 48-byte `edge_fused_vitals_pkt_t`) and
`WASM_OUTPUT_MAGIC` (ADR-040, variable-length `wasm_output_pkt_t`).
Both were transmitted. The sensing-server only had a WASM parser for
that magic and no fused-vitals parser, so on the ESP32-C6 + MR60BHA2
mmWave configuration the fused-vitals packet was silently misparsed
as a malformed WASM output — `breathing_rate` was read as
`event_count`, mmWave-fused vitals were lost, and spurious WASM events
were emitted to subscribers.

Fix

1. Reassign `WASM_OUTPUT_MAGIC` to `0xC511_0007` (next free slot per
   the registry in `rv_feature_state.h`). Smaller blast radius than
   moving fused-vitals — the registry already treats `0xC511_0004` as
   fused-vitals canonical and several years of deployed feature
   tracking depends on that assignment.

2. Add `parse_edge_fused_vitals` + `EdgeFusedVitalsPacket` in
   `wifi-densepose-sensing-server::main`. Byte layout taken directly
   from `edge_processing.h:129`, mirroring the firmware's
   `_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48)` so future
   firmware changes that grow the packet will break this parser
   loudly instead of silently.

3. Add a dispatch arm in the UDP receive loop. Fused-vitals is tried
   BEFORE WASM so a stale firmware (still emitting 0xC511_0004 with
   the WASM payload) fails to parse as fused-vitals (size mismatch),
   then fails to parse as WASM (magic mismatch on the new 0x...0007),
   and gets dropped — a deliberate "fail loud" outcome rather than the
   pre-fix silent garbage.

4. Update the registry comment in `rv_feature_state.h` to add the new
   0x...0007 row.

5. Add five tests in a new `issue_928_magic_collision_tests` mod:
   - `parse_edge_fused_vitals_extracts_fields_correctly`
   - `parse_edge_fused_vitals_rejects_short_buffer`
   - `parse_edge_fused_vitals_rejects_wrong_magic`
   - `parse_wasm_output_rejects_legacy_0004_magic`
   - `parse_wasm_output_accepts_new_0007_magic`

WebSocket payload

Fused-vitals now broadcasts as `{"type": "edge_fused_vitals", ...}`
with the mmWave-specific block nested under `mmwave`. Schema is
additive — existing subscribers that only inspect `type` are
unaffected; subscribers that switch on `type` gain a new branch.

Deployment note

This is a wire-protocol change. Firmware older than this commit that
emits WASM output on 0xC511_0004 will lose its WASM event stream
against an updated host (host expects 0xC511_0007). Per the issue
discussion, "fail loud" is preferred to silent misparsing. Operators
running C6+mmWave should reflash firmware concurrent with the host
upgrade.

Test results
  cargo test -p wifi-densepose-sensing-server --no-default-features
  --bin sensing-server
  → 122 passed / 0 failed (5 new + 117 existing, unchanged)

Co-Authored-By: claude-flow <ruv@ruv.net>
github-actions Bot pushed a commit to TheArkhitect/RuView that referenced this pull request Jun 3, 2026
…loses ruvnet#928) (ruvnet#931)

* fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action

Two real problems in the Static Application Security Testing job:

1. **It scanned a path that no longer exists.** `bandit -r src/` and
   `semgrep … src/` pointed at the repo-root `src/`, but the Python code
   moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten
   in Rust. So the SAST scan matched nothing — a silent no-op (this is also
   why `bandit-results.sarif` was "Path does not exist" on recent runs).
   Fixed both to `archive/v1/src/`.

2. **Deprecated + redundant + flaky semgrep step.** The
   `returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1`
   from Docker Hub every run (intermittently timing out → red check, e.g. on
   ruvnet#929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what
   feeds GitHub Security; the action only pushed to the Semgrep cloud app via
   SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes`
   rulesets into the pip semgrep command, so coverage is preserved with no
   Docker pull.

The job stays `continue-on-error: true` (non-gating). YAML validated.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(protocol): resolve 0xC511_0004 magic collision (closes ruvnet#928)

Background

`0xC511_0004` was assigned to two different packet formats in firmware
— `EDGE_FUSED_MAGIC` (ADR-063, 48-byte `edge_fused_vitals_pkt_t`) and
`WASM_OUTPUT_MAGIC` (ADR-040, variable-length `wasm_output_pkt_t`).
Both were transmitted. The sensing-server only had a WASM parser for
that magic and no fused-vitals parser, so on the ESP32-C6 + MR60BHA2
mmWave configuration the fused-vitals packet was silently misparsed
as a malformed WASM output — `breathing_rate` was read as
`event_count`, mmWave-fused vitals were lost, and spurious WASM events
were emitted to subscribers.

Fix

1. Reassign `WASM_OUTPUT_MAGIC` to `0xC511_0007` (next free slot per
   the registry in `rv_feature_state.h`). Smaller blast radius than
   moving fused-vitals — the registry already treats `0xC511_0004` as
   fused-vitals canonical and several years of deployed feature
   tracking depends on that assignment.

2. Add `parse_edge_fused_vitals` + `EdgeFusedVitalsPacket` in
   `wifi-densepose-sensing-server::main`. Byte layout taken directly
   from `edge_processing.h:129`, mirroring the firmware's
   `_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48)` so future
   firmware changes that grow the packet will break this parser
   loudly instead of silently.

3. Add a dispatch arm in the UDP receive loop. Fused-vitals is tried
   BEFORE WASM so a stale firmware (still emitting 0xC511_0004 with
   the WASM payload) fails to parse as fused-vitals (size mismatch),
   then fails to parse as WASM (magic mismatch on the new 0x...0007),
   and gets dropped — a deliberate "fail loud" outcome rather than the
   pre-fix silent garbage.

4. Update the registry comment in `rv_feature_state.h` to add the new
   0x...0007 row.

5. Add five tests in a new `issue_928_magic_collision_tests` mod:
   - `parse_edge_fused_vitals_extracts_fields_correctly`
   - `parse_edge_fused_vitals_rejects_short_buffer`
   - `parse_edge_fused_vitals_rejects_wrong_magic`
   - `parse_wasm_output_rejects_legacy_0004_magic`
   - `parse_wasm_output_accepts_new_0007_magic`

WebSocket payload

Fused-vitals now broadcasts as `{"type": "edge_fused_vitals", ...}`
with the mmWave-specific block nested under `mmwave`. Schema is
additive — existing subscribers that only inspect `type` are
unaffected; subscribers that switch on `type` gain a new branch.

Deployment note

This is a wire-protocol change. Firmware older than this commit that
emits WASM output on 0xC511_0004 will lose its WASM event stream
against an updated host (host expects 0xC511_0007). Per the issue
discussion, "fail loud" is preferred to silent misparsing. Operators
running C6+mmWave should reflash firmware concurrent with the host
upgrade.

Test results
  cargo test -p wifi-densepose-sensing-server --no-default-features
  --bin sensing-server
  → 122 passed / 0 failed (5 new + 117 existing, unchanged)

Co-Authored-By: claude-flow <ruv@ruv.net> 2c136ac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant