Aller au contenu

ADR-0013 — JWT asymétrique (RS256), kid + JWKS et rotation des clés de signature

Date : 2026-06-14 Catégorie : A (convention de sécurité transverse — réalise le palier 2 d'ADR-0011) Décideur : Lead architecte DYNORS Statut : Proposée (à valider avant bascule des cercles) Réalise : ADR-0011 §3 « Reste à faire — palier 2 : signature asymétrique RS256/EdDSA (dynors-auth JWKS), rotation ». Active le slot Codex existant TAIL_AUTH_KEY_MODE=RS256_ASYMMETRIC. S'appuie sur : ADR-0008 (isolement de secret par app — analogie), IDENTITE_APPLICATIVE_3_LAYERS.md (audience = tech code).


Contexte

ADR-0011 a rendu l'aud obligatoire mais a laissé la signature en HS256 symétrique : le secret ${JWT_SECRET} est partagé entre l'émetteur et tous les validateurs. Conséquence (écart A1 de l'audit 2026-06-14, docs/security/AUDIT_SECURITE_VAULT_SIRRAT_2026-06-14.md) : tout backend qui valide peut aussi forger un token pour n'importe quel realm/rôle. L'objectif « dynors-auth seul émetteur » n'est donc pas cryptographiquement tenu — seulement par convention.

Le Codex SIRRAT a déjà anticipé la cible (constantes présentes, SIRRAT_CODEX_REFERENCE.md) :

Constante Codex Rôle
TAIL_AUTH_KEY_MODE HS256_SHARED (transitoire) | RS256_ASYMMETRIC (cible)
TAIL_AUTH_JWKS_URL URL JWKS de dynors-auth (mode RS256)
TAIL_AUTH_JWT_ISSUER / TAIL_AUTH_AUDIENCE iss / aud attendus
TAIL_AUTH_JWT_SECRET (secret_refprotected:auth.jwt.secret) secret HS256 (mode transitoire)
TAIL_AUTH_JWT_EXPIRY_S / _REFRESH_EXPIRY_S / _REFRESH_ENABLED TTL + rotation refresh

Le point d'attention demandé : la gestion des constantes de cryptage doit être prévue en aval — c.-à-d. couvrir la rotation des clés (sans coupure) et rester un seul dictionnaire réutilisable par les futurs consommateurs (entitlements, webhooks signés…).

Décision

1. Algorithme cible : RS256 (asymétrique), émetteur unique

  • dynors-auth signe avec une clé privée ; les apps valident avec la clé publique. Un validateur ne détient aucun secret de forge → A1 fermé.
  • RS256 retenu plutôt qu'EdDSA pour la compatibilité JWKS/outillage (validation éventuelle par des partenaires B2B, librairies, introspection). EdDSA reste une option future (clés plus courtes) si le périmètre reste 100 % interne — non bloquant, l'allowlist (§4) autorise les deux.
  • Le transit service-à-service garde le modèle HKDF par app (ADR-0008) — inchangé. Deux plans de confiance distincts : identité utilisateur = asymétrique/JWKS ; machine-à-machine = HKDF symétrique.

2. kid + JWKS (le « en aval » : rotation sans coupure)

  • Chaque JWT porte un kid (key id) dans son en-tête.
  • Les validateurs résolvent la clé publique par kid depuis un JWKS :
  • cible : TAIL_AUTH_JWKS_URL (endpoint dynors-auth, cache TTL) ;
  • mode hors-ligne / dégradé : un ou plusieurs PEM publics injectés en config (public-keys), indexés par kid.
  • Le JWKS expose plusieurs clés simultanément → la rotation se fait par recouvrement, exactement comme le mode DUAL du transit SLY (ADR-0008) :
  • publier la nouvelle clé (nouveau kid) dans le JWKS, l'émetteur signe encore avec l'ancienne ;
  • basculer l'émetteur sur le nouveau kid ;
  • retirer l'ancienne clé du JWKS après expiration des derniers tokens (TTL access).

3. Convention Vault versionnée (matériel secret)

vault://dynors/{cercle}/platform/auth#jwt-private-{kid}      # clé privée par kid (émetteur)
vault://dynors/{cercle}/platform/auth#jwt-current-kid        # pointeur kid courant
  • Clé privée : Vault uniquement, lecture par dynors-auth seul (policy dédiée).
  • Clé publique / kid / JWKS : non secrets (diffusables) — config Codex, pas Vault.
  • Réutilisable tel quel par tout futur signataire (entitlements, webhooks) : vault://dynors/{cercle}/{scope}/{categorie}#{champ}-{kid}.

4. Allowlist d'algorithmes (contrat unique, dynors-security)

  • dynors-security impose une liste blanche : RS256 (+ EdDSA autorisé), HS256 toléré uniquement en HS256_SHARED transitoire. none toujours rejeté.
  • En prod, JwtProductionReadinessValidator :
  • RS256_ASYMMETRIC → exige une source de clé publique (jwks-url ou public-keys) + kid ;
  • HS256_SHARED → exige secret ≥ 32 octets, non prévisible (déjà en place), et log un avertissement « palier transitoire » ;
  • audience = tech code layer 3 (inchangé, ADR-0011).

5. Les constantes de crypto sont centralisées, jamais par-app libre

  • L'app ne choisit pas l'algorithme ni ne détient de clé privée (sauf l'émetteur). Elle consomme les constantes Codex (TAIL_AUTH_KEY_MODE, TAIL_AUTH_JWKS_URL, TAIL_AUTH_AUDIENCE…), défaut au niveau groupe, override par cercle.
  • Mapping propriétés dynors.security.jwt ↔ Codex :
Propriété core Constante Codex
algorithm (HS256/RS256) dérivé de TAIL_AUTH_KEY_MODE
jwks-url TAIL_AUTH_JWKS_URL
public-keys[kid] (mode hors-ligne) dérivé JWKS
secret TAIL_AUTH_JWT_SECRET (mode HS256 transitoire)
issuer / audience / expiration TAIL_AUTH_JWT_ISSUER / _AUDIENCE / _JWT_EXPIRY_S

Conséquences

Bénéfices

  • Un validateur compromis ne peut plus forger d'identité (clé publique seule).
  • Rotation sans coupure (kid + JWKS overlap) — prévu avant la première mise en prod.
  • Un seul dictionnaire de constantes crypto, réutilisable en aval (entitlements, webhooks).
  • Cohérent avec ADR-0008 (même logique d'overlap/migration).

Coûts / contraintes

  • dynors-auth doit exposer un JWKS + gérer le cycle de vie des kid (génération, publication, retrait) — opération plateforme.
  • Les validateurs doivent résoudre par kid (cache JWKS) — évolution de JwtTokenProvider d'une clé unique vers un résolveur multi-clés.
  • Migration ordonnée HS256 → RS256 par cercle (overlap), pilotée par TAIL_AUTH_KEY_MODE.

Composants impactés

  • core/.../security : SecurityProperties.jwt (key-mode, jwks-url, public-keys par kid), JwtKeys (cache JWKS + résolution par kid), JwtTokenProvider (pose kid à l'émission, résout par kid à la validation), JwtProductionReadinessValidator (allowlist + sources de clé).
  • dynors-auth (futur) : JWKS endpoint, signature par kid, rotation.
  • Codex SIRRAT : constantes déjà présentes — defaults groupe à figer.
  • Vault : chemins versionnés …#jwt-private-{kid}.

Alternatives considérées

A — Rester HS256 + clé par realm. Écartée : un validateur garde un secret de forge pour son realm ; ne supprime pas A1, complexifie la distribution de N secrets.

B — HKDF par audience (réutiliser ADR-0008 pour le JWT). Cohérente mais : un validateur peut forger des tokens pour sa propre audience, et l'émetteur (racine) forge tout. Acceptable pour le transit machine, insuffisant pour l'identité utilisateur qui traverse des frontières de confiance (B2B). Conservée pour le transit, pas pour l'identité.

C — EdDSA au lieu de RS256. Techniquement supérieure (clés courtes, rapide), mais outillage JWKS/B2B moins universel. Autorisée par l'allowlist, non retenue par défaut pour l'instant.

D — Mono-clé sans kid. Écartée : rend la rotation impossible sans coupure — précisément le point « prévu en aval » à ne pas rater.

Plan d'exécution

  1. (Fait, base) Mode asymétrique opt-in dans dynors-security (clé statique) — JwtKeys, JwtTokenProvider, validateur prod, tests (JwtTokenProviderAsymmetricTest).
  2. Évoluer vers multi-clés par kid : public-keys map + résolution par en-tête kid.
  3. Ajouter le consommateur JWKS (TAIL_AUTH_JWKS_URL, cache TTL) côté validateurs.
  4. dynors-auth : JWKS endpoint + signature par kid + rotation Vault versionnée.
  5. Bascule par cercle via TAIL_AUTH_KEY_MODE (overlap HS256↔RS256), puis retrait HS256.
  6. Allowlist d'algorithmes durcie (interdire HS256 en prod une fois la bascule faite).

Suivi

Critère de succès : un token signé par dynors-auth (kid courant) est validé par toute app du realm ; un validateur ne peut pas émettre de token accepté ailleurs ; une rotation de kid ne provoque aucune coupure (overlap). Test : rejouer un token de l'ancien kid pendant la fenêtre d'overlap → accepté ; après retrait → rejeté.

Références

  • Palier 1 : ADR-0011 (audience/realm)
  • Isolement de secret (analogie) : ADR-0008 (transit SLY HKDF)
  • Audit / écart A1 : docs/security/AUDIT_SECURITE_VAULT_SIRRAT_2026-06-14.md
  • Guide config : docs/security/GUIDE_CONFIG_JWT_ASYMETRIQUE.md
  • Constantes : SIRRAT_CODEX_REFERENCE.md (TAIL_AUTH_*), IDENTITE_APPLICATIVE_3_LAYERS.md
  • Code : core/.../security/jwt/{JwtKeys,JwtTokenProvider}.java, config/JwtProductionReadinessValidator.java