← All Posts

Decrypting HTTPS in Wireshark

Blue Team
Decrypting HTTPS in Wireshark — cover

A hands-on lab covering both TLS decryption methods in Wireshark — the private key method for non-PFS (RSA) sessions, and SSLKEYLOGFILE for PFS (DHE/ECDHE) sessions — with real capture examples for each.

Much of the conceptual framing here draws on Sally Vandeven's SANS GIAC Gold paper on network forensics and HTTP/2: giac.org/paper/gcia/10847. Labs 5.2 and 5.4 in that paper cover decrypting forward secrecy and exploring HTTP/2 — the labs below are my own hands-on version of those concepts.

§ 1 Foundations

1.1 HTTP vs HTTPS — The Basics

HTTP (Hypertext Transfer Protocol) is one of the most common network protocols on the internet, most commonly used to deliver web content. At its core it's an ASCII-based protocol, originally designed to deliver text and markup documents. It's a stateless protocol and runs on TCP port 80.

Protocol history:

  • HTTP/1.1 — 1997, still the most common baseline today
  • HTTP/2 — 2015, binary and multiplexed, almost always run over TLS, uses HPACK header compression
  • HTTP/3 — runs over QUIC instead of TCP, transported over UDP, requires TLS, uses QPACK header compression
HTTP/1.1 client-server timing diagram
Figure 1 — HTTP/1.1 client–server timing: SYN/SYN-ACK/ACK handshake followed by sequential GET/RESPONSE pairs, ending with connection close.

What HTTPS adds (TLS layer):

HTTPS is what encrypts the traffic — plain HTTP is unencrypted by default. TLS itself is just a wrapper that sits over other protocols; it doesn't change what's being transported, just how it's protected in transit.

Why this matters for packet capture analysis:

  • REST API activity inspection
  • Exfiltration and infiltration attempts
  • User web tracking
  • File upload/download activity
  • Beaconing (C2, malware check-ins)

1.2 TLS Fundamentals

TLS handles three things: the handshake, encryption of the data, and integrity checking. TLS has gone through several versions (1.0 → 1.3), and the version matters directly for decryption capability.

CRITICAL: TLS 1.3 removed RSA key exchange entirely. This means non-PFS decryption via private key is impossible on TLS 1.3 captures — the private-key method only works against TLS 1.2 (or earlier) sessions negotiated with an RSA cipher suite.

The TLS handshake at a high level:

  • Client Hello — the client sends supported cipher suites, compression algorithms, and TLS extensions.
  • Server Hello — the server picks the best match from what the client offered.
  • Certificate exchange — the server sends its certificate (public key, signed by a trusted CA).
  • Client verification and key exchange — the client checks the cert and establishes the session key:
    • With RSA key exchange, the client encrypts a pre-master secret using the server's RSA public key — the server's private key alone can later decrypt it.
    • With DHE/ECDHE key exchange, both sides derive the same shared secret via Diffie-Hellman math. The server's long-term private key only signs the exchange — it cannot retroactively decrypt sessions. This is what makes it PFS.

1.3 HTTP Versions — Why It Matters for Wireshark

HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) all show up differently in a packet capture, and that affects which Wireshark filters and dissectors you need.

KEY POINT: HTTP/3 runs over UDP, not TCP — this changes both how you capture traffic and what Wireshark shows you by default. Modern browsers (Firefox, Chrome) will silently upgrade to HTTP/3/QUIC whenever the server advertises support via Alt-Svc, which is why a capture of "just browsing Google" can be almost entirely QUIC packets instead of the HTTP/2-over-TCP you might expect.

1.4 Encryption Standards

Key Exchange / Asymmetric (Handshake Layer)

Algorithm PFS Notes
RSA No Private key = decryption key. Removed in TLS 1.3.
DHE Yes Ephemeral Diffie-Hellman, older PFS standard.
ECDHE Yes Elliptic curve variant of DHE, modern standard.

Bulk Encryption (Data Layer)

Algorithm Notes
AES-128/256-GCM Most common — what actually encrypts the payload data.
ChaCha20-Poly1305 Common on mobile/low-power devices, pushed by Google.

Integrity / Hashing

Algorithm Notes
SHA-256 / SHA-384 Seen in every cipher suite string, ensures data integrity.

1.5 Cipher Suite Breakdown

A cipher suite string tells you everything about how a session was negotiated. Example:

TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
 TLS   ECDHE   RSA    AES_256_GCM   SHA384
        |       |          |           |
      Key     Auth       Bulk        Hash/
    Exchange           Encryption    MAC

Reading it left to right tells you the key exchange method (and therefore whether the session is PFS), the authentication method, the bulk encryption cipher, and the hash/MAC algorithm.

