Skip to content

Commit a99bb0c

Browse files
committed
refactor(jwt)!: switch from HMAC-HS256 to RS256 asymmetric signing (ADR-T-007 Phase 3)
Replace the shared HMAC secret (`session_signing_key` / `email_verification_signing_key`) with an RSA-2048 key pair for all JWT operations. Key changes: - Sign tokens with `EncodingKey` (private) and verify with `DecodingKey` (public), both resolved once at startup from PEM files or inline PEM configuration. - Include a `kid` (Key ID) header derived from the SHA-256 hash of the public key, preparing for future key rotation. - Ship a development key pair at `share/default/jwt/` with a loud startup warning when it is used. - Remove `JwtSigningSecret`, the mandatory-option check for `auth.session_signing_key`, and all three serde aliases. - Verification (`verify`, `verify_email_token`) is now synchronous since keys are pre-loaded — no config lock needed per request. - Update all config files, container scripts, tests, and docs to use `private_key_path` / `public_key_path`. BREAKING CHANGE: The `auth.session_signing_key` and `auth.email_verification_signing_key` config fields are removed. Deployers must provide an RSA key pair via `auth.private_key_path` / `auth.public_key_path` (or the inline `_pem` variants). Existing HS256 tokens will be rejected after upgrade.
1 parent 2bfe60c commit a99bb0c

39 files changed

Lines changed: 370 additions & 248 deletions

.env.local

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
DATABASE_URL=sqlite://storage/database/data.db?mode=rwc
22
TORRUST_INDEX_CONFIG_TOML=
3-
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY=MaxVerstappenWC2021-session-key!
4-
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__EMAIL_VERIFICATION_SIGNING_KEY=MaxVerstappenWC2021-emailverify!
3+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH=./share/default/jwt/private.pem
4+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH=./share/default/jwt/public.pem
55
USER_ID=1000
66
TORRUST_TRACKER_CONFIG_TOML=
77
TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER=Sqlite3

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ serde_derive = "1"
8888
serde_json = "1"
8989
serde_with = "3"
9090
sha-1 = "0"
91+
sha2 = "0"
9192
sqlx = { version = "0", features = ["migrate", "mysql", "runtime-tokio-native-tls", "sqlite", "time"] }
9293
tera = { version = "1", default-features = false }
9394
text-colorizer = "1"

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,18 @@ TORRUST_INDEX_CONFIG_TOML=$(cat "./storage/index/etc/index.toml") cargo run
9595

9696
_For deployment, you __should__ override:
9797

98-
- The `tracker_api_token` and the JWT signing keys by using environmental variables:_
98+
- The `tracker_api_token` and RSA key paths by using environmental variables:_
9999

100100
```sh
101-
# Please use the secret that you generated for the torrust-tracker configuration.
101+
# Generate an RSA key pair for JWT signing:
102+
# openssl genrsa -out private.pem 2048
103+
# openssl rsa -in private.pem -pubout -out public.pem
104+
102105
# Override secrets in configuration using environmental variables
103106
TORRUST_INDEX_CONFIG_TOML=$(cat "./storage/index/etc/index.toml") \
104107
TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN=$(cat "./storage/tracker/lib/tracker_api_admin_token.secret") \
105-
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY="your-session-signing-secret-here!" \
106-
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__EMAIL_VERIFICATION_SIGNING_KEY="your-email-verify-secret-here!!" \
108+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH="/path/to/private.pem" \
109+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH="/path/to/public.pem" \
107110
cargo run
108111
```
109112

