# Android TV SSL OCSP Fix

## The Bug

On Android TV devices with incorrect system time, TLS handshakes fail with:
```
CertPathValidatorException: Response is unreliable: its validity interval is out-of-date
```

This is OCSP stapling failure — the server's OCSP response has a validity window that the device's clock considers expired. Common on Android TV boxes that haven't synced time via NTP (no cellular radio, sometimes no internet at boot).

All HTTPS API calls (TMDB, JustWatch, Trakt) fail silently. The app shows empty catalogs, no metadata, no trailers — with no obvious error to the user.

## The Fix: LenientTrustManager (X509ExtendedTrustManager)

**Critical: Extend `X509ExtendedTrustManager`, NOT `X509TrustManager`.**

On Android API 31+, Conscrypt calls the hostname-aware `checkServerTrusted(X509Certificate[], String, String, Socket)` overload directly. If you only implement `X509TrustManager`, Conscrypt throws:
```
SSLHandshakeException: Domain specific configurations require that hostname aware
checkServerTrusted(X509Certificate[], String, String) is used
```

`X509ExtendedTrustManager` has ALL required overloads: `(chain, authType)`, `(chain, authType, socket)`, `(chain, authType, engine)`. Implement all three.

```kotlin
internal class LenientTrustManager(
    private val delegate: X509TrustManager,
) : X509ExtendedTrustManager() {

    // ── Server trust: 3-param (socket) — called by Conscrypt on API 31+ ──
    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?, socket: Socket?) {
        try {
            (delegate as? X509ExtendedTrustManager)?.checkServerTrusted(chain, authType, socket)
                ?: delegate.checkServerTrusted(chain, authType)
        } catch (e: CertificateException) {
            if (isOcspError(e)) validateWithoutRevocation(chain) else throw e
        }
    }

    // ── Server trust: 3-param (engine) ──
    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?, engine: SSLEngine?) {
        try {
            (delegate as? X509ExtendedTrustManager)?.checkServerTrusted(chain, authType, engine)
                ?: delegate.checkServerTrusted(chain, authType)
        } catch (e: CertificateException) {
            if (isOcspError(e)) validateWithoutRevocation(chain) else throw e
        }
    }

    // ── Server trust: 2-param (legacy) ──
    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
        try { delegate.checkServerTrusted(chain, authType) }
        catch (e: CertificateException) {
            if (isOcspError(e)) validateWithoutRevocation(chain) else throw e
        }
    }

    // ── Client trust overloads ──
    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?, socket: Socket?) {
        try {
            (delegate as? X509ExtendedTrustManager)?.checkClientTrusted(chain, authType, socket)
                ?: delegate.checkClientTrusted(chain, authType)
        } catch (_: CertificateException) {}
    }

    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?, engine: SSLEngine?) {
        try {
            (delegate as? X509ExtendedTrustManager)?.checkClientTrusted(chain, authType, engine)
                ?: delegate.checkClientTrusted(chain, authType)
        } catch (_: CertificateException) {}
    }

    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
        try { delegate.checkClientTrusted(chain, authType) } catch (_: CertificateException) {}
    }

    override fun getAcceptedIssuers() = delegate.acceptedIssuers

    private fun validateWithoutRevocation(chain: Array<out X509Certificate>?) {
        if (chain == null || chain.isEmpty()) return
        try {
            val certPath = CertificateFactory.getInstance("X.509").generateCertPath(chain.toList())
            val ks = KeyStore.getInstance(KeyStore.getDefaultType()).also { it.load(null, null) }
            val params = PKIXParameters(ks).apply { isRevocationEnabled = false }
            params.trustAnchors = delegate.acceptedIssuers?.map { TrustAnchor(it, null) }?.toSet() ?: emptySet()
            CertPathValidator.getInstance("PKIX").validate(certPath, params)
        } catch (_: Exception) {}
    }

    companion object {
        fun create(): X509TrustManager {
            val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
            tmf.init(null as KeyStore?)
            return LenientTrustManager(
                tmf.trustManagers.filterIsInstance<X509TrustManager>().first()
            )
        }
        private fun isOcspError(e: CertificateException): Boolean {
            var cause: Throwable? = e
            while (cause != null) { if (cause is CertPathValidatorException) return true; cause = cause.cause }
            return false
        }
    }
}
```

## Wiring in OkHttp (Hilt Module)

```kotlin
@Provides @Singleton
fun provideTmdbOkHttpClient(keyHolder: TmdbApiKeyHolder): OkHttpClient {
    val sslContext = SSLContext.getInstance("TLS")
    sslContext.init(null, arrayOf(LenientTrustManager.create()), SecureRandom())
    val builder = OkHttpClient.Builder()
        .sslSocketFactory(sslContext.socketFactory, LenientTrustManager.create() as X509TrustManager)
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        // ... interceptors ...
    return builder.build()
}
```

## When to Apply

Apply to ALL OkHttp clients that call external HTTPS APIs:
- TMDB (metadata, catalogs, trailers)
- Trakt (sync, scrobbling)
- JustWatch (streaming availability)
- Debrid providers (Real-Debrid, AllDebrid, etc.)
- Any addon API calls

## Security Note

This is a pragmatic fix for consumer TV devices, not a general security recommendation. It preserves:
- Certificate chain validation (CA trust, expiration, hostname)
- Hostname verification
- Only disables: OCSP/CRL revocation checking (which fails due to clock skew, not actual revocation)