1.6 PFS vs Non-PFS — The Fork in the Road

Perfect Forward Secrecy (PFS) means a random ephemeral key is generated per session, so that even if the server's long-term private key is later stolen, past sessions can't be retroactively decrypted.

  • Non-PFS (RSA): the session key is encrypted with the server's public key → the private key decrypts it → Wireshark can replay this decryption at any time on any old capture, as long as you have that one private key.
  • PFS (DHE/ECDHE): ephemeral keys are never stored anywhere → the private key alone is useless for decryption → you need the session secrets logged at runtime instead.

All PFS standards use the exact same decryption method in Wireshark — it doesn't care whether the session used DHE or ECDHE, session secrets are session secrets. The only real fork in the road is RSA (private key) vs anything PFS (SSLKEYLOGFILE).

  • Non-PFS needs something from the server — which you'll rarely have unless you control the server yourself.
  • PFS needs something from the client — which you can always set up yourself, since you control your own browser/endpoint.

This is what makes SSLKEYLOGFILE the only practical method for client-side captures, regardless of whether PFS is in play — in the real world, you almost never have someone else's private key, but you always have access to your own client.

1.7 Decryption Method Mapping

Standard PFS Decryption Method What You Need
RSA No Private key in Wireshark Server .pem / .key
DHE Yes SSLKEYLOGFILE Initial handshake + rekey secrets
ECDHE Yes SSLKEYLOGFILE Initial handshake + rekey secrets
REAL-WORLD LIMITATION: Private key decryption only works if you own or control the server. If your client is talking to a public web server, you will never have their private key.
§ 2 Prerequisites

2.1 Non-PFS — Getting the Server Private Key

If you're running your own web server (lab environment), the private key was generated when you created your certificate — typically a .pem or .key file.

Common locations depending on your stack:

  • Apache: /etc/ssl/private/
  • Nginx: wherever you pointed ssl_certificate_key in your config
  • Let's Encrypt: /etc/letsencrypt/live/<domain>/privkey.pem

Format Wireshark expects:

  • RSA private key in PEM format (-----BEGIN RSA PRIVATE KEY-----)
  • If yours is in PKCS#12 / .pfx format, convert it first:
openssl pkcs12 -in cert.pfx -nocerts -nodes -out privatekey.pem

2.2 PFS — Setting Up SSLKEYLOGFILE

Linux

The most common failure is setting SSLKEYLOGFILE in a terminal that isn't the same process tree Firefox launches from (e.g., setting it in one terminal, then launching Firefox from your GUI app launcher instead of that terminal). Don't do this — launch Firefox from the same terminal session where you set the variable.

export SSLKEYLOGFILE=~/sslkeylog.log
Terminal showing SSLKEYLOGFILE exported and keylog output
Figure 2 — Terminal session showing SSLKEYLOGFILE exported, followed by cat keylog.txt confirming the browser is writing keys (CLIENT_TRAFFIC_SECRET_0 entry visible).

To make it persistent (for future shell launches), add it to ~/.bashrc or ~/.profile:

echo 'export SSLKEYLOGFILE=$HOME/sslkeys/keylog.txt' >> ~/.bashrc
source ~/.bashrc

Note: .bashrc only applies to interactive bash shells — if Firefox is launched via a .desktop file or the GNOME app launcher, it may not go through bash at all.

For a foolproof method, create ~/.config/environment.d/envvars.conf for systemd-managed sessions:

# ~/.config/environment.d/envvars.conf
SSLKEYLOGFILE=/home/YOURUSER/sslkeys/keylog.txt

Then log out/in, or reboot.

Windows (10 and 11)

Command line:

setx SSLKEYLOGFILE "C:\Users\<user>\sslkeylog.log"

Note: setx writes to the registry — open a new terminal after running it for the change to take effect.

Or through the GUI (persists across reboots, recommended):

  • Right-click Start → System (or Win+X → System)
  • Click Advanced system settings
  • Advanced tab → Environment Variables
  • Under User variables, click New
  • Variable name: SSLKEYLOGFILE
  • Variable value: C:\Users\<YourUsername>\Desktop\sslkeys\keylog.txt
  • OK → OK → OK

Then reboot.

Windows Environment Variables dialog with SSLKEYLOGFILE added
Figure 3 — Windows Environment Variables dialog with SSLKEYLOGFILE added under User variables.

Rules that apply on both platforms:

  • The browser must be launched after the variable is set, and from the same shell session (or login session for the persistent methods).
  • Supported browsers: Chrome, Firefox, Edge — all honor SSLKEYLOGFILE natively.
  • Do not use a browser that's already open — it won't pick up the variable retroactively.

