Skip to content

feat(licence): verify activation JWT ES256 signature before trusting payload#30

Draft
revelri wants to merge 2 commits into
mainfrom
revoy/str-licence-es256-verify
Draft

feat(licence): verify activation JWT ES256 signature before trusting payload#30
revelri wants to merge 2 commits into
mainfrom
revoy/str-licence-es256-verify

Conversation

@revelri

@revelri revelri commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

What

Adds client-side ES256 JWT signature verification to the licence activation path.

  • New src/licence/verify.rs: verify_es256 (p256 ECDSA + JOSE r||s signature decoding), embedded P-256 verifying-key constant (ACTIVATION_VERIFYING_KEY_B64, currently PLACEHOLDER), runtime override via STRIVO_LICENCE_PUBKEY. When the key is absent/PLACEHOLDER, verification is skipped with a WARN so dev/local-trial flows still work.
  • Wired into routes::licence so every backend token is checked before being written to the cache.
  • New deps: p256 0.13 (ecdsa), base64 0.22; Cargo.lock regenerated accordingly.

Addresses the ROADMAP HIGH item: "Verify Licence JWT ES256 signature (TODO(licence-verify)) before any Creator Edition commercial launch."

Verification

  • cargo test -p strivo-core --lib licence -> 12 tests pass, 0 failed, including accepts_valid_token, rejects_tampered_payload, rejects_signature_from_different_key, placeholder_key_skips_verification_and_returns_claims (tests generate an ephemeral P-256 keypair and sign a real JWT).
  • cargo build -p strivo-web -> clean (licence route wiring compiles).
  • Rebased clean onto current origin/main (resolved a sha2 version + new-dep manifest conflict, keeping origin's sha2 = 0.11.0); diff is 7 intended files; Cargo.lock carries the new p256/base64 entries.

Draft: review surface for the auditor; CI gates the merge.

…payload

Add `licence::verify::verify_es256` (p256 + JOSE r||s decoding) and wire
it into `routes::licence::persist_and_reply` so every backend token is
checked before being written to the cache.

The production P-256 public key is held in the `ACTIVATION_VERIFYING_KEY_B64`
constant in `src/licence/verify.rs` (currently "PLACEHOLDER") and can be
overridden at runtime via `STRIVO_LICENCE_PUBKEY`. When the key is absent or
equals "PLACEHOLDER", verification is skipped with a WARN log so dev and
local-trial flows remain functional.

Unit tests generate a fresh ephemeral P-256 keypair, sign a real JWT, and
assert accept on a valid token, reject on a tampered payload, and reject on a
signature from a different key.

New deps: p256 0.13 (ecdsa feature), base64 0.22 (already workspace-present
in strivo-web; now also a direct dep of strivo-core).
@revelri
revelri force-pushed the revoy/str-licence-es256-verify branch from d7b763a to 85fe12e Compare July 4, 2026 05:22
@revelri

revelri commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

The ES256 verification itself is sound (real p256/ECDSA verification, correct JOSE r||s handling, SPKI DER key parsing). Holding merge on two blockers:

  1. CI cargo fmt gate fails on src/licence/verify.rs and a few test helper signatures. cargo fmt --all + commit fixes it — no logic change.
  2. Test env-mutation race. The verify tests mutate the STRIVO_LICENCE_PUBKEY process-global env var without mutual exclusion. cargo test runs multi-threaded by default (the "sequential by default" comment is incorrect), so a parallel run reproduces a spurious verification failure in placeholder_key_skips_verification_and_returns_claims. Fix with serial_test::serial on the four verify tests, a Mutex guard, or by factoring a verify_es256_with_key fn that avoids env reads in tests.

Follow-on (not blocking this PR): the verified claim's tier is currently discarded in favour of the request-body tier — worth using the verified value in a later change.

Split key resolution out of the crypto: verify_es256_with_key(token, key_b64)
holds the verification body and takes the verifying key as a parameter;
verify_es256 stays a thin wrapper that resolves STRIVO_LICENCE_PUBKEY env ->
ACTIVATION_VERIFYING_KEY_B64 const (unchanged precedence/skip/warn semantics).

Tests now pass the key directly instead of mutating the process-global
STRIVO_LICENCE_PUBKEY env var, making them parallel-safe by construction under
cargo's default multi-threaded runner. Drops the with_key helper and every
set_var/remove_var, and corrects the wrong 'sequential by default' comment.
Also applies rustfmt to the file.
@revelri

revelri commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Addresses the two REVISION blockers on this PR (loop pass, independently verified):

  • rustfmt gatecargo fmt --all --check now exits 0 (was failing on verify.rs).
  • test env-race (root cause, not a serial band-aid) — factored the crypto core out into verify_es256_with_key(token, key_b64); verify_es256 stays a thin env→const key resolver that delegates. Tests now pass the verifying key directly and no longer mutate the process-global STRIVO_LICENCE_PUBKEY (grep -c set_var/remove_var → 0), so the race is eliminated by construction. verify_es256's runtime contract is unchanged.

Verification: cargo fmt --all --check exit 0; cargo test --lib licence::verify 4/4; 5× --test-threads=16 all green; cargo clippy --lib clean on verify.rs. Diff is src/licence/verify.rs only — no Cargo.toml/lock, no new deps.

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