[{"data":1,"prerenderedAt":19},["ShallowReactive",2],{"blog:post:en:testing-api-tls-cipher-suite-proxy-simulation":3},{"slug":4,"lang":5,"title":6,"summary":7,"date":8,"tags":9,"tag_slugs":14,"thumbnail_url":15,"translations":16,"body":17,"asset_base":18},"testing-api-tls-cipher-suite-proxy-simulation","en","Testing API TLS Compatibility with Proxy-Based Cipher Suite Simulation","Learn how to use proxies to force specific TLS versions and cipher suites when calling APIs, uncover compatibility issues, and ensure your clients work across diverse server configurations.","2026-10-06",[10,11,12,13],"proxies","tls","api-testing","security",[10,11,12,13],"https://blog-api.ro-proxy.com/api/blog/posts/testing-api-tls-cipher-suite-proxy-simulation/thumbnail.svg?lang=en",[5],"## Why TLS Version and Cipher Suite Matter\n\nModern APIs expose TLS endpoints that must be consumable by a wide range of clients—from legacy Java 8 services to the latest browsers. Server administrators often disable old protocols (TLS 1.0/1.1) or weak ciphers (RC4, 3DES) for security, while some internal systems still require them for compatibility. When you integrate a third‑party API, you need to verify that your client library or custom code works with the exact TLS configuration the server expects. \n\nTesting this directly against the production endpoint can be risky: you might trigger rate limits, cause alerting, or inadvertently expose sensitive data. A safer approach is to place a controllable TLS‑terminating proxy between your client and the API. The proxy handles the client‑side TLS handshake, letting you dictate which protocol version and cipher suite are offered, then re‑encrypts the traffic to the upstream server using its own (usually permissive) settings. \n\nThis guide shows how to set up such a proxy with **mitmproxy**, enforce TLS versions and cipher suites, and verify API responses using Python. The same principles apply to other tools (Squid with SSL‑bump, HAProxy, or custom Go/TLS servers).\n\n## Prerequisites\n\n- A Linux/macOS workstation with Docker (optional) or root access to install mitmproxy.\n- Python 3.9+ and the `requests` library.\n- An API endpoint you control or a test service (e.g., https://httpbin.org/tls‑version).\n\n### Installing mitmproxy\n\n```bash\n# Using pip (recommended for latest version)\npip3 install mitmproxy\n# Verify\nmitmproxy --version\n```\n\nIf you prefer Docker:\n\n```bash\ndocker run -it --rm -p 8080:8080 mitmproxy/mitmproxy\n```\n\nThe proxy will listen on `localhost:8080` for HTTP and HTTPS traffic.\n\n## Understanding the TLS Handshake Flow\n\nWhen a client connects through an HTTP CONNECT proxy to an HTTPS site, the proxy merely forwards the raw TLS ClientHello. With mitmproxy in **transparent** mode, we can terminate the TLS connection, inspect (or modify) the ClientHello, and then initiate a new TLS connection to the upstream server. This gives us full control over the version and cipher suite presented by the client.\n\nMitmproxy achieves this via an *addon* that replaces the default `SSLContext` used for the client‑side connection.\n\n## Step‑by‑Step: Forcing TLS 1.2 with a Specific Cipher Suite\n\nWe’ll create a simple addon that forces the client side to use TLS 1.2 and only the cipher `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` (IANA name `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`).\n\n1. **Create the addon file** `tls_force.py`:\n\n```python\nfrom mitmproxy import tls, ctx\n\nclass TLSCipherForcer:\n    def __init__(self, tls_version: tls.TLSVersion, cipher_name: str):\n        self.tls_version = tls_version\n        self.cipher_name = cipher_name\n\n    def clientconnect(self, data: tls.ClientHelloData):\n        # Called after the ClientHello is parsed but before we send our own ClientHello\n        ctx.log.info(f\"Forcing TLS version {self.tls_version} and cipher {self.cipher_name}\")\n        # Build a new ClientHello with our constraints\n        data.version = self.tls_version\n        # Keep only the desired cipher; remove all others\n        data.ciphers = [self.cipher_name]\n        # Optionally disable extensions that could influence version negotiation\n        data.extensions = []\n\naddons = [\n    TLSCipherForcer(tls.TLSVersion.TLSv1_2, \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\")\n]\n```\n\n2. **Run mitmproxy with the addon**:\n\n```bash\nmitmproxy -s tls_force.py --mode regular --listen-port 8080\n```\n\nThe `--mode regular` flag tells mitmproxy to act as a standard forward proxy (not transparent). Your client will need to point to `http://localhost:8080` as its proxy.\n\n3. **Configure your Python client to use the proxy**\n\n```python\nimport requests\n\nproxies = {\n    \"http\":  \"http://localhost:8080\",\n    \"https\": \"http://localhost:8080\",\n}\n\nresponse = requests.get(\n    \"https://httpbin.org/tls-version\",\n    proxies=proxies,\n    timeout=10\n)\n\nprint(response.status_code)\nprint(response.json())\n```\n\n`httpbin.org/tls-version` returns the TLS version and cipher suite negotiated by the server, letting you verify that the server saw the forced client settings.\n\n### Expected Output\n\nIf the server supports TLS 1.2 and the chosen cipher, you’ll see something like:\n\n```json\n{\n  \"tls_version\": \"TLS 1.2\",\n  \"cipher\": \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"\n}\n```\n\nIf the server does **not** support that combination, the connection will fail with an SSL error (`SSL: WRONG_VERSION_NUMBER` or `handshake failure`).\n\n## Testing Multiple Configurations Automatically\n\nManually changing the addon for each combination is tedious. Instead, we can drive mitmproxy from a Python script that updates the addon’s parameters on the fly. Mitmproxy’s `--set` option allows runtime changes, but a simpler approach is to create a temporary addon file for each test.\n\nHere’s a helper that generates a new addon, runs mitmproxy in a subprocess, executes a request, and collects the result:\n\n```python\nimport subprocess, time, json, os, signal\nfrom pathlib import Path\n\ndef test_tls_config(tls_version_str: str, cipher_name: str, target_url: str) -> dict:\n    # Map string to mitmproxy TLSVersion enum\n    version_map = {\n        \"TLSv1.0\": \"TLSv1_0\",\n        \"TLSv1.1\": \"TLSv1_1\",\n        \"TLSv1.2\": \"TLSv1_2\",\n        \"TLSv1.3\": \"TLSv1_3\",\n    }\n    tls_enum = version_map[tls_version_str]\n\n    addon_code = f\"\"\"\nfrom mitmproxy import tls, ctx\n\nclass TLSCipherForcer:\n    def clientconnect(self, data: tls.ClientHelloData):\n        data.version = tls.{tls_enum}\n        data.ciphers = [\"{cipher_name}\"]\n        data.extensions = []\n\naddons = [TLSCipherForcer()]\n\"\"\"\n    addon_path = Path(\"tmp_tls_addon.py\")\n    addon_path.write_text(addon_code)\n\n    # Start mitmproxy\n    proc = subprocess.Popen([\n        \"mitmproxy\",\n        f\"-s\", str(addon_path),\n        \"--mode\", \"regular\",\n        \"--listen-port\", \"8080\",\n        \"--quiet\"\n    ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\n    # Give the proxy a moment to start\n    time.sleep(2)\n\n    try:\n        resp = requests.get(target_url, proxies={\"http\": \"http://localhost:8080\", \"https\": \"http://localhost:8080\"}, timeout=10)\n        result = {\n            \"tls_version\": tls_version_str,\n            \"cipher\": cipher_name,\n            \"status_code\": resp.status_code,\n            \"json\": resp.json() if resp.headers.get(\"content-type\", \"\").startswith(\"application/json\") else None,\n            \"text\": resp.text[:200]\n        }\n    except Exception as e:\n        result = {\n            \"tls_version\": tls_version_str,\n            \"cipher\": cipher_name,\n            \"error\": str(e)\n        }\n    finally:\n        proc.send_signal(signal.SIGINT)\n        proc.wait(timeout=5)\n        addon_path.unlink(missing_ok=True)\n    return result\n\n# Example matrix\nmatrix = [\n    (\"TLSv1.0\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\"),\n    (\"TLSv1.1\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA8\"),\n    (\"TLSv1.2\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"),\n    (\"TLSv1.3\", \"TLS_AES_128_GCM_SHA256\"),\n]\n\nfor ver, cip in matrix:\n    out = test_tls_config(ver, cip, \"https://httpbin.org/tls-version\")\n    print(json.dumps(out, indent=2))\n```\n\nRunning the script yields a table of successes/failures, giving you a quick compatibility matrix for the API.\n\n## Real‑World Example: Validating a Payment Gateway\n\nSuppose you integrate with a payment processor that claims to support only TLS 1.2 with forward‑secrecy ciphers. Using the approach above, you can:\n\n1. **Confirm** that the gateway rejects TLS 1.0/1.1 connections (helps you satisfy PCI‑DSS requirements).\n2. **Detect** if the gateway unexpectedly allows a weak cipher like `TLS_RSA_WITH_3DES_EDE_CBC_SHA` (a potential downgrade vector).\n3. **Document** the exact TLS configuration your integration relies on, which is useful for auditors and for future migration planning.\n\n### Sample Output (Success)\n\n```json\n{\n  \"tls_version\": \"TLSv1.2\",\n  \"cipher\": \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n  \"status_code\": 200,\n  \"json\": {\n    \"tls_version\": \"TLS 1.2\",\n    \"cipher\": \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"\n  }\n}\n```\n\n### Sample Output (Failure)\n\n```json\n{\n  \"tls_version\": \"TLSv1.0\",\n  \"cipher\": \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\",\n  \"error\": \"HTTPSConnectionPool(host='httpbin.org', port=443): Max retries exceeded with url: /tls-version: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:1129)\"\n}\n```\n\nThese results make it trivial to produce a compliance report.\n\n## Extending the Technique\n\n- **Client Certificate Testing:** Replace the `clientconnect` hook to load a specific client cert and test mutual TLS.\n- **SNI Manipulation:** Modify the `servername` extension to verify virtual‑host handling.\n- **Protocol Downgrade Detection:** Run a sweep from TLS 1.3 down to TLS 1.0 and plot the first version that succeeds.\n- **Integration with CI:** Package the script as a Docker image and run it in your pipeline to gate releases on TLS compatibility.\n\n## Troubleshooting Tips\n\n| Symptom                                 | Likely Cause                                 | Fix                                                            |\n|-----------------------------------------|----------------------------------------------|---------------------------------------------------------------|\n| Connection timeout                      | Proxy not listening or firewall blocking     | Verify `mitmproxy` is running and ports are open (`netstat -tlnp`). |\n| `SSL: CERTIFICATE_VERIFY_FAILED`        | Proxy uses its own CA; client doesn’t trust it | Add mitmproxy’s CA to your trust store or set `verify=False` (for testing only). |\n| Server returns 400 Bad Request          | Host header altered by proxy                 | Ensure you’re using a regular forward proxy (`--mode regular`) not transparent mode. |\n| No change in observed TLS version       | Addon not applied (syntax error)             | Check mitmproxy logs for startup errors; run with `--verbose`. |\n\n## Conclusion\n\nUsing a TLS‑terminating proxy gives you precise, repeatable control over the client‑side TLS handshake without modifying your application code. By forcing specific versions and cipher suites, you can:\n\n- Validate that external APIs meet your security baseline.\n- Discover unintended weak‑protocol support that could be abused.\n- Produce concrete evidence for audits, penetration tests, or vendor discussions.\n\nThe combination of mitmproxy’s extensible addon system and a simple Python driver lets you automate large compatibility matrices and embed the checks into your CI/CD pipeline. Start with the tiny example above, expand the matrix to cover all the ciphers your organization cares about, and make TLS compatibility testing a routine part of your API integration workflow.\n\n---  \n*Ready to try it out? Spin up a mitmproxy instance, drop the addon file, and run a single request—you’ll instantly see which TLS settings your API truly accepts.*\n","https://blog-api.ro-proxy.com/api/blog/posts/testing-api-tls-cipher-suite-proxy-simulation/assets",1792303107154]