Step 3 — Generate your traffic: browse to your target and trigger the HTTPS traffic you want to capture. The log file should populate immediately on the first connection.

Step 4 — Verify the keylog file:

cat ~/sslkeylog.log

You should see lines like:

CLIENT_RANDOM <hex> <hex>
CLIENT_HANDSHAKE_TRAFFIC_SECRET <hex> <hex>
SERVER_HANDSHAKE_TRAFFIC_SECRET <hex> <hex>

If the file is empty or missing, the browser didn't pick up the variable — go back to Step 1.

WARNING: If SSLKEYLOGFILE wasn't running at the start of the session, decryption will fail. The initial handshake secrets are derived once — if they weren't logged at that exact moment, they're gone for good. Always have logging running before you open the browser or trigger traffic.

2.3 Scenario Artifacts

For this lab, the goal was to generate a realistic, varied set of artifacts to hunt for in the resulting pcaps:

  • Google.com visit
  • Search for "AnyDesk RMM tool"
  • Visit the AnyDesk website
  • Attempt to download the tool
§ 3 Decryption: Non-PFS (Private Key)

3.1 Minimal Lab Setup

Generate a self-signed cert + RSA key pair:

mkdir ~/tlslab && cd ~/tlslab
openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt \
  -days 365 -nodes -subj "/CN=tlslab.local"

