Digital Signatures, Certificates & TLS

Cyber Security · Lesson 10 of 31 8 min read Updated

Encryption keeps data secret, but on the internet you also need to know who you are talking to. When Meera opens her bank's website, how does her browser know the public key it receives really belongs to the bank and not to an attacker on the café Wi-Fi? The answer is a chain of digital signatures called certificates.

In this lesson you will learn how digital signatures work, what a certificate contains, how certificate authorities create a chain of trust, and how TLS 1.3 combines all of this to protect every HTTPS connection. You will sign and verify messages with Python, including a toy version you can run in the browser.

Digital signatures

A digital signature proves three things about a message: it was not changed (integrity), it came from the holder of a particular private key (authenticity), and that person cannot later deny signing it (non-repudiation).

Signing works like this:

  1. Hash the message (for example with SHA-256).
  2. Use the private key to create a signature from the hash.
  3. Anyone with the matching public key can check the signature against the message.

The toy below uses RSA with tiny prime numbers so you can see every step. Real RSA uses primes hundreds of digits long; these numbers can be broken instantly and are for learning only.

PythonRun it yourself
import hashlib

# TOY RSA with tiny primes: for learning only, trivially breakable
p, q = 47, 59
n = p * q
phi = (p - 1) * (q - 1)
e = 17
d = pow(e, -1, phi)          # private exponent
print("Public key  (n, e):", (n, e))
print("Private key (n, d):", (n, d))

def digest(message):
    return int(hashlib.sha256(message.encode()).hexdigest(), 16) % n

def sign(message):
    return pow(digest(message), d, n)          # uses the PRIVATE key

def verify(message, signature):
    return pow(signature, e, n) == digest(message)   # uses the PUBLIC key

sig = sign("Pay Rohan ₹500")
print("Signature:", sig)
print("Genuine message: ", verify("Pay Rohan ₹500", sig))
print("Changed message: ", verify("Pay Rohan ₹5000", sig))
Output
Public key  (n, e): (2773, 17)
Private key (n, d): (2773, 157)
Signature: 2325
Genuine message:  True
Changed message:  False

pow(e, -1, phi) computes the modular inverse that turns the public exponent into the private one. Only someone who knows p and q can compute it, and with real key sizes, finding p and q from n is far beyond any computer today.

In practice you would use a modern signature algorithm such as Ed25519 from a trusted library. This version needs the cryptography package (pip install cryptography), so run it on your computer:

Python
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.exceptions import InvalidSignature

private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()

message = b"release-notes v2.4: security fixes"
signature = private_key.sign(message)
print("Signature bytes:", len(signature))

public_key.verify(signature, message)
print("Genuine: signature valid")

try:
    public_key.verify(signature, b"release-notes v2.4: no changes")
except InvalidSignature:
    print("Changed: InvalidSignature")
Output
Signature bytes: 64
Genuine: signature valid
Changed: InvalidSignature

Signatures protect software updates, Git commits, email (S/MIME and DKIM), PDF documents and every certificate on the web. In India, the IT Act gives legal recognition to electronic signatures. The Controller of Certifying Authorities (CCA) licenses the certifying authorities that issue the Digital Signature Certificates (DSCs) people use for company filings, tax returns and tenders.

Certificates

A public key on its own is just a number. A certificate (in the X.509 format) binds a public key to a name and is signed by a certificate authority (CA) that vouches for the binding. Its main fields are:

FieldMeaning
SubjectWho the certificate is for
Subject Alternative Name (SAN)The exact domain names it is valid for; browsers check this field
IssuerThe CA that signed it
Validity"Not before" and "not after" dates
Public keyThe key that belongs to the subject
SignatureThe issuer's signature over all of the above

Browsers and operating systems ship with a trust store of root CA certificates. A website sends its own (leaf) certificate plus one or more intermediate certificates. The browser checks that each certificate is signed by the next one up, that the chain ends at a trusted root, that the dates are valid and that the domain matches the SAN. If anything fails, you see a full-page warning. Clicking through that warning on a public network is exactly what an attacker in the middle hopes you will do.

Free, automated CAs such as Let's Encrypt issue certificates using the ACME protocol, so renewal can be fully automatic. Automation matters more every year:

Expired certificates cause real outages. A simple monitor checks the days left on each certificate you own:

PythonRun it yourself
from datetime import date

today = date(2026, 11, 10)
certs = [
    {"host": "shop.example", "san": ["shop.example", "www.shop.example"], "not_after": date(2027, 4, 2)},
    {"host": "api.shop.example", "san": ["api.shop.example"], "not_after": date(2026, 11, 24)},
    {"host": "old.shop.example", "san": ["shop.example"], "not_after": date(2026, 10, 30)},
]

