A hands-on networking project to understand TLS, HTTPS, certificates, Certificate Authorities, key exchange, symmetric encryption, and secure communication using Node.js and OpenSSL.
The goal is not simply to use HTTPS, but to understand what happens underneath when a client connects to a server such as:
https://chatgpt.com
Suppose a client sends:
POST /login HTTP/1.1
username=alice&password=secretWithout TLS, the HTTP data travels as plaintext.
Conceptually:
Client
│
│ username=alice
│ password=secret
▼
Network
│
▼
Server
Someone capable of observing the traffic could potentially read or tamper with it.
TLS provides three important security properties:
TLS
│
┌─────────┼─────────┐
▼ ▼ ▼
Confidentiality Integrity Authentication
Attackers observing network traffic should not be able to read the application data.
Attackers should not be able to modify the data without detection.
The client can verify the identity represented by the server certificate.
For HTTP/1.1 and HTTP/2:
┌─────────────────────┐
│ HTTP │
├─────────────────────┤
│ TLS │
├─────────────────────┤
│ TCP │
├─────────────────────┤
│ IP │
├─────────────────────┤
│ Ethernet / Wi-Fi │
└─────────────────────┘
Therefore:
HTTPS = HTTP over TLS
HTTP is responsible for the application protocol.
TCP is responsible for reliable byte transport.
TLS provides cryptographic security between the application and transport layers.
Suppose the user enters:
https://agrichain.com
A simplified sequence is:
Browser
│
│ DNS
▼
IP address of agrichain.com
│
▼
TCP 3-way handshake
│
▼
TLS handshake
│
├── Server authentication
│
├── Key agreement
│
└── Session keys
│
▼
Encrypted HTTP communication
So the browser does not immediately send the HTTP request.
It first establishes a secure TLS session.
The browser needs to answer:
"Who am I actually talking to?"
Suppose the browser connects to:
https://agrichain.com
The server sends a certificate containing information such as:
Certificate
├── Domain identity
├── Public key
├── Validity period
├── Issuer
├── Usage / constraints
└── Digital signature
The certificate essentially binds:
agrichain.com
↕
server public key
The important idea is:
A certificate is not the symmetric encryption key for the connection.
Its main purpose here is authentication and identity binding.
When the client receives the certificate, it performs certificate/path validation.
Conceptually:
Server certificate
│
▼
Intermediate CA
│
▼
Trusted Root CA
│
▼
Client trust store
The client performs checks such as:
✓ Hostname matches
✓ Certificate is currently valid
✓ Certificate signatures are valid
✓ Certificate chain leads to a trusted root
✓ Certificate is appropriate for server authentication
✓ Relevant constraints and policies are valid
✓ Revocation/status checks may be performed
If the necessary checks succeed, the client can accept the server's identity for the TLS connection.
No.
The browser does NOT have a certificate for every website.
Instead, the client already has a collection of trusted Root CA certificates.
Conceptually:
Client / OS
│
└── Trust Store
├── Root CA A
├── Root CA B
├── Root CA C
└── Root CA D
The client trusts those roots as trust anchors.
A website's certificate can therefore be validated through a chain:
agrichain.com certificate
│
│ signed by
▼
Intermediate CA
│
│ signed by
▼
Trusted Root CA
│
▼
Client trust store
The client does not need to know agrichain.com beforehand.
This is the foundation of the Web PKI.
Cryptography can prove:
"This certificate was signed by this CA."
But cryptography alone cannot prove:
"This CA is trustworthy."
That initial decision is a trust decision made by the software/platform.
Operating systems and browsers maintain trust stores containing Root CAs they choose to trust.
Therefore:
Trust store
↓
Trusted Root CA
↓
Intermediate CA
↓
Website certificate
The Root CA is the trust anchor.
There is no single organization controlling every CA.
There are several major participants:
Certificate Authorities
├── Let's Encrypt / ISRG
├── DigiCert
├── Sectigo
├── GlobalSign
└── others
Root-store operators
├── Mozilla
├── Apple
├── Microsoft
├── Google / Chrome
└── others
Industry requirements are also influenced by organizations such as the CA/Browser Forum.
The important relationship is:
Root-store operator
│
│ trusts
▼
Root CA
│
▼
Intermediate CA
│
▼
Website certificate
A CA does not receive permission from a central "Internet authority" to issue every certificate.
For a domain such as agrichain.com, the domain owner proves control of the domain to the CA, and the CA issues the certificate according to its policies and applicable requirements.
Not necessarily.
Each machine/application can have its own trust store.
CLIENT SERVER
──────── ────────
Trust Store Trust Store
├── Root A ├── Root X
├── Root B ├── Root Y
└── Root C └── Root Z
A normal HTTPS server does not need to trust its own certificate.
The server's trust store becomes important when the server itself acts as a TLS client.
For example:
Backend A
│
│ HTTPS
▼
Backend B
Backend A needs to verify Backend B's certificate.
A typical TLS server has:
Server
├── Private key
├── Server certificate
└── Intermediate certificate(s)
The private key must remain secret.
The certificate can be sent to clients.
Conceptually:
SERVER
Private key
│
└── SECRET
Certificate
│
└── sent during TLS handshake
Suppose:
https://agrichain.com
You would typically:
1. Register agrichain.com
2. Configure DNS
3. Prove control of the domain to a CA
4. Obtain a TLS certificate
5. Install the certificate and private key on the server
6. Configure HTTPS
7. Renew the certificate automatically
You do NOT directly sign your website certificate with a Root CA private key.
The typical public PKI hierarchy is:
Trusted Root CA
│
▼
Intermediate CA
│
▼
agrichain.com certificate
Once the secure session is established, the actual application traffic is protected using symmetric cryptography.
Symmetric encryption uses secret key material shared by both sides.
Conceptually:
Plaintext
│
▼
Encryption + key
│
▼
Ciphertext
│
▼
Decryption + key
│
▼
Plaintext
Examples of modern symmetric encryption include:
AES-GCM
ChaCha20-Poly1305
Symmetric encryption is efficient for large amounts of application data.
Asymmetric cryptography is comparatively expensive for bulk data.
TLS therefore uses a hybrid approach:
Handshake
│
├── Asymmetric cryptography
│
├── Authentication
│
└── Key agreement
│
▼
Symmetric traffic keys
│
▼
Actual HTTP traffic
This gives the security benefits of asymmetric cryptography while using efficient symmetric cryptography for the data transfer.
A common beginner explanation is:
"The server sends the symmetric key encrypted with its public key."
That is not the correct mental model for modern TLS 1.3.
Modern TLS normally uses an ephemeral Diffie-Hellman key agreement, typically ECDHE.
The simplified idea is:
CLIENT SERVER
temporary private key temporary private key
temporary public key temporary public key
public key ────────────>
<──────────── public key
│ │
▼ ▼
derive shared secret derive shared secret
SAME SHARED SECRET
The private values are never sent across the network.
An observer can see the public key-exchange values but cannot practically derive the shared secret.
The key-exchange keys are temporary.
Conceptually:
Connection 1
↓
temporary keys
Connection 2
↓
different temporary keys
Connection 3
↓
different temporary keys
This contributes to forward secrecy.
If the server's long-term private key is compromised later, previously recorded TLS sessions should not automatically become decryptable when ephemeral key exchange has been used correctly.
These are two different jobs.
CERTIFICATE
↓
"Who is this server?"
ECDHE
↓
"What shared secret can we use?"
The server certificate contains the server's long-term public-key identity.
The ephemeral ECDHE keys are used for the connection's key agreement.
So:
Certificate
↓
Authentication
ECDHE
↓
Key agreement
Derived traffic keys
↓
Encryption
Suppose the certificate binds:
agrichain.com
↕
public key
The server must also prove that it actually possesses the corresponding private key.
It does this through a cryptographic signature during the handshake.
Conceptually:
Certificate
"This public key belongs to agrichain.com."
Server
"I possess the corresponding private key."
Client
"Verify the cryptographic proof."
↓
✓
This prevents someone from simply copying a legitimate certificate and pretending to be the real server.
A simplified mental model is:
CLIENT SERVER
ClientHello
+ supported options
+ key-share information
────────────────────────────────────────────>
ServerHello
+ key-share
+ certificate
+ authentication proof
<──────────────────
Validate certificate
Validate server proof
derive shared secret ←────→ derive shared secret
↓ ↓
derive traffic keys
encrypted handshake messages
<────────────────────────────>
===== encrypted HTTP =====
<────────────────────────────>
The real TLS 1.3 handshake contains more detail, but this captures the main concepts.
Once the handshake succeeds:
TLS handshake
↓
traffic keys established
↓
encrypted application data
Suppose the browser wants to send:
POST /login HTTP/1.1
username=alice&password=secretThe flow is:
Browser
↓
HTTP request
↓
TLS encryption
↓
Encrypted TLS records
↓
TCP
↓
Network
↓
Server
↓
TLS decryption
↓
HTTP request
↓
Backend application
The server application ultimately receives the decrypted HTTP data.
No.
TLS primarily protects data in transit.
The simplified lifecycle is:
Browser
│
│ plaintext inside browser
▼
TLS encryption
│
│ encrypted over network
▼
TLS decryption
│
▼
Server application
│
▼
Database / storage
So:
TLS
↓
protects data in transit
Database encryption
↓
protects data at rest
These solve different security problems.
Normal HTTPS generally provides server authentication.
The typical flow is:
Server
│
│ certificate + authentication proof
▼
Client
The client generally does not present a certificate for an ordinary website.
User authentication usually happens through mechanisms such as:
password
session cookie
OAuth
access token
There is also mutual TLS (mTLS) where both sides authenticate using certificates:
Client certificate ─────────→ Server
Server certificate ─────────→ Client
Plain HTTP:
HTTP
↓
TCP
↓
IP
HTTPS:
HTTP
↓
TLS
↓
TCP
↓
IP
With plain HTTP, the application data can be visible in plaintext on the network.
With HTTPS:
HTTP
↓
TLS encryption
↓
TCP
↓
IP
The network carries encrypted TLS records instead.
Earlier we learned:
TCP
↓
socket
↓
HTTP/1.1
↓
HTTP/2
TLS fits into that architecture:
HTTP/1.1
↓
TLS
↓
TCP
↓
IP
For HTTP/2:
HTTP/2
↓
TLS
↓
TCP
For HTTP/3:
HTTP/3
↓
QUIC
↓
UDP
QUIC incorporates TLS 1.3 cryptographic mechanisms into its connection establishment rather than using TLS as a separate layer over TCP in the same way.
For development, this project uses a self-signed certificate.
Generate one with:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout cert/server.key \
-out cert/server.crt \
-days 365 \
-subj "/CN=localhost"This gives:
cert/
├── server.key
└── server.crt
The certificate is self-signed:
localhost certificate
↓
signed by itself
It is useful for learning but is not equivalent to a publicly trusted production certificate.
Use:
openssl x509 -in cert/server.crt -text -nooutLook for:
Subject
Issuer
Validity
Public Key
Signature Algorithm
For the self-signed local certificate:
Subject = localhost
Issuer = localhost
Example Node.js server:
const https = require("node:https");
const fs = require("node:fs");
const options = {
key: fs.readFileSync("cert/server.key"),
cert: fs.readFileSync("cert/server.crt"),
};
const server = https.createServer(options, (req, res) => {
console.log("HTTPS request received");
console.log("Method:", req.method);
console.log("Path:", req.url);
res.writeHead(200, {
"Content-Type": "text/plain",
});
res.end("Hello from HTTPS\n");
});
server.listen(8443, "127.0.0.1", () => {
console.log("HTTPS server listening on https://127.0.0.1:8443");
});Because the certificate is self-signed:
curl -k -v https://127.0.0.1:8443/helloThe -k option tells curl not to enforce normal certificate trust verification for this local experiment.
Do not use this as a production configuration.
Start the server:
node src/https-server.jsThen:
openssl s_client -connect 127.0.0.1:8443OpenSSL acts as the client.
You can inspect:
Certificate
Certificate chain
TLS protocol
Cipher
Verification result
For example:
Protocol : TLSv1.3
Cipher : ...
The exact values can vary.
Because our local certificate is self-signed.
By default:
Server certificate
↓
self-signed
↓
not anchored in the normal trust store
So OpenSSL may report a verification error.
You can explicitly trust your local certificate:
openssl s_client \
-connect 127.0.0.1:8443 \
-CAfile cert/server.crtNow the certificate is explicitly provided as a trust anchor for this experiment.
This demonstrates an important difference:
Receiving a certificate
≠
Trusting the certificate
Example:
const https = require("node:https");
const options = {
hostname: "127.0.0.1",
port: 8443,
path: "/hello",
method: "GET",
// Only for the self-signed local lab.
rejectUnauthorized: false,
};
const request = https.request(options, (response) => {
console.log("Status:", response.statusCode);
response.on("data", (chunk) => {
console.log("Body:", chunk.toString());
});
response.on("end", () => {
console.log("Response finished");
});
});
request.on("socket", (socket) => {
socket.on("secureConnect", () => {
console.log("TLS connection established");
console.log("Protocol:", socket.getProtocol());
console.log("Cipher:", socket.getCipher());
const certificate = socket.getPeerCertificate();
console.log("Certificate subject:", certificate.subject);
console.log("Certificate issuer:", certificate.issuer);
});
});
request.on("error", (error) => {
console.error("Request error:", error.message);
});
request.end();The important operations are:
socket.getProtocol()
↓
TLS version
socket.getCipher()
↓
negotiated cryptographic cipher
socket.getPeerCertificate()
↓
server certificate
When you visit:
https://agrichain.com
think:
HTTPS
Browser
│
│ DNS
▼
IP address
│
▼
TCP handshake
│
▼
TLS handshake
│
┌─────────┴─────────┐
▼ ▼
Server certificate Key agreement
│ │
▼ ▼
Verify identity Shared secret
│ │
└─────────┬─────────┘
▼
Traffic keys
│
▼
Encrypted HTTP
│
▼
Backend
Identity ↔ Public key
Trust anchor
Server certificate
↓
Intermediate CA
↓
Trusted Root CA
Key agreement
Actual application data protection
Authentication
+
Key establishment
+
Confidentiality
+
Integrity
HTTP over TLS
USER
│
│
https://agrichain.com
│
▼
DNS
│
▼
Server IP
│
▼
TCP 3-Way Handshake
│
SYN → SYN-ACK → ACK
│
▼
TLS Handshake
│
┌───────────────┴───────────────┐
│ │
▼ ▼
Server Certificate ECDHE
│ │
▼ ▼
Certificate Chain Key Agreement
│ │
▼ ▼
Trusted Root CA Shared Secret
│ │
└───────────────┬───────────────┘
▼
Traffic Keys Created
│
▼
Symmetric Encryption
│
▼
Encrypted HTTP
│
▼
Backend
│
▼
Database
This lab should be built in this order:
1. Plain HTTP server
↓
2. Self-signed certificate
↓
3. HTTPS server
↓
4. Node.js HTTPS client
↓
5. Certificate inspection
↓
6. OpenSSL s_client
↓
7. Certificate verification
↓
8. Trust store understanding
↓
9. TLS handshake inspection
↓
10. Symmetric session-key concept
↓
11. Packet inspection
↓
12. Compare HTTP vs HTTPS
The long-term goal is to understand the complete path:
Application
↓
HTTP
↓
TLS
↓
TCP
↓
IP
↓
Network
rather than treating HTTPS as a black box.