DEV Community

Noufal Ibrahim
Noufal Ibrahim

Posted on

HTTPS and HTTP

TL;DR

  • HTTP is plain text. Anyone on the path can read and change the URL, the cookies and the page.
  • HTTPS encrypts all of that, and a certificate signed by a trusted CA proves you reached the right server.
  • The server name, the IP address and the DNS lookup still travel in the clear, and the trust is only as good as the CAs on the list.

Given the recent certificate scandal with eMudhra, HTTPS is in the news. This has some special significance to a domain blocker like glocker that runs on your own machine. Let's discuss what HTTPS is in this article. In part 2, we'll talk about the significance for glocker.
Let's walk through what happens using a few basic commands on the shell. nc (netcat) opens a raw network connection and passes along whatever you type, adding nothing of its own, so it lets us play the part of the browser or the server by hand. curl is a command-line client that builds requests the way a browser does and prints what comes back. Later on, openssl handles the encrypted part. So, shut down your agents, open up a terminal and follow along.

Plain HTTP

HTTP is plain text, so you can type it by hand. Connect to example.com on port 80, the HTTP port, and you're talking directly to its web server.

Let's play the part of the browser. Start nc as a client and point it at example.com's port 80.

Type the two lines below, then press Enter once more on an empty line.

$ nc example.com 80
GET / HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Date: Wed, 11 Nov 2026 08:34:16 GMT
Content-Type: text/html; charset=utf-8
Server: cloudflare
...

<!doctype html><html lang=en><head><meta charset=utf-8>...<title>Example Domain</title>
Enter fullscreen mode Exit fullscreen mode

That's the whole protocol. The first line says what you want, the Host header says which site, the empty line ends the request, and the page comes back. The server keeps the connection open for another request, so press Ctrl-C to leave.

Now, let's play the part of the server and start nc in listening mode using -l. We'll make it listen on port 8080 on our own machine and point an actual HTTP client at it. We'll use curl. Try the commands below.

Run nc in listening mode and point curl at it. --resolve tells curl that shop.example lives at 127.0.0.1, so it connects to our listener while thinking it's talking to a shop:

$ nc -l 127.0.0.1 8080
Enter fullscreen mode Exit fullscreen mode
$ curl -b 'session=4f9a2c' 'http://shop.example:8080/account/orders?card=last4' \
       --resolve shop.example:8080:127.0.0.1
Enter fullscreen mode Exit fullscreen mode

The listener prints:

GET /account/orders?card=last4 HTTP/1.1
Host: shop.example:8080
User-Agent: curl/8.14.1
Accept: */*
Cookie: session=4f9a2c
Enter fullscreen mode Exit fullscreen mode

An HTTP request is like a postcard. It passes through many hands on its way, and anyone who handles it can read what's written on it, or rewrite it before passing it on.

Over HTTP the request travels readable through the wifi and every network in between, and any of them can change it on the way.
Plain HTTP

The page you asked for, the query string and the session cookie are all there in plain text. The listener here is on your own machine, but your wifi, your ISP and every network in between see exactly the same bytes. They can also change the response on its way back to you.

Now, imagine you're in a hotel or an untrusted network. You would typically access sites through a proxy server. In theory, these should just route your traffic and the responses but that's a matter of trusting them. A malicious proxy could alter the contents returned by a site to trick you. Here's what a setup would look like.

The browser sends its request to a proxy, which keeps a copy and forwards it to example.com. The reply comes back through the proxy, which changes Example Domain to Changed Domain before the browser sees it.

A man in the middle

These are called man in the middle (or MITM) attacks. We can simulate this by making one instance of nc behave like a proxy. It will listen on port 8080 and forward everything to example.com while keeping a copy in request.txt. On the way back it runs the reply through sed, which swaps one phrase for another. reply is a named pipe that carries the answer back to the listener:

$ mkfifo reply
$ nc -l 127.0.0.1 8080 < reply | tee request.txt | nc example.com 80 \
    | sed -u 's/Example Domain/Changed Domain/' > reply
Enter fullscreen mode Exit fullscreen mode

In a second terminal, ask curl to use it as a proxy, the way a browser on a company or hotel network often does:

$ curl -s -x http://127.0.0.1:8080 http://example.com/
<!doctype html><html lang=en><head>...<title>Changed Domain</title>...
$ cat request.txt
GET http://example.com/ HTTP/1.1
Host: example.com
User-Agent: curl/8.14.1
Accept: */*
Proxy-Connection: Keep-Alive
Enter fullscreen mode Exit fullscreen mode

