← Back to Blog

How Rockxy Intercepts HTTPS Without Compromising Security

· 7 min read

Rockxy uses a standard local certificate-authority model for HTTPS interception on macOS. The root CA is created locally, trust changes require explicit user approval, intercepted traffic stays on your Mac, and the implementation is publicly auditable. Related evidence: privacy policy, open-source philosophy, and the software development lifecycle policy.

Why HTTPS interception is hard

HTTPS exists to prevent exactly what a debugging proxy does. The entire point of TLS is to stop a third party from sitting between a client and server, reading or modifying traffic in transit. When you open https://api.stripe.com in your app, TLS guarantees two things: the data is encrypted, and you're actually talking to Stripe's server and not an impersonator. A debugging proxy violates both guarantees by design.

During a TLS handshake, the server presents a certificate signed by a Certificate Authority (CA) that your operating system already trusts. The client checks three things: the certificate hasn't expired, the hostname in the certificate matches the server it connected to, and the certificate chain leads back to a trusted root CA. If any of those checks fail, the connection is rejected.

A proxy that wants to read HTTPS traffic has to terminate the TLS connection from the client, decrypt the traffic, inspect or modify it, then open a separate TLS connection to the real server and forward the data. To do this, the proxy needs to present a certificate that the client will accept. But the proxy isn't the real server. It doesn't have Stripe's private key. It can't present Stripe's real certificate.

This is fundamentally a trust problem, not a cryptographic one. The crypto is fine. The question is: how do you get the client to trust a certificate that wasn't issued by the real server's CA? The answer involves creating your own CA and asking the operating system to trust it.

The certificate chain approach

When you first launch Rockxy, it generates a root Certificate Authority -- a self-signed X.509 certificate with the CA basic constraint set to true. This root CA is the anchor for all HTTPS interception. Every forged leaf certificate that Rockxy presents to clients will chain back to this root.

The root CA's private key is stored in the macOS Keychain using Security.framework. It is never written to disk as a PEM or DER file. The Keychain encrypts the key at rest using hardware-backed encryption on Macs with a Secure Enclave, and it enforces access control -- only Rockxy's signed binary can retrieve the key. If you uninstall Rockxy, the key is removed with it.

After generating the CA, Rockxy asks you to trust it. This opens the standard macOS trust dialog (the one managed by Security.framework), and you must enter your administrator password to add the CA to the System keychain with "Always Trust" for SSL. Rockxy cannot do this silently. macOS requires explicit user authorization, and that's exactly the right design.

Once the root CA is trusted, Rockxy can intercept HTTPS. For each new connection, it generates a leaf certificate on the fly using swift-certificates, Apple's open-source ASN.1/X.509 library. The leaf certificate includes the target host's Subject Alternative Name (SAN), is signed by the local root CA's private key, and has a short validity period (24 hours). The client sees a certificate chain that looks like this:

Leaf: CN=api.stripe.com, SAN=api.stripe.com
  Issuer: Rockxy Local CA (SHA-256: a1b2c3...)
    Root: Rockxy Local CA [Trusted in System Keychain]

The client validates the chain, finds the root CA in the System keychain, and the handshake succeeds. This is the same approach used by Charles Proxy, Proxyman, and mitmproxy. It's the standard technique for TLS-intercepting proxies because it's the only approach that works without modifying the client application.

Per-host certificates vs. wildcard certificates

Some proxies take a shortcut: they generate a single wildcard certificate (e.g., CN=*) and present it for every domain. This works -- the client sees a trusted cert -- but it has real downsides.

A wildcard cert means every intercepted domain shares the same certificate. You can't revoke trust for a single domain without breaking all of them. You can't implement per-domain SSL proxying rules because the certificate doesn't distinguish between hosts. And if the wildcard cert or its key leaks, an attacker could impersonate any domain for any client that trusts your root CA.

Rockxy generates a unique leaf certificate for every hostname it intercepts. api.github.com gets one certificate. api.stripe.com gets a different one. cdn.example.com gets a third. Each certificate has the correct SAN for its specific host, and each is signed independently by the root CA.

This per-host model enables several things:

  • Domain-level SSL control. You can tell Rockxy to intercept api.myapp.com but pass accounts.google.com through without decryption. The proxy makes this decision before generating a certificate.
  • Better isolation. If you're debugging a specific API, only that API's certificate exists in memory. Other domains' TLS connections are unaffected.
  • Easier auditing. Each intercepted domain has its own certificate with its own serial number. Logs can reference specific certificates for specific hosts.

To avoid the performance cost of generating a new certificate for every single request, Rockxy caches generated certificates in an in-memory dictionary keyed by hostname. The cache lives for the duration of the proxy session. When Rockxy restarts, the cache is cleared and certificates are regenerated on demand. No generated leaf certificates are persisted to disk.

How the MITM proxy flow works, step by step

Here's what actually happens when your app makes an HTTPS request through Rockxy. This is the real sequence, not a simplification.

Step 1: The client sends an HTTP CONNECT request. When your app is configured to use Rockxy as an HTTP proxy (either via system proxy settings or explicit proxy configuration), HTTPS requests start with a CONNECT method. The request looks like this:

CONNECT api.stripe.com:443 HTTP/1.1
Host: api.stripe.com:443

Step 2: Rockxy accepts the tunnel. Rockxy responds with HTTP/1.1 200 Connection Established. At this point, the client thinks it has a raw TCP tunnel to api.stripe.com:443. It doesn't -- it has a tunnel to Rockxy.

