Status: accepted (D10 del BRIEF; secreto cifrado como invariante en CONSTITUTION)
Context: La firma del token es HMAC-SHA256 (simétrica): el mismo secreto vive en
~250 cajas y en el servidor. Hay que poder rotar sin corte de servicio.
Alternatives: JKS/keystore de PKI (concepto de firma asimétrica que acá no aplica);
secreto único hardcodeado (sin rotación); tabla clave_firma por key_id (elegida).
Decision: "Keystore" = tabla clave_firma(key_id, secreto_cifrado, estado,
vigencia_desde, vigencia_hasta). El server valida contra la clave que indica el
keyId (offset 0) del token. Rotación: alta de nueva clave con nuevo keyId,
distribución a cajas, ambas válidas durante la transición.
Consequences: Compromiso de una caja se mitiga rotando keyId — riesgo aceptado
y documentado (RISKS.md). El secreto se guarda cifrado at-rest (master key vía
env/Vaultwarden), nunca en claro en repo ni logs.
Riesgo aceptado: quien extraiga el secreto de una caja puede fabricar vouchers
válidos hasta la rotación. Mitigación: rotación de keyId + señales antifraude
(concentración por caja/DNI/conexión).
Tabla clave_firma; cifrado del secreto con AES-GCM y master key por variable de
entorno; caché en memoria del secreto descifrado.
Validación: leer keyId del token → clave correspondiente → HMAC en tiempo constante.
Precisión (QA pass 1): la aceptación de una clave en validación se resuelve por
estado (activa y rotada validan; revocada no), NUNCA por reloj contra vigencia_*:
un voucher emitido con una clave luego rotada debe seguir validando ("rotación sin
corte"). Las fechas de vigencia son informativas para operación/rotación en cajas.
Clave de prueba para el ambiente de test (M6) con keyId reservado.