The proxy saw the whole request, and the page curl received is not the one example.com sent. Nothing in the reply shows it was changed. The replacement here is the same length as the original so the byte counts in the response still add up; a real attacker would take care of that too.

When HTTP was designed at CERN in 1990–91, none of this mattered much. The web was a way for physicists to share documents. The network was mostly universities and research labs, there were no logins, payments or private pages, and nobody had much reason to tamper with the traffic. That changed once people started shopping, banking and logging in on the web, and once traffic began passing through networks you don't control, like coffee-shop wifi. Anyone along the way could read passwords and session cookies. In 2010 a Firefox add-on called Firesheep showed this to everyone: it let anyone on a shared wifi network take over other people's Facebook and Twitter sessions with a click. Networks also changed what they carried. Some ISPs inserted their own ads and notices into pages. And nothing told you whether the reply really came from the site you asked for. Netscape added SSL to its browser in the mid-1990s so people could shop safely online, and TLS replaced SSL in 1999.

HTTPS

To solve this snooping problem, HTTPS was invented. It has two jobs.

The first is encryption: scrambling the data so that only someone with the right key can unscramble it. The problem is that the browser and the server have never met, so they have no key in common, and anything they say to agree on one can be overheard. This is a larger topic, but the gist of it is that each side keeps a secret of its own and sends the other a value made from it. By combining its own secret with what the other side sent, each side arrives at the same key. Someone who overheard both values can't, because they don't have either secret. This is called a key exchange.

The second is identity. An encrypted connection is no use if it's to the wrong server. So the server shows a certificate, which works like an ID card: it says "this server is example.com". Anyone can print a card like that, so someone trusted has to vouch for it. A certificate authority (CA) is an organisation that checks that whoever asks for a certificate really controls the website, and then signs it. Only the CA can make its signature, because it needs a secret called the CA's private key, but anyone can check it. Your operating system comes with a list of CAs it trusts, and your browser accepts a certificate signed by any of them.

TLS (Transport Layer Security) is the set of rules for doing both jobs, and HTTPS is plain HTTP sent through a TLS connection. A TLS connection starts with a handshake, an exchange of messages in which the two sides agree on how to encrypt, do the key exchange, and check the certificate. No HTTP is sent until the handshake is done. The browser opens the handshake with a message called the ClientHello.

The same request over HTTPS

Now do the same with HTTPS. Start the listener on another port, save what arrives, and point curl at it:

$ nc -l 127.0.0.1 8443 > capture.bin
Enter fullscreen mode Exit fullscreen mode

Unlike the earlier example, we're dumping our output into a file so that we can look at it later. It will shortly become obvious why.

$ curl -b 'session=4f9a2c' 'https://shop.example:8443/account/orders?card=last4' \
       --resolve shop.example:8443:127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Notice the s in the scheme: https instead of http. --resolve works as before: curl believes it is connecting to shop.example on port 8443, but the connection goes to our listener on 127.0.0.1, and curl still puts shop.example in the ClientHello.

nc doesn't speak TLS, so curl sends its ClientHello and waits for a reply that never comes. capture.bin is not even a text file. Here's what it looks like.

