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_ref → protected: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-authsigne 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
kiddepuis un JWKS : - cible :
TAIL_AUTH_JWKS_URL(endpointdynors-auth, cache TTL) ; - mode hors-ligne / dégradé : un ou plusieurs PEM publics injectés en config
(
public-keys), indexés parkid. - Le JWKS expose plusieurs clés simultanément → la rotation se fait par
recouvrement, exactement comme le mode
DUALdu 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-authseul (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-securityimpose une liste blanche :RS256(+EdDSAautorisé),HS256toléré uniquement enHS256_SHAREDtransitoire.nonetoujours rejeté.- En prod,
JwtProductionReadinessValidator: RS256_ASYMMETRIC→ exige une source de clé publique (jwks-urloupublic-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-authdoit exposer un JWKS + gérer le cycle de vie deskid(génération, publication, retrait) — opération plateforme.- Les validateurs doivent résoudre par
kid(cache JWKS) — évolution deJwtTokenProviderd'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(posekidà l'émission, résout parkidà la validation),JwtProductionReadinessValidator(allowlist + sources de clé).dynors-auth(futur) : JWKS endpoint, signature parkid, 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¶
- (Fait, base) Mode asymétrique opt-in dans
dynors-security(clé statique) —JwtKeys,JwtTokenProvider, validateur prod, tests (JwtTokenProviderAsymmetricTest). - Évoluer vers multi-clés par
kid:public-keysmap + résolution par en-têtekid. - Ajouter le consommateur JWKS (
TAIL_AUTH_JWKS_URL, cache TTL) côté validateurs. dynors-auth: JWKS endpoint + signature parkid+ rotation Vault versionnée.- Bascule par cercle via
TAIL_AUTH_KEY_MODE(overlap HS256↔RS256), puis retrait HS256. - 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