This gives you server.key (the private key you'll feed into Wireshark) and server.crt.

Stand up a minimal TLS server forcing RSA key exchange (non-PFS):

openssl s_server is perfect for this — lightweight, no real service to configure, and lets you explicitly pin the cipher suite.

openssl s_server -accept 4433 -cert server.crt -key server.key \
  -cipher "AES128-SHA" -tls1_2 -www

Breaking this down:

  • -cipher "AES128-SHA" — a plain RSA key exchange cipher suite (no ECDHE), exactly the non-PFS behavior we want.
  • -tls1_2 — forces TLS 1.2, since that's the last version where non-PFS RSA key exchange is even possible. TLS 1.3 removed it entirely.
  • -www — makes the server respond to simple HTTP-ish requests with debug info, so there's something to see in the decrypted payload.
NOTE: -tls1_2 and -no_tls1_3 are two ways of expressing the same restriction — OpenSSL will reject you if you pass both together. Pick one; -tls1_2 is the more explicit/deterministic choice for a repeatable lab.

3.2 Injecting a Visible Marker

Since s_client lets you type raw text into the TLS session once connected, and s_server -www will echo back request info, the simplest way to prove decryption is to include a unique flag string as part of the HTTP request:

openssl s_client -connect localhost:4433 -cipher "AES128-SHA" -tls1_2

Once connected, type:

GET /TLS-LAB-FLAG-DECRYPT-ME HTTP/1.1
Host: tlslab.local

(hit Enter twice — HTTP requests need a blank line to terminate headers)

The string TLS-LAB-FLAG-92f3a7-DECRYPT-ME is now sitting inside the encrypted TLS Application Data record. Before decryption, Wireshark shows this as garbage ciphertext. After loading the RSA key, the string appears plainly in the decrypted HTTP tab.

3.3 Loading the Key into Wireshark

  • Open Wireshark and load your .pcap
  • Navigate to Edit → Preferences → Protocols → TLS
  • Add the private key under RSA keys list (IP, port, protocol, key file path)
IP address Port Protocol Key File
127.0.0.1 4433 http /path/to/tlslab/server.key
Wireshark TLS RSA Keys List dialog with server private key loaded
Figure 4 — Wireshark TLS preferences — RSA Keys List dialog with the server private key loaded, mapped to 127.0.0.1:4433 over http.
  • Apply and observe decrypted traffic
  • Verify it worked: the HTTP layer should now be visible in packet details — look for status codes, URIs, headers

3.4 Before / After

Before decryption — a TLS packet, but nothing readable:

9  0.005010134 127.0.0.1 -> 127.0.0.1 TLSv1.2  336 New Session Ticket, Change Cipher Spec
10 0.045451193 127.0.0.1 -> 127.0.0.1 TCP        66 44610 -> 4433 [ACK]
11 26.012479536 127.0.0.1 -> 127.0.0.1 TLSv1.2  171 [TLS segment of a reassembled PDU]
12 26.012589111 127.0.0.1 -> 127.0.0.1 TLSv1.2 2027 [TLS segment of a reassembled PDU]
Wireshark hex/ASCII pane — pure ciphertext before decryption
Figure 5 — Wireshark hex/ASCII pane for the TLS Application Data packet — pure ciphertext, no readable text anywhere in the frame.

After decryption — the same packet, now with a Decrypted TLS section. Selecting that tab reveals the actual request:

Frame (171 bytes) | Decrypted TLS (56 bytes)

47 45 54 20 2f 54 4c 53  4c 41 42 2d 46 4c 41 47   GET /TLS-LAB-FLAG
2d 44 45 43 52 59 50 54  2d 4d 45 20 48 54 54 50   -DECRYPT-ME HTTP
2f 31 2e 31 20 48 6f 73  74 3a 20 74 6c 73 6c 61   /1.1 Host: tlsla
62 2e 6c 6f 63 61 6c 0a                             b.local
Wireshark Decrypted TLS tab showing flag string in plaintext
Figure 6 — Wireshark packet detail pane showing the new "Decrypted TLS (56 bytes)" tab, with the flag string visible in plaintext in the ASCII column.

3.5 If It Didn't Work — Common Reasons

  • Wrong key, or a key mismatch
  • Capture was TLS 1.3 — the private key method is not supported on TLS 1.3 (RSA key exchange doesn't exist in 1.3)
  • Key is not in PEM format
§ 4 Decryption: PFS (SSLKEYLOGFILE)
Lab setup: Unlike Section 3, you don't need to spin up your own server here. Just make sure your browser is configured to write to SSLKEYLOGFILE (see Section 2.2), start a Wireshark capture on your active network interface, then browse normally — any HTTPS site will do. Stop the capture, save the pcap, and follow the steps below to decrypt it.

4.1 Loading the Keylog into Wireshark

  • Open Wireshark and load your .pcap
  • Navigate to Edit → Preferences → Protocols → TLS
  • Point Wireshark at your keylog file in the (Pre)-Master-Secret log filename field
  • Apply and observe decrypted traffic
  • Verify it worked: HTTP layer visible, status codes, URIs, and headers readable in packet details
Wireshark TLS preferences with SSLKEYLOGFILE path set
Figure 7 — Wireshark TLS preferences pane with the (Pre)-Master-Secret log filename field pointed at the local keylog.txt file.

4.2 Before / After

Before decryption — protocol hierarchy shows HTTP traffic, but no HTTP/2 or HTTP/3, which is unusual for a modern browser hitting a modern website:

Wireshark Protocol Hierarchy Statistics — pre-decryption, no HTTP/2 visible
Figure 8 — Protocol Hierarchy Statistics (pre-decryption): Frame → Ethernet → IPv4 → TCP → TLS → HTTP at ~0.9% of packets, with no HTTP/2 or QUIC entries visible.
FORENSICS TIP: Even without decryption, unencrypted native protocols can still be used to profile user traffic. DNS is a good example — filtering for DNS traffic alone reveals which sites were visited, even though the search content itself is still encrypted.
Wireshark filtered on DNS — anydesk.com and google hostnames visible
Figure 9 — Wireshark packet list filtered on dns, showing queries for pagead2.googlesyndication.com and anydesk.com — hostnames visible via plaintext DNS resolution even before any TLS decryption.

After decryption — protocols previously hidden inside TLS are now visible, including HTTP/2:

Wireshark Protocol Hierarchy Statistics — post-decryption, HTTP/2 now visible
Figure 10 — Protocol Hierarchy Statistics (post-decryption): the TCP/TLS branch now shows HyperText Transfer Protocol 2 at ~1.9% of packets — confirming HTTP/2 traffic was present but hidden until decryption.

If we follow the TCP stream, we can even see the visit to download the AnyDesk app:

Follow TCP Stream — decrypted session to download.anydesk.com
Figure 11 — Wireshark "Follow TCP Stream" for the decrypted session to download.anydesk.com, showing readable HTTP/2 headers — the SNI and "h2 http/1.1" negotiation are visible in the reconstructed stream.

4.3 A Note on QUIC / HTTP/3

This same SSLKEYLOGFILE method also decrypts QUIC/HTTP-3 traffic — Firefox writes QUIC secrets to the identical keylog file, since QUIC uses the same TLS 1.3 stack under the hood, just tunneled over UDP instead of TCP. A Google search performed with HTTP/3 enabled will show up as a wall of QUIC packets rather than HTTP/2, and pointing Wireshark at the same keylog file decrypts those sessions the same way.

4.4 If It Didn't Work — Common Reasons

  • Keylog file didn't cover the session start
  • Browser wasn't launched from the correct shell session after setting the variable
  • Keylog file and pcap don't cover the same session
SHARED REMINDER FOR BOTH METHODS: Your .pcap and your keylog file or private key must cover the same session. If you captured traffic without your prereqs ready, decryption will fail. Always have everything running before you generate traffic.