@@ -128,7 +131,7 @@ The following services are provided by the default configuration:
128131
- [ADR-T-004: Remove `located-error` Package](adr/004-remove-located-error.md) — Replace the `torrust-index-located-error` wrapper with `tracing` for error context.
129132
- [ADR-T-005: Migrate to Rust Edition 2024](adr/005-edition-2024.md) — Migrate the entire workspace to `edition = "2024"` and raise the MSRV to 1.85.
130133
- [ADR-T-006: Refactor the Error System](adr/006-error-system-refactor.md) — Replace the 41-variant `ServiceError` god enum with domain-scoped error enums (`AuthError`, `UserError`, `TorrentError`, `CategoryTagError`) and a thin `ApiError` wrapper.
131-
- [ADR-T-007: Refactor the JWT System](adr/007-jwt-system-refactor.md) — Centralise JWT handling into `src/jwt.rs`, redesign claims to RFC 7519, split into per-purpose signing keys, and enforce minimum secret length.
134+
- [ADR-T-007: Refactor the JWT System](adr/007-jwt-system-refactor.md) — Centralise JWT handling into `src/jwt.rs`, redesign claims to RFC 7519, and move to RS256 asymmetric signing with a public/private RSA key pair.
132135

133136
## Contributing
134137

adr/007-jwt-system-refactor.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-T-007: Refactor the JWT System
22

3-
**Status:** Phase 2 implemented
3+
**Status:** Phase 3 implemented
44
**Date:** 2026-04-14
55

66
## Context
@@ -388,21 +388,25 @@ phased rollout that subsumes Options A and B.
388388
- **Breaking change:** existing HS256 tokens are invalidated;
389389
users must re-login.
390390

391-
#### Phase 3 — RS256 Asymmetric Signing (Option C scope)
391+
#### Phase 3 — RS256 Asymmetric Signing (Option C scope) ✅ Implemented
392392

393-
- Replace `HS256` with `RS256` (`Algorithm::RS256`).
394-
- Config provides:
393+
- Replace `HS256` with `RS256` (`Algorithm::RS256`).
394+
- Config provides:
395395
- `auth.private_key_path` (PEM / PKCS#8) for signing.
396396
- `auth.public_key_path` for verification.
397-
- Alternatively, inline PEM via environment variable.
398-
- Generate a default development key pair on first run (with a
399-
loud warning) so the zero-config experience is preserved for
400-
local development.
401-
- Use `EncodingKey::from_rsa_pem` / `DecodingKey::from_rsa_pem`.
402-
- Only the signing service loads the private key; the
397+
- Alternatively, inline PEM via environment variable
398+
(`auth.private_key_pem`, `auth.public_key_pem`).
399+
- ✅ Development key pair shipped at `share/default/jwt/` with loud
400+
startup warning when the default dev keys are used.
401+
- Use `EncodingKey::from_rsa_pem` / `DecodingKey::from_rsa_pem`.
402+
- Only the signing service loads the private key; the
403403
verification path uses the public key.
404-
- Add a `kid` (Key ID) field to the JWT header to support future
405-
key rotation.
404+
- ✅ A `kid` (Key ID) is included in every JWT header (SHA-256
405+
fingerprint of the public key) to support future key rotation.
406+
- **Breaking change:** existing HS256 tokens and config
407+
(`session_signing_key`, `email_verification_signing_key`) are
408+
no longer supported. Deployers must generate an RSA key pair
409+
and update their configuration.
406410

407411
#### Future — Optional Revocation (Option E scope)
408412

compose.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ services:
1313
- TORRUST_INDEX_DATABASE=${TORRUST_INDEX_DATABASE:-e2e_testing_sqlite3}
1414
- TORRUST_INDEX_DATABASE_DRIVER=${TORRUST_INDEX_DATABASE_DRIVER:-sqlite3}
1515
- TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN=${TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN:-MyAccessToken}
16-
- TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY=${TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY:-MaxVerstappenWC2021-session-key!}
17-
- TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__EMAIL_VERIFICATION_SIGNING_KEY=${TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__EMAIL_VERIFICATION_SIGNING_KEY:-MaxVerstappenWC2021-emailverify!}
16+
- TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH=${TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH:-/var/lib/torrust/index/jwt/private.pem}
17+
- TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH=${TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH:-/var/lib/torrust/index/jwt/public.pem}
1818
networks:
1919
- server_side
2020
ports:

contrib/dev-tools/container/e2e/sqlite/mode/private/e2e-env-up.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ USER_ID=${USER_ID:-1000} \
88
TORRUST_INDEX_DATABASE="e2e_testing_sqlite3" \
99
TORRUST_INDEX_DATABASE_DRIVER="sqlite3" \
1010
TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \
11-
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY="MaxVerstappenWC2021-session-key!" \
11+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH="/var/lib/torrust/index/jwt/private.pem" \
12+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH="/var/lib/torrust/index/jwt/public.pem" \
1213
TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.private.e2e.container.sqlite3.toml) \
1314
TORRUST_TRACKER_DATABASE="e2e_testing_sqlite3" \
1415
TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER="sqlite3" \

