Files
admin/_docs/02_tasks/todo/AZ-532_asymmetric_signing_jwks.md
T
Oleksandr Bezdieniezhnykh 3a925b9b0f
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status
refactor: remove obsolete resource download and installer endpoints
- Deleted the `POST /resources/get/{dataFolder?}` and `GET /resources/get-installer` endpoints as part of the architectural shift towards simplified resource management.
- Removed associated methods and configurations, including `ResourcesService.GetEncryptedResource`, `ResourcesService.GetInstaller`, and related properties in `ResourcesConfig`.
- Cleaned up environment variables and configuration files to reflect the removal of installer-related settings.
- Eliminated the `GetResourceRequest` DTO and its validator, along with the `WrongResourceName` error code.
- Updated documentation to clarify the changes in resource handling and the retirement of per-user file encryption.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 04:17:55 +03:00

4.8 KiB

Asymmetric Signing (RS256/ES256) + JWKS Endpoint

Task: AZ-532_asymmetric_signing_jwks Name: Asymmetric signing (RS256/ES256) + JWKS endpoint Description: Switch admin's JWT signing from shared-secret HS256 to ES256 (preferred) so verifiers hold only public keys. Expose a standard GET /.well-known/jwks.json. Verifiers can no longer mint tokens even if compromised; new verifiers can be added without secret distribution. Complexity: 5 points Dependencies: None (independent of AZ-531; can land before or after) Component: Admin API + Services Tracker: AZ-532 Epic: AZ-529

Problem

Access tokens are signed with HS256 using a shared symmetric secret (JWT_SECRET). Every verifier (satellite-provider today, gps-denied + ui tomorrow) holds material that can mint valid admin tokens — a breach of any one verifier compromises the whole auth domain. Adding a new verifier requires distributing the secret out-of-band.

Outcome

  • Admin signs access tokens with a private key (ES256 preferred for small signatures + speed; RS256 acceptable). Public key lives nowhere outside the JWKS endpoint.
  • GET /.well-known/jwks.json returns the active public key set with kid per key. Cache headers: Cache-Control: public, max-age=3600 (verifiers cache, refresh hourly).
  • Tokens carry kid in the header so verifiers select the right key during rotation overlap.
  • Key material lives in admin's secrets dir (secrets/jwt_signing_key.pem) — NOT in env vars.
  • Documented rotation procedure: generate new key → add to JWKS as second entry → wait verifier-cache TTL → switch signing to new kid → wait until all old-kid tokens expire → remove old from JWKS.

Scope

Included

  • ES256 keypair generation script in scripts/ (one-time setup + rotation tool).
  • IJwtSigningKeyProvider interface + file-backed impl loading from secrets/.
  • Update AuthService.CreateToken to use asymmetric signing.
  • New GET /.well-known/jwks.json minimal-API handler (anonymous, cacheable, .AllowAnonymous()).
  • Update appsettings.json / .env.example to drop JWT_SECRET (keep temporarily as fallback for one release for rollback safety).
  • Tests: round-trip sign/verify, JWKS payload shape, kid header presence, alg-confusion attack rejection.

Excluded

  • Verifier-side migration in satellite-provider / gps-denied / ui (filed under those workspaces once admin ships).
  • Hardware HSM / KMS integration (file-backed PEM is sufficient for now; HSM is a future ticket).
  • Mission-token specific signing path (handled in AZ-533; uses same key).

Acceptance Criteria

AC-1: Admin signs with ES256 Given admin is configured with an ES256 keypair When POST /login succeeds Then the returned access token's header has alg=ES256 and kid matching the active key.

AC-2: JWKS endpoint serves the public key Given a fresh admin instance When GET /.well-known/jwks.json is called (no auth) Then response is 200 with body { "keys": [ { "kty":"EC", "crv":"P-256", "kid":"...", "x":"...", "y":"...", "alg":"ES256", "use":"sig" } ] }. Cache-Control: public, max-age=3600.

AC-3: Two-key overlap during rotation Given two valid signing keys are configured (kid-A active, kid-B inactive but kept) When JWKS is fetched Then both keys appear; tokens signed with kid-A still verify; switching active to kid-B starts producing kid-B tokens; both verify until kid-A is removed.

AC-4: Private key never leaves admin Given the JWKS endpoint When response is inspected Then no d field (private scalar for EC) or p/q (RSA private primes) appears. Only public components.

AC-5: alg-confusion attack rejected Given a forged token with alg=HS256 and signature computed with the public key as the HMAC secret When presented to a verifier configured for ES256 Then verification fails. (Pin expected algorithm explicitly in TokenValidationParameters.ValidAlgorithms.)

Blackbox Tests

AC Ref Initial Data/Conditions What to Test Expected Behavior NFR References
AC-1 ES256 key configured POST /login → decode header alg=ES256, kid present
AC-2 Fresh admin GET /.well-known/jwks.json 200, JWKS shape, max-age=3600
AC-3 Two keys configured GET JWKS twice across rotation Both keys present in overlap
AC-4 JWKS response Inspect for private fields No d/p/q present
AC-5 Forged HS256-as-ES256-pubkey token POST any protected endpoint 401

Risks / Notes

  • HS256 → ES256 is a breaking change for verifiers. Coordinate the cutover: admin keeps signing HS256 in parallel for one release while verifiers add ES256 verification, then admin flips to ES256-only.
  • Document the cutover in _docs/02_document/architecture.md (suite-level).