# Radio Babasónica — Signature-256 Results

## Resumen

```text
Actualización completada: SI
Signature-256 implementado: SI
SHA-256 implementado: SI
Fallback Signature/SHA-1: SI
Downgrade bloqueado: SI
Tests generales: PASS
Tests Signature-256: PASS
PHP lint: PASS
Endpoint público sin cambios: SI
```

La implementación sigue la recomendación vigente de Amazon: `Signature-256` se valida con SHA-256 sobre el body HTTP completo. `Signature`/SHA-1 permanece únicamente como fallback cuando `Signature-256` está ausente.

## Inspección inicial

- `src/AlexaRequestVerifier.php` obtenía `SignatureCertChainUrl` y `Signature` mediante comparación de header sin distinguir casing, decodificaba Base64 y ejecutaba `openssl_verify` con `OPENSSL_ALGO_SHA1` sobre el body RAW.
- `src/AlexaRequestHandler.php` devolvía HTTP 400 genérico ante errores de verificación y enviaba sólo mensajes técnicos sanitizados al logger.
- `public/index.php` entregaba a la capa de transporte los headers de `getallheaders()` y el contenido exacto de `php://input`.
- `src/bootstrap.php` construía el verificador, controller, handler y logger sin modo inseguro.
- `src/SanitizedLogger.php` no registraba headers, firmas, certificados ni bodies.

## Archivos modificados

- `src/AlexaRequestVerifier.php`
- `src/AlexaRequestHandler.php`
- `src/RadioBabasonicaController.php`
- `src/SanitizedLogger.php`
- `public/index.php`
- `README.md`

## Archivos creados

- `tests/signature256.php`
- `RADIO_BABASONICA_ALEXA_SIGNATURE256_RESULTS.md`

No se crearon certificados ni claves persistentes. La clave RSA de prueba se genera en memoria en cada ejecución.

## Lógica final

```text
Request Alexa
   |
   +-- Signature-256 presente?
   |       |
   |       +-- SI --> SHA-256 --> válida? --> aceptar (sha256)
   |                           \
   |                            --> inválida --> rechazar, sin fallback
   |
   +-- NO --> Signature presente?
           |
           +-- SI --> SHA-1 fallback --> válida? --> aceptar/rechazar
           |
           +-- NO --> rechazar
```

El algoritmo se selecciona antes de verificar. Por eso una `Signature-256` presente impide que se consulte `Signature`, incluso si la firma SHA-256 falla.

## Código relevante

Selección prioritaria:

```php
$signature256 = $this->header($headers, 'Signature-256');
if ($signature256 !== null) {
    return ['value' => $signature256, 'openssl_algorithm' => OPENSSL_ALGO_SHA256, 'log_name' => 'sha256'];
}
```

Fallback explícito:

```php
$signature = $this->header($headers, 'Signature');
if ($signature !== null) {
    return ['value' => $signature, 'openssl_algorithm' => OPENSSL_ALGO_SHA1, 'log_name' => 'sha1-fallback'];
}
```

Verificación de los bytes RAW:

```php
$decodedSignature = base64_decode($signature, true);
return $decodedSignature !== false
    && openssl_verify($body, $decodedSignature, $certificate, $algorithm) === 1;
```

## Pruebas Signature-256

| Caso | Esperado | Resultado |
|---|---|---|
| Signature-256 válida | aceptar SHA-256 | PASS |
| Signature-256 inválida | rechazar | PASS |
| Signature-256 + Signature | usar SHA-256 | PASS |
| Signature-256 inválida + Signature válida | rechazar, sin downgrade | PASS |
| sólo Signature válida | fallback SHA-1 | PASS |
| sin firmas | rechazar | PASS |

También pasó el caso adicional de nombres de headers con casing mixto y formato `HTTP_SIGNATURE_256`/`HTTP_SIGNATURE` de Apache/PHP.

## Pruebas de regresión

| Prueba | Resultado |
|---|---|
| LaunchRequest | PASS |
| PlayRadioIntent | PASS |
| PauseIntent | PASS |
| ResumeIntent | PASS |
| StopIntent | PASS |
| PlaybackStarted sin session | PASS |
| PlaybackStopped sin session | PASS |
| PlaybackFailed sin session | PASS |
| PlaybackController Play sin propiedades prohibidas | PASS |
| PlaybackController Pause sin propiedades prohibidas | PASS |
| Interaction Model | PASS |
| JSON inválido | PASS |
| Sólo POST | PASS |

`tests/run.php`: `ALL TESTS PASSED`.

## PHP lint

Todos los archivos PHP bajo `public`, `src`, `config` y `tests` pasaron `php -l` sin errores de sintaxis.

## Endpoint

El endpoint permanece sin cambios:

```text
https://aproposito.ddns.net/radio-babasonica-alexa/public/
```

- HTTPS: PASS
- GET: HTTP 405
- POST con JSON inválido: HTTP 400 controlado
- Content-Type: `application/json; charset=utf-8`

No se enviaron firmas falsas para intentar superar la verificación pública.

## Seguridad

- `Signature-256` tiene prioridad absoluta y usa `OPENSSL_ALGO_SHA256`.
- `Signature` usa `OPENSSL_ALGO_SHA1` sólo cuando `Signature-256` no existe.
- No existe downgrade tras una falla SHA-256.
- Se verifica el body RAW exacto recibido desde `php://input`; no se re-serializa.
- Continúan activas la restricción HTTPS/host/path/puerto de `SignatureCertChainUrl`, parseo X.509, vigencia y SAN `echo-api.amazon.com`.
- Continúan activas la tolerancia máxima de timestamp de 150 segundos y la comparación del Skill ID.
- Los logs sólo añaden `signature_algorithm=sha256`, `sha1-fallback` o `null`; nunca registran firmas, certificado, body o headers completos.
- Los fallos de firma usan identificadores sanitizados como `signature_verification_failed` y `signature_header_missing`.

### Hallazgo preexistente documentado

La implementación anterior —conservada en esta tarea salvo por el algoritmo de firma— valida que el certificado se descargue mediante TLS confiable y comprueba el X.509 de firma, su vigencia y SAN. No realiza una validación criptográfica explícita de cada certificado intermedio contenido en una cadena PEM descargada. La documentación oficial de Amazon también pide validar la cadena completa hasta una CA raíz confiable. No se cambió silenciosamente ese comportamiento porque esta tarea está limitada a Signature-256; debe considerarse un endurecimiento separado antes de certificación si Amazon entrega una cadena múltiple que PHP no valide implícitamente durante la descarga HTTPS.

## Pendientes

```text
Skill ID real aún no configurado
Prueba real Alexa aún no ejecutada
Developer Console aún no configurada
Echo físico aún no probado
Revisar/implementar validación explícita de la cadena PEM completa antes de certificación
```

## Criterios de aceptación

```text
[x] Signature-256 tiene prioridad
[x] Signature-256 usa SHA-256
[x] Signature usa SHA-1 sólo como fallback opcional
[x] No hay downgrade si Signature-256 está presente y falla
[x] Body RAW se verifica sin modificar
[x] Validaciones existentes de certificado permanecen activas
[x] Timestamp sigue validándose
[x] Skill ID sigue validándose
[x] Logs no exponen firmas
[x] Tests Signature-256 pasan
[x] Tests anteriores siguen pasando
[x] PHP lint pasa
[x] Endpoint HTTPS no cambió
[x] No se modificó MediaMTX
[x] No se modificó radiobbs
[x] No se reinició ningún servicio
[x] Reporte final generado
```