$ xxd capture.bin | head -14
00000000: 1603 0106 1f01 0006 1b03 03a2 7655 9139  ............vU.9
00000010: 30e7 ee9a 68ac c99a 9f7f f045 ab30 ce1d  0...h......E.0..
...
00000090: 0001 0000 0000 1100 0f00 000c 7368 6f70  ............shop
000000a0: 2e65 7861 6d70 6c65 000b 0004 0300 0102  .example........
000000b0: 000a 0012 0010 11ec 001d 0017 001e 0018  ................
000000c0: 0019 0100 0101 0010 000e 000c 0268 3208  .............h2.
000000d0: 6874 7470 2f31 2e31 0016 0000 0017 0000  http/1.1........
Enter fullscreen mode Exit fullscreen mode

We can also do a crude inspection to see if there's anything readable inside this file.

$ strings -n 6 capture.bin
shop.example
http/1.1
...
Enter fullscreen mode Exit fullscreen mode

What we captured is curl's ClientHello. Here's a breakdown:

Bytes Field What curl sent
0–4 Record header 16 03 01 06 1f: a handshake record, 1,567 bytes follow
5–8 Handshake header 01 00 06 1b: type 1, ClientHello, 1,563 bytes
9–10 Version 03 03, TLS 1.2. Kept for old middleboxes; the real versions are in an extension below
11–42 Random 32 random bytes
43–75 Session ID 32 more bytes, also for compatibility
76–137 Cipher suites 30 encryption methods curl accepts, best first: 13 02 is TLS_AES_256_GCM_SHA384
138–139 Compression none
140 onwards Extensions 1,430 bytes, the interesting part:
server_name (SNI) shop.example, in plain text
supported_groups the key exchange methods: X25519MLKEM768, X25519, P-256 and others
ALPN h2, http/1.1: what curl can speak once the connection is up
signature_algorithms which signatures curl can check on the certificate
supported_versions TLS 1.3 and TLS 1.2
key_share 1,258 bytes: curl's half of the key exchange (1,216 bytes for X25519MLKEM768, 32 for X25519)

This is the ClientHello, the first message of a TLS connection. 16 marks a handshake record and the 01 a few bytes later says it's a ClientHello. The rest is a list of what curl supports: TLS versions, ciphers, and h2 and http/1.1 as the protocols it can speak afterwards. Most of the 1,572 bytes are curl's half of the key exchange, which is why it looks like noise.

The path, the query string and the cookie aren't in there at all. They haven't been encrypted yet. They haven't been sent. Over HTTPS, the browser doesn't send the request until the handshake has finished and both sides share a key.

One thing is readable, though: shop.example. That's the SNI field, short for Server Name Indication. One IP address often serves many websites, and the server has to know which one you want before it can show the right certificate, so the name goes out before anything is encrypted. Keep it in mind. It comes up again in part 2.

Doing the handshake properly

nc can't talk to an HTTPS server, because the server expects a TLS handshake before anything else. openssl s_client is a TLS client for the terminal. It connects, does the handshake, checks the certificate and prints a short summary. After that it behaves like nc: what you type is encrypted and sent, and what comes back is decrypted and printed. -servername sets the SNI field, and -brief keeps the summary short.

Connect, wait for the summary, then type the same two lines as before and an empty line:

$ openssl s_client -connect example.com:443 -servername example.com -brief
Connecting to 104.20.23.154
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=example.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
GET / HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
...
Enter fullscreen mode Exit fullscreen mode

The request is identical to the one we sent with nc. The only difference is the layer underneath. Verification: OK means the certificate checked out.

curl -v shows the steps of the handshake as they happen:

* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
> GET / HTTP/2
Enter fullscreen mode Exit fullscreen mode

Sequence diagram: the browser says hello and names the site, the server replies with its certificate, the browser checks it, both sides arrive at a shared key, and everything after that is encrypted.

How an HTTPS connection starts

The ClientHello carries curl's half of the key exchange, and the server's reply, the ServerHello, carries the other half. From the two, both sides calculate the same key, as described earlier. There are several mathematical methods for this. The Negotiated TLS1.3 group line from s_client names the one used here, X25519MLKEM768, which is two methods combined: an older one called X25519 and a newer one called ML-KEM. ML-KEM is designed to stay safe against quantum computers, which could one day break the older methods. Its values are much larger, which is why curl's half took up most of those 1,572 bytes.