for c in certs:
    days = (c["not_after"] - today).days
    if days < 0:
        status = f"EXPIRED {-days} days ago"
    elif days <= 30:
        status = f"renew soon: {days} days left"
    else:
        status = f"ok: {days} days left"
    name_ok = "name ok" if c["host"] in c["san"] else "NAME MISMATCH"
    print(f'{c["host"]:<18} {status:<28} {name_ok}')
Output
shop.example       ok: 143 days left            name ok
api.shop.example   renew soon: 14 days left     name ok
old.shop.example   EXPIRED 11 days ago          NAME MISMATCH

How TLS 1.3 protects a connection

TLS (Transport Layer Security) is the protocol inside HTTPS. TLS 1.3 completes its handshake in one round trip:

  1. ClientHello: the browser lists the cipher suites it supports and sends a key share, its half of an elliptic-curve Diffie-Hellman key exchange.
  2. ServerHello: the server picks a cipher suite and sends its own key share. Both sides can now compute the same secret, and everything after this point is encrypted.
  3. The server sends its certificate chain and a CertificateVerify message: a signature with its private key over the handshake, proving it owns the certificate's key.
  4. Both sides send Finished messages, and application data (the HTTP request) flows, encrypted with AES-GCM or ChaCha20-Poly1305.

Diffie-Hellman is the clever part: two sides agree on a shared secret while an eavesdropper sees every number they exchange. Here it is with tiny numbers (toy only; real TLS uses elliptic curves such as X25519):

PythonRun it yourself
p, g = 23, 5            # public: a prime and a generator (real ones are huge)
a, b = 6, 15            # private: chosen secretly by each side

A = pow(g, a, p)        # Meera sends A in the open
B = pow(g, b, p)        # the server sends B in the open
print("Sent over the network:", A, B)

meera_key = pow(B, a, p)
server_key = pow(A, b, p)
print("Meera computes: ", meera_key)
print("Server computes:", server_key)
Output
Sent over the network: 8 19
Meera computes:  2
Server computes: 2

Because new key shares are generated for every connection and then thrown away, TLS 1.3 has forward secrecy: even if the server's private key is stolen next year, recorded traffic from today cannot be decrypted.

To inspect the certificate of a TLS service in your lab (for example an HTTPS server you set up on the target VM), use openssl on Kali:

Terminal
openssl s_client -connect 192.168.56.101:443 -servername lab.local < /dev/null
openssl s_client -connect 192.168.56.101:443 < /dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

The first command shows the whole handshake, including the TLS version and cipher; the second prints just the subject, issuer and validity dates.

Common mistakes

  • Clicking through certificate warnings. On a hostile network, that warning may be the only sign of an attack.
  • Turning off certificate verification in code (for example verify=False in Python requests) to "fix" an error. It silently removes all protection against impersonation.
  • Forgetting renewal. With certificate lifetimes shrinking, automate renewal and alert well before expiry.
  • Protecting the certificate but not the private key. Anyone with the private key can impersonate the server. Store it with strict permissions or in a hardware security module.

Key points

  • A signature is made with a private key and checked with the public key; it proves integrity, authenticity and non-repudiation.
  • A certificate binds a public key to names (in the SAN) and is signed by a CA; browsers follow the chain to a trusted root.
  • Maximum public certificate lifetime is 200 days from March 2026, shrinking to 47 days by 2029, so automate renewal.
  • TLS 1.3 uses ephemeral Diffie-Hellman key shares, a certificate signature and authenticated encryption in one round trip.
  • Forward secrecy means a stolen server key cannot decrypt old recorded traffic.

Exercise

Using the toy RSA code, write a function forge(message) that tries to "sign" a message using only the public key (n and e) and show that verification fails for it. Then extend the certificate monitor with a fourth certificate for pay.shop.example that expires on date(2026, 12, 1) and has the SAN list ["pay.shop.example"].

Show solution
Python
import hashlib
from datetime import date

p, q, e = 47, 59, 17
n = p * q
d = pow(e, -1, (p - 1) * (q - 1))

def digest(m):
    return int(hashlib.sha256(m.encode()).hexdigest(), 16) % n

def verify(m, s):
    return pow(s, e, n) == digest(m)

def forge(m):
    return pow(digest(m), e, n)   # attacker only has the public exponent

print("Forged signature valid?", verify("Pay Kabir ₹9000", forge("Pay Kabir ₹9000")))

today = date(2026, 11, 10)
print("pay.shop.example days left:", (date(2026, 12, 1) - today).days)

The forged signature fails because creating a valid one needs the private exponent d. The new certificate has 21 days left, so the monitor would print "renew soon".

Quick Quiz

1. Which key is used to create a digital signature?

2. Which certificate field do browsers check to see if a certificate matches the domain?

3. What does forward secrecy protect against?

4. What is the maximum lifetime of a public TLS certificate issued after 15 March 2026?

Learn Cyber Security with a mentor

Free lessons get you started. Our Ethical Hacking & Cyber Security training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call