contrib/dev-tools/container/e2e/sqlite/mode/public/e2e-env-up.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ USER_ID=${USER_ID:-1000} \
88
TORRUST_INDEX_DATABASE="e2e_testing_sqlite3" \
99
TORRUST_INDEX_DATABASE_DRIVER="sqlite3" \
1010
TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \
11-
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY="MaxVerstappenWC2021-session-key!" \
11+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH="/var/lib/torrust/index/jwt/private.pem" \
12+
TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH="/var/lib/torrust/index/jwt/public.pem" \
1213
TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.public.e2e.container.sqlite3.toml) \
1314
TORRUST_TRACKER_DATABASE="e2e_testing_sqlite3" \
1415
TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER="sqlite3" \

docs/containers.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,10 @@ The following environmental variables can be set:
149149

150150
- `TORRUST_INDEX_CONFIG_TOML_PATH` - The in-container path to the index configuration file, (default: `"/etc/torrust/index/index.toml"`).
151151
- `TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN` - Override of the admin token. If set, this value overrides any value set in the config.
152-
- `TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY` - Override of the auth session signing key. If set, this value overrides any value set in the config.
153-
- `TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__EMAIL_VERIFICATION_SIGNING_KEY` - Override of the auth email-verification signing key. If set, this value overrides any value set in the config.
152+
- `TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH` - Override of the RSA private key PEM file path for JWT signing. If set, this value overrides any value set in the config.
153+
- `TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH` - Override of the RSA public key PEM file path for JWT verification. If set, this value overrides any value set in the config.
154+
- `TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PEM` - Override with an inline RSA private key PEM string instead of a file path.
155+
- `TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PEM` - Override with an inline RSA public key PEM string instead of a file path.
154156
- `TORRUST_INDEX_DATABASE_DRIVER` - The database type used for the container, (options: `sqlite3`, `mysql`, default `sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file.
155157
- `TORRUST_INDEX_CONFIG_TOML` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_INDEX_CONFIG_TOML=$(cat index-index.toml)`).
156158
- `USER_ID` - The user id for the runtime crated `torrust` user. Please Note: This user id should match the ownership of the host-mapped volumes, (default `1000`).
@@ -203,7 +205,8 @@ mkdir -p ./storage/index/lib/ ./storage/index/log/ ./storage/index/etc/
203205
## Run Torrust Index Container Image
204206
docker run -it \
205207
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MySecretToken" \
206-
--env TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__SESSION_SIGNING_KEY="MaxVerstappenWC2021-session-key!" \
208+
--env TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH="/var/lib/torrust/index/jwt/private.pem" \
209+
--env TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH="/var/lib/torrust/index/jwt/public.pem" \
207210
--env USER_ID="$(id -u)" \
208211
--publish 0.0.0.0:3001:3001/tcp \
209212
--volume ./storage/index/lib:/var/lib/torrust/index:Z \

share/default/config/index.container.mysql.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ threshold = "info"
1515
token = "MyAccessToken"
1616

1717
[auth]
18-
session_signing_key = "MaxVerstappenWC2021-session-key!"
19-
email_verification_signing_key = "MaxVerstappenWC2021-emailverify!"
18+
private_key_path = "/var/lib/torrust/index/jwt/private.pem"
19+
public_key_path = "/var/lib/torrust/index/jwt/public.pem"
2020

2121
[database]
2222
connect_url = "mysql://root:root_secret_password@mysql:3306/torrust_index"

0 commit comments

Comments
 (0)