Everything after the Server hello is encrypted, including the server's certificate. > GET / only appears at the very end, after both sides have sent Finished. (curl picked HTTP/2 here, using the h2 it offered in the ClientHello.)

The certificate says who you're talking to

Encryption on its own isn't enough. If someone gets your traffic to the wrong server, through a fake wifi network or a poisoned DNS answer, you'd have a perfectly encrypted connection to the wrong party. The certificate is what prevents that. Here's example.com's:

$ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
subject=CN=example.com
issuer=C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
notBefore=Sep 26 22:49:11 2026 GMT
notAfter=Dec 25 22:56:35 2026 GMT
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:*.example.com
Enter fullscreen mode Exit fullscreen mode

It names the sites it covers, it expires in 90 days, and it was signed by an issuer, Cloudflare TLS Issuing ECC CA 3. That issuer isn't on your system's list of trusted CAs. A CA's most valuable key is the one behind the certificate on that list, called its root. CAs keep it locked away offline and use it only to sign certificates for a few intermediate CAs, which do the day-to-day signing. So the server sends its own certificate along with the intermediate ones, and the browser follows the signatures from one to the next until it reaches a root it trusts. This is the chain. -showcerts prints it, with each certificate's subject (s:) and issuer (i:):

$ openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
 0 s:CN=example.com
   i:C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
 1 s:C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
   i:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
 2 s:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
   i:C=US, O=SSL Corporation, CN=SSL.com TLS ECC Root CA 2022
 3 s:C=US, O=SSL Corporation, CN=SSL.com TLS ECC Root CA 2022
   i:C=GB, ST=Greater Manchester, L=Salford, O=Comodo CA Limited, CN=AAA Certificate Services
Verify return code: 0 (ok)
Enter fullscreen mode Exit fullscreen mode

Certificate 2 was issued by SSL.com TLS ECC Root CA 2022, which is on the list, so the chain is trusted. (The server also sends that root signed by an older Comodo root, for devices whose list is too old to have the new one.) On Debian, the list is a directory of files:

$ ls /etc/ssl/certs/*.pem | wc -l
164
$ ls /etc/ssl/certs | grep -i ssl.com_tls
SSL.com_TLS_ECC_Root_CA_2022.pem
SSL.com_TLS_RSA_Root_CA_2022.pem
Enter fullscreen mode Exit fullscreen mode

Firefox ships its own list, and Chrome uses its own root store. Either way, the check is the same. The name must match, the dates must be valid, every signature up the chain must verify, and the top must be on the list. Finally, the server has to sign the handshake with the private key that matches its certificate. That's the CERT verify step above. The certificate itself is public, so anyone can copy it. Only the real server holds the key.

Watching a counterfeit fail

Let's build a counterfeit example.com. Anyone can make a certificate that says example.com:

$ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
    -keyout fake.key -out fake.crt -days 30 \
    -subj /CN=example.com -addext subjectAltName=DNS:example.com
$ openssl s_server -accept 8443 -cert fake.crt -key fake.key
Enter fullscreen mode Exit fullscreen mode

-newkey makes the private key, which is just a random 256-bit number saved in fake.key. Yours will be different:

$ openssl pkey -in fake.key -text -noout | head -5
Private-Key: (256 bit)
priv:
    6a:98:5b:23:d3:bb:50:40:92:6b:f8:24:e7:8d:77:
    a7:80:ab:d7:a1:79:17:33:62:bd:43:28:45:4b:97:
    0d:4f
Enter fullscreen mode Exit fullscreen mode

The certificate holds the matching public key and is signed with this private key, by itself. That's what "self-signed" means below.

Then point curl at it, with --resolve playing the part of a poisoned DNS answer:

$ curl -b 'session=4f9a2c' https://example.com:8443/login \
       --resolve example.com:8443:127.0.0.1
curl: (60) SSL certificate problem: self-signed certificate
Enter fullscreen mode Exit fullscreen mode

The verbose log shows where it stopped:

* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self-signed certificate
Enter fullscreen mode Exit fullscreen mode

The name on the certificate matched, but no CA on the list signed it, so curl sent an alert and hung up. All the fake server got was this:

error:0A000418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca
Enter fullscreen mode Exit fullscreen mode

No path and no cookie. curl gave up before it sent the request.

A real server shows a certificate signed by a trusted CA and the browser accepts it. A fake server for the same name has no valid signature and the browser refuses it.

Why a counterfeit gets caught

Now do what people do when a browser shows a certificate warning and they click "Accept the risk". With curl, that's -k:

$ curl -k -b 'session=4f9a2c' https://example.com:8443/login \
       --resolve example.com:8443:127.0.0.1
Enter fullscreen mode Exit fullscreen mode

This time the fake server prints:

GET /login HTTP/1.1
Host: example.com:8443
User-Agent: curl/8.14.1
Accept: */*
Cookie: session=4f9a2c
Enter fullscreen mode Exit fullscreen mode

