ADR-0013 — JWT asymétrique (RS256), kid + JWKS et rotation des clés de signature¶
Date : 2026-06-14 (clarifiée 2026-06-21)
Catégorie : A (convention de sécurité transverse — réalise le palier 2 d'ADR-0011)
Décideur : Lead architecte DYNORS
Statut : Acceptée — palier 1 livré 2026-06, palier 2 à venir (split clarifié 2026-06-21)
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).
⚠️ Split des paliers (clarification 2026-06-21 — incohérence index résolue)¶
L'ADR couvre deux paliers distincts dont l'état réel est différent. L'index a été corrigé pour refléter cette réalité.
Palier 1 — Multi-algorithmes statique (LIVRÉ)¶
- ✅
JwtKeys(core/security) supporte HS256, RS256, ES256, EdDSA (parsing PEM PKCS#8 / X.509 via JCE standard). - ✅
JwtTokenProviderpeut signer et valider en asymétrique avec clé statique injectée par config. - ✅
JwtProductionReadinessValidatorallowlist d'algorithmes (refusnone, exigence source de clé en RS256). - ✅ Slots Codex
TAIL_AUTH_KEY_MODE,TAIL_AUTH_JWKS_URL,TAIL_AUTH_JWT_SECRETprésents dans le seed. - ✅ Tests
JwtTokenProviderAsymmetricTest,JwtProductionReadinessValidatorTestprésents.
Statut palier 1 : Acceptée — livrée. Une app peut basculer en RS256 statique dès maintenant via Codex.
Palier 2 — JWKS dynamique + multi-kid + dynors-auth émetteur (EN COURS)¶
MAJ 2026-06-30 — état réévalué sur le code réel (l'inventaire « 0 fichier » ci-dessous était périmé).
- ✅
dynors-auth(l'émetteur centralisé) : existe (dynors-platform/auth) —JwtIssuer,KeyProvider,JwksService,AuthController, testsJwtRotationTest/KeyProviderTest/JwtIssuerTest. - ✅ JWKS endpoint :
JwksServiceexpose les clés publiques RSA (kty/use/alg/kid/n/e), multi-kidprêt pour la rotation. - ✅ Multi-clés par
kid: côté validateurs,JwtKeys.publicKeysByKid+JwtTokenProviderposent et résolvent lekid(carte statiquepublic-keys). - ✅ Consommateur JWKS distant (cache TTL, refresh sur
kidinconnu) : livré 2026-06-30 —JwksClient(dynors-security), câblé dansJwtTokenProvider.resolveVerificationKey(JWKS distant prioritaire, replipublic-keyshors-ligne) ;JwtProductionReadinessValidatoracceptejwks-urlcomme source de clé prod ; testsJwksClientTest+JwtTokenProviderJwksTest. - ✅ Durcissement allowlist (item 6) : livré 2026-06-30 —
dynors.security.jwt.allow-hs256-in-prod(défauttruetransition) ; àfalse,JwtProductionReadinessValidatorrefuse HS256 en prod (warning « palier transitoire » tant que toléré). Tests dédiés. - ✅ Runbook bascule par cercle (item 5) : documenté 2026-06-30 — procédure overlap pilotée par
TAIL_AUTH_KEY_MODEdansdocs/security/GUIDE_CONFIG_JWT_ASYMETRIQUE.md§7. - ⏳ Exécution de la bascule par cercle : reste l'opération (générer/publier les
kidau JWKS, basculer émetteur puis validateurs, retirerJWT_SECRET, poserallow-hs256-in-prod=false) + le branchement de la rotation Vault versionnée au déploiement.
Statut palier 2 : En cours — briques livrées, reste l'exécution ops. L'émetteur dédié dynors-auth (clé privée lue par lui seul) + la consommation JWKS + le verrou anti-HS256 ferment l'écart A1 dès qu'un cercle est basculé en RS256_ASYMMETRIC avec jwks-url et allow-hs256-in-prod=false. Il ne reste plus de code de bibliothèque à écrire — la migration est désormais une opération par cercle (runbook §7).
Conséquence pratique¶
| Capacité | Palier 1 (livré) | Palier 2 (à livrer) |
|---|---|---|
| Algorithme RS256 supporté côté apps | ✅ | ✅ |
| Rotation de clé sans coupure | ❌ (clé statique unique) | ✅ (overlap kid) |
| Émetteur unique séparé des validateurs | ❌ (clé privée potentiellement partagée) | ✅ (dynors-auth exclusif) |
| Écart A1 fermé cryptographiquement | ❌ | ✅ |
| JWKS B2B exposable | ❌ | ✅ |
Recommandation 2026-06-21 : prioriser le palier 2 (livraison dynors-auth JWKS) dans le même train que l'ADR-0001 FISCAL, car les deux bloquent le passage en mode « plateforme mature » (auth centralisée + multi-juridictions).
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). - ✅ Multi-clés par
kid:public-keysmap + résolution par en-têtekid(JwtKeys.publicKeysByKid). - ✅ Consommateur JWKS (
TAIL_AUTH_JWKS_URL, cache TTL + refresh rotation) côté validateurs —JwksClient(2026-06-30). - ✅
dynors-auth: JWKS endpoint (JwksService) + signature parkid(JwtIssuer) + provider de clés (KeyProvider, tests rotation). Rotation Vault versionnée : à brancher en déploiement. - ✅ Runbook bascule par cercle via
TAIL_AUTH_KEY_MODE(overlap HS256↔RS256) —GUIDE_CONFIG_JWT_ASYMETRIQUE.md§7 (exécution ops par cercle à dérouler). - ✅ Allowlist durcie :
allow-hs256-in-prod=false→ HS256 refusé en prod (JwtProductionReadinessValidator, 2026-06-30).
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