Step 3: Rockxy connects to the upstream server. In parallel, Rockxy opens its own TLS connection to the real api.stripe.com:443. It performs a full TLS handshake with Stripe's server and validates the real certificate. This is important -- Rockxy verifies the upstream server's identity just like a normal client would. If the upstream certificate is invalid, Rockxy flags it.

Step 4: Rockxy reads the server's certificate. From the upstream TLS handshake, Rockxy extracts the server's hostname from the certificate's SAN extension. This ensures the generated leaf certificate matches exactly what the client expects.

Step 5: Rockxy generates a leaf certificate. Using swift-certificates, Rockxy creates a new X.509 certificate with the SAN set to api.stripe.com, signs it with the local root CA's private key (retrieved from the Keychain), and sets a 24-hour expiry.

Step 6: Rockxy performs a TLS handshake with the client. Using the freshly generated leaf certificate, Rockxy initiates a TLS handshake with the client on the tunnel connection. The client validates the certificate chain: leaf cert signed by Rockxy's root CA, root CA is in the System keychain with "Always Trust" -- handshake succeeds.

Step 7: Plaintext access. Rockxy now holds two separate TLS sessions: one with the client, one with the upstream server. Data from the client is decrypted, inspected (headers, body, timing), and then re-encrypted and forwarded to the upstream server. Responses follow the same path in reverse.

Step 8: Traffic inspection and rules. With plaintext access to the HTTP layer, Rockxy can apply its full rules engine: breakpoints to pause and edit requests mid-flight, Map Local to serve responses from disk, Map Remote to redirect requests to a different server, header modification, throttling, and response body rewriting. The rules operate on decrypted HTTP, not on TLS records.

Step 9: Connection teardown. When either side closes the connection, Rockxy tears down both TLS sessions and logs the completed request/response pair to the session store.

What about certificate pinning?

Certificate pinning is a technique where an application hardcodes the expected certificate -- or its public key hash -- and refuses to connect if the server presents anything else. Banking apps, payment SDKs, and some Apple system services use pinning. It's a defense against exactly the kind of interception a debugging proxy performs.

Rockxy cannot intercept pinned connections. When a pinned app sees Rockxy's generated certificate instead of the expected one, the TLS handshake fails. The app rejects the connection. This is correct behavior -- pinning exists to prevent interception, and Rockxy respects that boundary.

In Rockxy's traffic list, pinned connections show up as failed TLS handshakes with a clear "Certificate Pinning Detected" indicator. The connection entry shows the hostname, the timestamp, and the failure reason, but no decrypted content (because there is none).

For pinned domains that you don't need to inspect, Rockxy provides an SSL Bypass List. Domains on this list are passed through without interception -- Rockxy acts as a plain TCP tunnel, forwarding encrypted bytes without touching them. The client connects directly to the real server through the tunnel, and the pinning check succeeds because the real certificate is presented.

Common domains that users add to the bypass list include:

  • *.apple.com -- Apple system services with aggressive pinning
  • *.icloud.com -- iCloud sync traffic
  • *.googleapis.com -- some Google SDKs pin their certificates
  • Banking and payment app domains

This is the same approach that Charles Proxy and Proxyman use. There is no way to bypass certificate pinning from a proxy without modifying the client application itself (which would require disabling the pinning checks in the app's code or using a tool like ssl-kill-switch on jailbroken devices). Rockxy doesn't go there.

Security boundaries we chose not to cross

Building a MITM proxy means making deliberate choices about where to stop. Here are the security boundaries we set in Rockxy and why.

The root CA private key never leaves the Keychain. It's generated inside the Keychain, stored inside the Keychain, and used for signing operations through Security.framework APIs that never expose the raw key bytes to user-space memory. On Macs with a Secure Enclave, the key material is hardware-protected. Exporting the key requires explicit user action through Keychain Access -- Rockxy provides no mechanism to export it programmatically.

No silent trust modification. Rockxy cannot add its root CA to the System keychain without the macOS trust dialog and your admin password. We don't use security add-trusted-cert in a shell script. We don't suppress the dialog. The first time you install the CA, you know exactly what's happening because macOS tells you.

XPC privilege validation. Rockxy's privileged helper (the component that modifies system proxy settings) runs as a launchd daemon. Every incoming XPC connection is validated by comparing the caller's code signing certificate chain against Rockxy's known signing identity. An unsigned or differently-signed process cannot communicate with the helper. This prevents other applications from hijacking the privileged helper to modify proxy settings.

Zero network calls from Rockxy itself. Rockxy makes no outbound network requests. No telemetry. No analytics. No update checks (updates are handled through the standard macOS Sparkle framework with user-initiated checks). No license validation server. Your traffic data never leaves your machine. If you run Rockxy with a network monitor pointed at Rockxy's own process, you'll see zero connections initiated by the app.

Full source availability. The entire TLS interception implementation is in the open source repository. The certificate generation code, the proxy engine, the Keychain integration, the XPC validation -- all of it is readable. If you're uncomfortable with any of the trust decisions described in this post, you can verify them line by line.

Read the source

The TLS interception code lives in the proxy engine module of the Rockxy repository. The certificate generation logic, the CONNECT handler, the upstream TLS client, and the per-host certificate cache are all there.

If you find a concern -- a key that's exposed where it shouldn't be, a validation step that's missing, a trust boundary that's weaker than described here -- open an issue. Security issues get priority treatment. We'd rather know about a problem from a reader than discover it in production.

HTTPS interception is a trust-sensitive operation. The least we can do is let you read the code that does it.