# Radio Babasónica Alexa — Security Hardening FINAL Results

## Resumen

```text
Hardening completado: SI
URL normalization: PASS
SSRF protection: PASS
Full X.509 chain validation: PASS
Trusted CA source identificado: SI
Signature-256: PASS
Anti-downgrade: PASS
Timestamp: PASS
Skill ID validation: PASS
Functional tests: PASS
PHP lint: PASS
Endpoint HTTPS: PASS
```

El backend queda **SECURITY READY** para la siguiente fase manual, Alexa Developer Console, sujeto a configurar el Skill ID real y probar una petición auténtica de Alexa.

## Inventario inspeccionado

Se inventarió recursivamente el proyecto real. Existían:

- `config/config.php`
- `interaction-model/es-MX.json`
- `public/.htaccess`, `public/index.php`
- `src/AlexaRequestHandler.php`, `AlexaRequestVerifier.php`, `AlexaResponse.php`, `RadioBabasonicaController.php`, `SanitizedLogger.php`, `bootstrap.php`
- `storage/cert-cache/.gitkeep`, `storage/logs/.gitkeep`, log runtime excluido del ZIP
- `tests/run.php`, `tests/signature256.php` y diez fixtures JSON funcionales
- `README.md`, `.gitignore`
- Los documentos de instrucciones y los dos reportes anteriores
- Un ZIP anterior `radio-babasonica-alexa.zip`, que no se anidará en el ZIP final

Todos los archivos mencionados por reportes anteriores existen realmente.

Antes del cambio se confirmó que `Signature-256` seleccionaba `OPENSSL_ALGO_SHA256`, impedía downgrade, `Signature` usaba SHA-1 sólo como fallback y la firma se comprobaba contra el string RAW leído de `php://input`. Esa lógica permanece intacta.

## Trust store detectado

```text
openssl.cafile: \xampp\apache\bin\curl-ca-bundle.crt
openssl.capath: (vacío)
curl.cainfo: \xampp\apache\bin\curl-ca-bundle.crt
OpenSSL Library Version: OpenSSL 3.0.11 19 Sep 2023
CA bundle usado: C:\xampp\apache\bin\curl-ca-bundle.crt
```

El bundle ya existía en XAMPP, es el configurado por PHP para OpenSSL/cURL y no fue descargado, inventado ni modificado.

## Archivos modificados

- `config/config.php`
- `src/bootstrap.php`
- `src/AlexaRequestVerifier.php`
- `README.md`

## Archivos creados

- `tests/security_final.php`
- `RADIO_BABASONICA_ALEXA_SECURITY_FINAL_RESULTS.md`
- `radio-babasonica-alexa-security-final.zip`

## URL normalization

`normalizeSignatureCertChainUrl()` primero parsea la URL, elimina el fragmento, normaliza el path mediante una pila de segmentos que elimina `.`/`..` y slashes duplicados, reconstruye la URL y sólo entonces valida:

- scheme exacto `https`, sin importar casing;
- host exacto `s3.amazonaws.com`, sin resolver aliases;
- puerto ausente o exactamente 443;
- path case-sensitive iniciado por `/echo.api/`;
- sin userinfo ni query;
- sin backslashes ni codificaciones `%2e`, `%2f` o `%5c` capaces de alterar la ruta.

La validación sucede antes de cualquier I/O. La descarga usa HTTPS con peer/name validation, timeout de 3 segundos, límite de 65,536 bytes y redirects desactivados (`follow_location=0`). Esto bloquea localhost, loopback, LAN/otros hosts, esquemas locales y redirecciones a destinos no autorizados.

| Entrada | Esperado | Resultado |
|---|---|---|
| `https://s3.amazonaws.com/echo.api/echo-api-cert.pem` | aceptar | PASS |
| `https://s3.amazonaws.com:443/echo.api/echo-api-cert.pem` | aceptar | PASS |
| `/echo.api/../echo.api/` | normalizar y aceptar | PASS |
| `/echo.api/./` | normalizar y aceptar | PASS |
| `//echo.api//` | normalizar y aceptar | PASS |
| `/echo.api/foo/../` | normalizar y aceptar | PASS |
| URL con `#fragment` | quitar fragment y aceptar | PASS |
| `http://s3.amazonaws.com/...` | rechazar | PASS |
| `ftp://` / `file://` | rechazar | PASS |
| `https://evil.com/...` | rechazar | PASS |
| `https://s3.amazonaws.com.evil.com/...` | rechazar | PASS |
| puerto 444 | rechazar | PASS |
| `/ECHO.API/` / `/Echo.Api/` | rechazar | PASS |
| `/invalid.path/` / `/echo.api.evil/` | rechazar | PASS |
| localhost / `127.0.0.1` / `::1` | rechazar | PASS |
| userinfo, query, backslash o dot/slash codificado | rechazar | PASS |

## Certificate chain validation

La respuesta PEM se separa por todos sus bloques `BEGIN/END CERTIFICATE` y cada bloque debe ser parseable. Se exige leaf más al menos un certificado adicional. El orden se conserva:

```text
certificado 0: signing/leaf
certificados 1..n: intermediarios no confiables
trust anchor: C:\xampp\apache\bin\curl-ca-bundle.crt
```