The connection was encrypted the whole way. It just went to the wrong party, who now has the session cookie. That's why the warning matters, and why sites that send an HSTS header take the "accept" button away completely.

What HTTPS doesn't hide

The capture above already showed one gap. Here's the full list of what someone on the network can still see:

  • The server name. SNI is in the clear, as strings showed. A newer extension, Encrypted ClientHello (ECH), encrypts it, but only where both the browser and the site support it.
  • The DNS lookup. Before connecting, your machine asks for the site's address, usually over plain UDP. DNS over HTTPS hides that, but your system resolver doesn't use it by default.
  • The IP address. Packets have to be addressed. If a site has its own IP, the address alone gives it away.
  • Sizes and timing. How much you send and receive, and when, can sometimes identify the page even without reading it.

What HTTPS does hide is everything inside: the path, the query string, the headers, the cookies, what you typed and the page itself.

The weak point is the list

The certificate check is only as strong as the CAs on the list, and any of them can sign for any name. In October we saw what happens when one gets this wrong. On 17 October, a researcher reported that the private keys of eight intermediate CAs belonging to eMudhra, an Indian CA, could be recovered. Six of them signed website certificates. The keys should have been huge random numbers. They were all smaller than 256, and one was reportedly the number 4.

With one of those keys, our counterfeit would have passed. It failed because fake.crt was signed by its own key, and no browser trusts that. An attacker with a leaked eMudhra intermediate key would sign fake.crt with that key instead, and have the fake server send it along with eMudhra's intermediate certificate, which is public. curl would follow the chain: the fake certificate is signed by the eMudhra intermediate, the intermediate is signed by an eMudhra root, and eMudhra's roots (the emSign files in /etc/ssl/certs) are on the list. Every check passes. Anyone who can redirect your traffic, through a fake wifi network or a poisoned DNS answer, gets the request with the cookie, and nothing on the screen looks wrong. Since any CA can sign for any name, this works for every site, not only those that buy certificates from eMudhra.

With a leaked CA key, a fake server can present a validly signed certificate for any name and the browser accepts it. Once browsers remove that CA, the same certificate is refused.
What a leaked CA key changes

Mozilla and Google blocked the keys within hours, and Microsoft did the same on Windows. There's also a second check that makes this kind of forgery visible. Chrome only accepts certificates recorded in public Certificate Transparency logs, so a forged certificate has to be published where anyone can see it. The maths held up. What failed was one company's key handling, and the fix was to take it off the list.

Next

That's the whole picture: HTTP is plain text, and HTTPS wraps it in a channel that's encrypted and authenticated by a certificate. A counterfeit server gets refused before the browser sends anything, and the only thing the browser sends before that check is the server's name.

Part 2 is about a program that sits on your own machine and wants to answer for a website, which is exactly what a blocker like glocker does, and what HTTPS is built to refuse.

Top comments (0)