El leaf se valida por `NotBefore`, `NotAfter` y SAN DNS exacto `echo-api.amazon.com`. Los intermediarios se escriben en un archivo temporal seguro, `openssl_x509_checkpurpose()` valida criptográficamente el leaf y la cadena contra el bundle confiable, y el temporal se elimina siempre en `finally`. No se ejecuta `openssl.exe` por request. Un certificado cacheado vuelve a pasar PEM, vigencia, SAN y chain trust.

TLS de descarga y confianza de la cadena Alexa son controles separados: el primero autentica `s3.amazonaws.com`; el segundo autentica la cadena del certificado de firma descargado.

## Tests criptográficos

La prueba genera en memoria una PKI RSA-2048 sintética Root → Intermediate → Leaf con extensiones CA y SAN apropiadas. Los únicos archivos temporales son config, raíces públicas y temporales de validación; se destruyen al finalizar. No persiste ninguna private key.

| Prueba | Esperado | Resultado |
|---|---|---|
| cadena válida | aceptar | PASS |
| root no confiable | rechazar | PASS |
| intermediate incorrecto | rechazar | PASS |
| cadena/firma rota | rechazar | PASS |
| leaf expirado | rechazar | PASS |
| leaf no vigente aún | rechazar | PASS |
| SAN incorrecto | rechazar | PASS |
| intermediate faltante | rechazar | PASS |

## Signature-256 tests

| Prueba | 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 | aceptar SHA-1 fallback | PASS |
| sin firmas | rechazar | PASS |
| casing/formato `HTTP_*` | reconocer | PASS |

## Regresión funcional

| 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.PlayCommandIssued | PASS |
| PlaybackController.PauseCommandIssued | PASS |
| JSON inválido | PASS |
| sólo POST | PASS |

Timestamp continúa limitado a 150 segundos y el applicationId continúa comparándose en tiempo constante con `ALEXA_SKILL_ID` antes de entrar al controller.

## PHP lint

Todos los `.php` bajo `config`, `public`, `src` y `tests` pasaron `php -l`: **PASS**.

## Endpoint

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

- HTTPS válido: PASS
- GET: HTTP 405
- POST JSON inválido: HTTP 400 controlado
- Content-Type: `application/json; charset=utf-8`
- No se intentó falsificar una request Alexa en producción.

## Stream sin impacto

La URL permanece exactamente:

```text
https://aproposito.ddns.net/live/radiobbs/index.m3u8
```

`ffprobe` confirmó AAC-LC, audio-only, 48 kHz, stereo y ~129 kbps. No se modificó `radiobbs`, MediaMTX, `/live/`, FFmpeg, Apache ni otro servicio; no se reinició ningún servicio.

## Logging y errores

Los rechazos usan motivos sanitizados: `cert_url_invalid`, `cert_download_failed`, `cert_pem_invalid`, `cert_expired`, `cert_not_yet_valid`, `cert_san_invalid`, `cert_chain_invalid`, `signature_verification_failed` y `signature_header_missing`. El cliente sólo recibe un error genérico HTTP 400. No se registran firmas, body RAW, certificados completos, tokens, headers sensibles ni claves.

## Pendientes reales

```text
Skill ID real configurado: NO
Alexa Developer Console configurada: NO
Request Alexa real probada: NO
Echo físico probado: NO
```

Los últimos tres puntos pertenecen a la fase siguiente y no son fallos de este hardening.

## ZIP final auditado

```text
Ruta: C:\xampp\htdocs\radio-babasonica-alexa\radio-babasonica-alexa-security-final.zip
Tamaño auditado antes de la incorporación final de esta línea: 47,868 bytes
Entradas: 33
Proyecto completo: SI
```

Se confirmó dentro del ZIP la presencia de `public/index.php`, `config/config.php`, `interaction-model/es-MX.json`, `src/AlexaRequestVerifier.php`, `tests/run.php`, `README.md` y este reporte. También incluye las demás fuentes, fixtures, modelos, documentación y `.gitkeep` del proyecto.

Se excluyeron el ZIP anterior, el propio ZIP de salida, `.git`, el log runtime, payloads de caché y temporales. La auditoría confirmó: ningún `.key`/`.pfx`/`.p12`, ningún marcador PEM de private key, ningún log runtime y ningún certificado cacheado.

## Criterios finales

```text
[x] SignatureCertChainUrl se normaliza antes de validar
[x] dot segments, duplicate slashes y fragment eliminados
[x] HTTPS, host exacto, path case-sensitive y puerto 443
[x] protección SSRF y redirects bloqueados
[x] cadena PEM completa parseada
[x] leaf NotBefore, NotAfter y SAN exacto validados
[x] cadena completa termina criptográficamente en CA confiable
[x] trust store real identificado, sin roots inventados
[x] Signature-256, SHA-256 y anti-downgrade preservados
[x] timestamp <= 150 s y Skill ID preservados
[x] tests URL, PKI, Signature-256 y funcionales pasan
[x] PHP lint y endpoint HTTPS pasan
[x] radiobbs, MediaMTX y Apache sin cambios/reinicios
[x] reporte final y ZIP completo generados/auditados
```
