The previous lesson ended with a dead end: a physiotherapist's mobile app and the Nimbus API need a common symmetric key, but the only channel they have between them is exactly the one they want to protect. For four thousand years that problem had no solution; in the 1970s it was solved with an idea that sounds impossible the first time you hear it: use two different keys, one that is published and one that is never shared. This lesson develops that idea — the key pair, RSA, elliptic curve cryptography, the Diffie-Hellman exchange and forward secrecy, the hybrid encryption that explains how TLS really works, and the digital signature, the only primitive that gives non-repudiation. It matters because practically everything that makes the modern web secure, from HTTPS to passkeys and the signature on the Nimbus JWT, is asymmetric cryptography.
Contents
- The idea of the key pair
- What you do with the public key and what with the private key
- RSA: the hard problem of factoring
- Elliptic curve cryptography: the same security with much shorter keys
- Diffie-Hellman key exchange
- Ephemeral, ECDHE and forward secrecy
- Hybrid encryption: how it is really used
- Digital signature: what it guarantees and how it differs from a MAC
- Signing a Nimbus booking receipt with Ed25519
- Inspecting an RSA pair with
openssl - The question that remains open and the quantum threat
- The idea of the key pair
In an asymmetric scheme, two mathematically related keys are generated:
- The public key: it can be published on a website, printed on a T-shirt or sent by unencrypted e-mail. Its circulation compromises nothing.
- The private key: it never leaves the system that generated it. Ideally it is not even exportable, as happens with the FIDO2 passkeys of 02-05.
The relationship between the two is a trapdoor: it is easy to compute the public key from the private one, and computationally infeasible to go the other way. All of asymmetric cryptography rests on mathematical problems with that asymmetry — multiplying two large primes is trivial, factoring the product is not.
flowchart TB
G["GENERATION\nA single process produces\nthe complete pair"]
G --> PUB["PUBLIC KEY\nHanded out freely.\nEncrypt / Verify signatures"]
G --> PRIV["PRIVATE KEY\nNever leaves the system.\nDecrypt / Sign"]
PRIV -.->|"easy"| PUB
PUB -.->|"infeasible"| PRIV
This solves distribution at a stroke: to receive encrypted messages it is enough to publish the public key. And the number of keys stops growing quadratically: n participants need n pairs, not n(n-1)/2 shared keys. With 10,000 users, 10,000 pairs instead of fifty million keys.
- What you do with the public key and what with the private key
This is the table people most often misremember, and getting it wrong inverts the guarantees completely:
| Operation | The key used is the... | Who can do it | What you get |
|---|---|---|---|
| Encrypt | Recipient's public key | Anybody | Only the recipient will be able to read it → confidentiality |
| Decrypt | Recipient's private key | Only the recipient | — |
| Sign | Sender's private key | Only the sender | Authenticity, integrity and non-repudiation |
| Verify | Sender's public key | Anybody | — |
The mnemonic: the private key is the one that "closes" your identity and "opens" your messages. You sign with what only you have; you decrypt with what only you have.
The usual mistake is to invert it, and the consequences differ in each direction:
- "We encrypt with the private key so that only our people can read it." False and dangerous: what is encrypted with the private key is decrypted by anybody with the public one, because the public key is public. That is not encrypting, it is — conceptually — signing.
- "We sign with the public key." Impossible: if anybody can sign, the signature proves nothing.
An important consequence for the design of Nimbus: in a system with thousands of customers, encrypting with a public key is not the usual thing; the usual thing is to agree a symmetric key using asymmetric cryptography and encrypt the data with it (section 7). Direct asymmetric encryption is reserved for very small data, such as a key.
- RSA: the hard problem of factoring
RSA (Rivest, Shamir and Adleman, 1977) was the first practical asymmetric scheme and is still ubiquitous, especially in certificates and token signatures.
The intuition behind the hard problem. Multiplying two large primes is instantaneous:
The reverse — given n, find p and q — is easy with small numbers and becomes infeasible when n has 2048 or 3072 bits (more than 600 decimal digits). The public key contains n; the private one depends on knowing p and q. The security of RSA is exactly the difficulty of factoring.
| RSA key size | Approx. security bits | Status in 2026 |
|---|---|---|
| 1024 | ~80 | Forbidden. Within reach of well-resourced adversaries |
| 2048 | ~112 | Acceptable minimum. Being progressively retired |
| 3072 | ~128 | Recommended for new long-lived keys |
| 4096 | ~152 | High security; noticeably slower |
Three things you have to know about RSA in order to use it well:
- It is slow, above all the operation with the private key (signing or decrypting). Remember the performance table in 03-02: about a thousand operations per second against gigabytes per second for AES.
- It cannot encrypt large data. With RSA-2048 the physical limit is the size of the modulus, and with the mandatory padding some 190 usable bytes remain. It is not a practical limitation you can get around with more keys: it is structural.
- The padding matters as much as the algorithm. The old PKCS#1 v1.5 scheme has known attacks. Today you use OAEP for encryption and PSS for signatures. Serious libraries force you to choose it explicitly; never copy code that uses
PKCS1v15for new encryption.
- Elliptic curve cryptography: the same security with much shorter keys
Elliptic curve cryptography (ECC) rests on a different hard problem: the discrete logarithm over the points of an elliptic curve. Its great practical advantage is efficiency.
| Security bits | RSA / DH | ECC | Size difference |
|---|---|---|---|
| 80 | 1024 | 160 | ~6× |
| 112 | 2048 | 224 | ~9× |
| 128 | 3072 | 256 (P-256, Curve25519) | ~12× |
| 192 | 7680 | 384 (P-384) | ~20× |
| 256 | 15360 | 512 (P-521, Curve448) | ~30× |
Read the highlighted row carefully: a 256-bit ECC key offers the same security as a 3072-bit RSA key. That means much smaller keys and signatures, faster handshakes and lower power consumption, which matters especially in the Nimbus mobile app and in any battery-powered device.
Curves you will see in practice:
| Curve | Main use | Notes |
|---|---|---|
| P-256 (secp256r1) | TLS, certificates, JWT with ES256 | NIST standard, universal support |
| P-384 | High security environments | NIST standard |
| Curve25519 / X25519 | Key exchange | Modern design, hard to implement badly |
| Ed25519 | Digital signature | Fast, 64-byte signatures, no parameters to choose |
| secp256k1 | Blockchains | Do not use it outside that context |
Ed25519 and X25519 are the recommended default pair for new code. Not because they are "more mathematical", but because their design removes most of the decisions where a developer could go wrong: there is no padding to choose, no curves to validate by hand, and the signature algorithm is deterministic, which avoids the historical failure of ECDSA signatures with faulty randomness (the same kind of failure we saw in 03-01 with the Android generator).
A warning about naming, because it causes constant confusion: X25519 is for exchanging keys, Ed25519 is for signing. They are related curves but not interchangeable, and the libraries will not let you mix them.
- Diffie-Hellman key exchange
This is the mechanism that solves the problem 03-02 closed with. Diffie-Hellman allows two parties who have never met to agree a shared secret talking over a channel the attacker listens to in full, without the attacker being able to deduce the secret.
The paint analogy
- Ana and the API publicly agree a base colour, say yellow. The attacker sees it.
- Ana secretly picks a colour of her own, red, and mixes it with the yellow: she gets orange, and sends it. The attacker sees the orange.
- The API secretly picks blue, mixes it with yellow and gets green, which it sends. The attacker sees the green.
- Ana mixes the green she received with her secret red. The API mixes the orange it received with its secret blue. Both get the same brown.
- The attacker has yellow, orange and green, and cannot obtain the brown, because separating a paint mixture into its components is far harder than mixing it.
That difficulty of "separating the mixture" is, in the real mathematics, the discrete logarithm problem.
sequenceDiagram
participant A as Mobile app (Ana)
participant R as Network (attacker listens)
participant S as Nimbus API
A->>A: generates private a<br/>computes public A
S->>S: generates private b<br/>computes public B
A->>R: sends A (public)
R->>S: A
S->>R: sends B (public)
R->>A: B
A->>A: secret = combine(B, a)
S->>S: secret = combine(A, b)
Note over A,S: Both have the SAME shared secret
Note over R: The attacker saw A and B<br/>and cannot deduce the secret
Two fundamental clarifications:
- The shared secret is not used directly as a key. It is passed through a derivation function (HKDF, from 03-02) to obtain encryption keys with the correct format and domain separation.
- Diffie-Hellman on its own does not authenticate. An active attacker can run an exchange with each end and sit in the middle: it is the classic MITM of 02-02. The solution is to authenticate the exchange with a digital signature backed by a certificate — and that is where the module links to TLS (03-05) and to PKI (03-06).
- Ephemeral, ECDHE and forward secrecy
In TLS you will see the acronym ECDHE: Elliptic Curve Diffie-Hellman Ephemeral. The key word is the last one.
- Static: the parties always use the same key pair for the exchange.
- Ephemeral: a new pair is generated for every session and destroyed when it ends.
From this comes the most valuable property in modern cryptography:
Forward secrecy (also called perfect forward secrecy): compromising the server's long-lived private key does not allow past sessions to be decrypted.
It is worth grounding this in Nimbus with a concrete scenario:
- An attacker records all the encrypted traffic between the mobile apps and the API for months. They cannot read anything. They keep it anyway.
- Two years later, they obtain the private key of the Nimbus server — through an intrusion, a badly wiped backup or a court order in another jurisdiction.
| Without forward secrecy | With forward secrecy (ECDHE) |
|---|---|
| The session key was encrypted with the server's public key and travelled inside the handshake | The session key never travelled: it was agreed with an ephemeral pair that was destroyed on hanging up |
| With the private key, the attacker decrypts everything recorded, retroactively | With the private key, the attacker cannot decrypt anything recorded |
| A single compromise ruins years of traffic | A compromise allows the server to be impersonated from then on, but not the past to be read |
This is why TLS 1.3 removed from the standard every mode without forward secrecy and why RSA key exchange is forbidden in modern configurations (03-05). And it is also the direct answer to the "harvest now, decrypt later" scenario of section 11.
- Hybrid encryption: how it is really used
We now have the two pieces and their crossed limitations:
- Asymmetric cryptography solves distribution but is slow and cannot handle large data.
- Symmetric cryptography is extremely fast but cannot distribute its own key.
The solution is to combine them, and it is called hybrid encryption. It is the pattern behind TLS, PGP, cloud envelope encryption and almost everything else:
flowchart TB
A["1. A random SYMMETRIC KEY is generated,\nunique to this message\nor this session (session key)"]
A --> B["2. The DATA is encrypted with that key\nusing AES-GCM: fast,\nno size limit"]
A --> C["3. The SYMMETRIC KEY is protected\nwith asymmetric cryptography:\nECDHE (agreement) or RSA-OAEP (encryption)"]
B --> D["4. Sent or stored:\nprotected key + encrypted data"]
C --> D
D --> E["5. The recipient recovers the symmetric\nkey with their private key and\ndecrypts the data"]
Why this is the correct answer and not a workaround:
- 190 bytes are encrypted with the asymmetric part instead of 3 MB. The cost of the slow part is constant and negligible.
- The message size stops having a limit.
- If ECDHE is used in step 3 instead of encrypting the key, you also get forward secrecy.
This same pattern, applied to storage instead of to communication, is called envelope encryption: a data key encrypts the object and a master key in the KMS encrypts the data key. It is how Nimbus encrypts the attachments in the A-05 bucket, and it is developed in 03-06 and 03-07.
- Digital signature: what it guarantees and how it differs from a MAC
Signing is not encrypting. A digital signature hides nothing: the message travels readable and the signature is attached. What it contributes is proof of origin.
The real flow — and it is important, because it explains why signing a 4 GB file is fast:
flowchart LR
M["MESSAGE\nany size"] --> H["HASH\nSHA-256 -> 32 bytes"]
H --> F["OPERATION WITH THE\nPRIVATE KEY\nover those 32 bytes"]
F --> S["SIGNATURE\n64 bytes (Ed25519)"]
M --> V["VERIFY:\nrecompute the hash and check\nwith the PUBLIC key"]
S --> V
It is not the message that is signed: it is its hash. That is why the cost of signing does not depend on the size of the file, and why a broken hash function (MD5, SHA-1) breaks the signature too: if the attacker finds two documents with the same hash, a valid signature for one is valid for the other. Hashes are studied in 03-04.
Signature versus MAC
| MAC (HMAC) | Digital signature | |
|---|---|---|
| Type of key | Shared between the two parties | A pair: the private one signs, the public one verifies |
| Integrity? | Yes | Yes |
| Authenticity? | Yes, against the other holder of the key | Yes, against everybody |
| Non-repudiation? | No | Yes |
| Cost | Very low (it is a hash) | High (an asymmetric operation) |
| Who can verify? | Only whoever has the key | Anybody with the public key |
| Case in Nimbus | Payment gateway webhook | JWT signature, DKIM, artefact signing |
The decisive difference is non-repudiation. If Nimbus and the gateway share an HMAC key, a webhook with a valid MAC proves that one of the two generated it, and since Nimbus also has the key, it is no good as proof before a third party: Nimbus could have fabricated it. With a signature, on the other hand, only the gateway's private key can produce it, and Nimbus does not have it; that is why a signature is proof.
The practical rule: use a MAC when the two parties know each other, share a secret and only need to detect tampering; use a signature when the verifier could be anybody or when you need evidence that stands up against the other party.
- Signing a Nimbus booking receipt with Ed25519
Nimbus wants to issue verifiable booking receipts: the clinic must be able to check, months later, that the receipt was issued by Nimbus and has not been altered.
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey, Ed25519PublicKey,
)
from cryptography.hazmat.primitives import serialization
from cryptography.exceptions import InvalidSignature
import json
# ---------------------------------------------------------------
# 1. GENERATING THE PAIR. Done ONCE. The private key goes to the
# secrets manager or the KMS (03-06); the public one is published.
# ---------------------------------------------------------------
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
pub_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
print(pub_pem.decode())
# ---------------------------------------------------------------
# 2. THE MESSAGE. CANONICAL serialisation: same object -> same bytes.
# sort_keys and separators stop a space from invalidating the signature.
# ---------------------------------------------------------------
receipt = {
"tenant": "CL-014",
"booking_id": 88421,
"patient_ref": "PX-7731", # a reference, not the name
"date": "2026-08-14T10:30:00+02:00",
"amount_cents": 4500,
"issued_by": "nimbus-reservas",
"issued_at": "2026-08-02T09:12:44+02:00",
}
message = json.dumps(receipt, sort_keys=True, separators=(",", ":")).encode("utf-8")
# ---------------------------------------------------------------
# 3. SIGN with the private key -> 64 bytes
# ---------------------------------------------------------------
signature = private_key.sign(message)
print("Signature length:", len(signature), "bytes")
# ---------------------------------------------------------------
# 4. VERIFY with the public key
# ---------------------------------------------------------------
def verify(public_pem: bytes, message: bytes, signature: bytes) -> bool:
key: Ed25519PublicKey = serialization.load_pem_public_key(public_pem)
try:
key.verify(signature, message) # returns nothing if it is valid
return True
except InvalidSignature:
return False
print("Receipt intact:", verify(pub_pem, message, signature))
# ---------------------------------------------------------------
# 5. ALTERING THE MESSAGE: the amount is changed from 45.00 to 5.00 euros
# ---------------------------------------------------------------
altered_receipt = dict(receipt, amount_cents=500)
altered_message = json.dumps(
altered_receipt, sort_keys=True, separators=(",", ":")
).encode("utf-8")
print("Receipt altered:", verify(pub_pem, altered_message, signature))Output:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAJ1n2rQ0kXsF8mB4vC7pLd9TgYhWq3ZeR6aKcNfUx0sM=
-----END PUBLIC KEY-----
Signature length: 64 bytes
Receipt intact: True
Receipt altered: FalseWhat you need to grasp from this code:
- The public key in PEM takes three lines. Compare it with an RSA-3072 public key, which takes about fifteen. That is the ECC size advantage from section 4.
- Canonical serialisation is indispensable.
sort_keys=Truefixes the order of the keys andseparators=(",", ":")removes the spaces. Without this, two serialisations of the same object would produce different bytes and the signature would fail for a reason that is not an attack. It is integration failure number one with signatures over JSON. private_key.sign(message)does the hashing internally. Ed25519 uses SHA-512 under the bonnet; you do not touch that part, and that is precisely the point.verifydoes not returnTrue/False: it raisesInvalidSignature. It is a deliberate design, so that it is impossible to ignore the result by accident with a badly writtenif. Never catch that exception without acting on it.- Changing the amount invalidates the signature. A single modified field and verification fails: that is integrity and authenticity together.
- The receipt carries
patient_ref, not the name. A signed document is hard to withdraw from the world and tends to end up filed away for years. Minimise the personal data it contains (02-04; the legal framework, in 06-03).
Professional validation note. If a signed receipt is going to be used as evidence with legal or tax effect in Spain, the cryptographic signature is a necessary but not sufficient condition: the electronic signature and electronic invoicing rules impose additional requirements on the certificate, the provider and the retention. Validate the design with legal advice before giving it evidential weight; the regulatory aspects are covered in module 6.
- Inspecting an RSA pair with
openssl
opensslEven though the recommendation for new code is ECC, RSA is still in most certificates and in many JWT issuers, so you have to know how to handle it.
# 1. Generate a 3072-bit RSA private key (~128 bits of security)
openssl genpkey -algorithm RSA \
-pkeyopt rsa_keygen_bits:3072 \
-out nimbus_private.pem
chmod 600 nimbus_private.pem # ONLY the owner can read it
# 2. Extract the public key from the pair
openssl pkey -in nimbus_private.pem -pubout -out nimbus_public.pem
# 3. Inspect the public key
openssl pkey -pubin -in nimbus_public.pem -text -noout
# 4. Compare: also generate an Ed25519 pair and look at its size
openssl genpkey -algorithm ed25519 -out nimbus_ed25519.pem
ls -l nimbus_private.pem nimbus_ed25519.pemOutput of step 3 (trimmed):
Public-Key: (3072 bit)
Modulus:
00:c4:1f:9a:e7:2b:8d:05:6f:31:a0:4c:d8:77:e2:
b9:15:3c:80:6a:f4:29:1d:cb:07:5e:a3:44:90:11:
... (384 bytes in total)
Exponent: 65537 (0x10001)Output of step 4:
-rw------- 1 lucia lucia 2484 Aug 2 09:41 nimbus_private.pem
-rw-r--r-- 1 lucia lucia 119 Aug 2 09:41 nimbus_ed25519.pemHow to read these results:
- The modulus is the
n = p*qfrom section 3, here 3072 bits (384 bytes). It is public. The factorspandqare only in the private key. - The public exponent 65537 is the standard value: prime, small and with few bits set to one, which makes the public operation fast. Seeing something different is a warning sign.
- 2484 bytes against 119. The same table from section 4, made visible in the file system.
chmod 600is not decorative. A private key readable by every user on the system is a common audit finding, and it will come up again in 03-06 and in 05-06.- Never use
-des3or weak passwords to protect the private key. If it needs encryption at rest, use modern encryption; and if it is a service key, the right answer is for it to live in a secrets manager or an HSM, not in a file (03-06).
- The question that remains open and the quantum threat
Whose public key is this, really?
The whole lesson has rested on an assumption we have not yet justified. Look again at the exchange in section 5: the mobile app receives a public key over the network and uses it to agree the secret. How does it know that key belongs to Nimbus and not to an attacker sitting in the middle?
If there is no way to check, the MITM works perfectly: the attacker runs an exchange with the app while pretending to be Nimbus and another with Nimbus while pretending to be the app, and decrypts and re-encrypts everything that goes past. Both ends would have impeccable encryption with the wrong person.
Asymmetric cryptography moves the problem, it does not eliminate it. You no longer have to distribute secrets, but you do have to authenticate public keys. That is the problem digital certificates and public key infrastructure solve, and it is the subject of 03-06. The specific way TLS applies it on every connection is covered in 03-05.
Post-quantum cryptography and "harvest now, decrypt later"
A sufficiently large quantum computer could run Shor's algorithm, which breaks RSA, Diffie-Hellman and elliptic curve cryptography all at once. It does not weaken them: it solves them. No such machine exists today, and public estimates talk of years or decades, but that does not make the subject irrelevant today:
| Primitive | Quantum impact | Response |
|---|---|---|
| RSA, DH, ECC | Broken by Shor's algorithm | Migrate to post-quantum algorithms (ML-KEM, ML-DSA, standardised by NIST in 2024) |
| AES-256 | Weakened to ~128 effective bits by Grover | Sufficient. Using 256 bits is enough |
| SHA-256/512 | Weakened, not broken | Sufficient |
Why it matters already: "harvest now, decrypt later". A well-resourced adversary can record encrypted traffic today that they cannot read, store it, and decrypt it once they have the machine. If the data is still sensitive fifteen years from now, the problem belongs to today.
For Nimbus, in practice: appointment data loses its value fairly quickly, so it is not a priority for this year. What is sensible is to (1) use forward secrecy always, which already limits the damage of scenarios of this kind; (2) keep the TLS software up to date, because browsers and libraries are switching on post-quantum hybrid modes (X25519 combined with ML-KEM) transparently; and (3) keep an inventory of where each algorithm is used, which is what will make the migration cheap when the time comes. That cryptographic agility — being able to change algorithm without rewriting the system — is the practical conclusion of the section, and it connects with the version byte we introduced in the encryption format of 03-02.
Common Mistakes and Tips
Conceptual mistakes
- Inverting the use of the keys. You encrypt with the recipient's public key and sign with your own private key. "Encrypting with the private key" protects nothing.
- Believing that a signature encrypts. A signature does not hide the content; it vouches for it. If confidentiality is also needed, you have to encrypt separately.
- Confusing a MAC with a signature. Only the signature gives non-repudiation. If the verifier could be anybody, you need a signature.
- Thinking ECC is "less secure" because it has shorter keys. P-256 is equivalent to RSA-3072. Length is not comparable between families.
- Assuming that a public key received over the network belongs to whoever it claims. It does not, until something certifies it (03-06).
Implementation mistakes
- Encrypting large data with RSA. It does not fit, and it is extremely slow. Use hybrid encryption.
- Using PKCS#1 v1.5 in new code. OAEP for encryption, PSS for signatures; or simply Ed25519, which does not let you choose badly.
- Signing JSON without canonical serialisation. A space or a reordering of fields breaks verification and produces incidents that look like attacks.
- Generating keys with
randomor on devices without good entropy. It is the lesson of 03-01, and in ECDSA signatures it is lethal. - Leaving the private key in a world-readable file, in the repository or in the container image. The problem of 03-06.
- Ignoring the result of
verify. If it does not raise an exception it is valid; if it does, the message is discarded and the event is logged. Never "carry on just in case".
Tips
- For new code: Ed25519 for signing, X25519 for exchanging. If interoperability forces RSA on you, use 3072 bits with OAEP/PSS.
- Always store a key identifier (
kid) alongside the signature. Without it, rotating the key forces you to stop the service; with it, rotation is transparent. It is exactly what the JWKS of 02-05 did and it is developed in 03-06. - Publish your public keys over a channel the client can verify, and plan from the outset how you are going to rotate them.
- Note in your technical inventory which algorithm each component uses. On the day of the post-quantum migration, that document will be the difference between a project and a crisis.
Exercises
Exercise 1 — Choosing the right primitive
For each Nimbus requirement, state: which primitive you would use (symmetric encryption, asymmetric encryption, key exchange, signature or MAC), with which specific algorithm, which key each party uses and why you rule out the alternatives.
- The mobile app and the API must agree a session key without ever having met.
- Nimbus must prove before a court, two years later, that a receipt was issued by its system.
- The API must check that a webhook comes from the payment gateway, with which it shares a configured secret.
- Nimbus wants to send the decryption key for a backup to the consultancy (A-19) for a one-off restore.
- The updates to the agent Nimbus installs on some customers' servers must be verifiable by the customer.
- An 18 MB attachment must be stored encrypted in the A-05 bucket.
Exercise 2 — Explaining forward secrecy
Marta has read in an audit that "the server does not offer forward secrecy" and asks whether that means the traffic is unencrypted.
You are asked to: (a) answer Marta in two paragraphs without formulas; (b) describe the concrete damage scenario at Nimbus if the server's private key were to leak within three years, with and without forward secrecy; (c) explain which specific configuration guarantees it and why TLS 1.3 solves it by design; (d) relate this to the risk of "harvest now, decrypt later".
Exercise 3 — Diagnosing a signature design
A developer proposes this design for the receipts:
import hashlib
SECRET_KEY = "nimbus-2026"
def sign_receipt(receipt: dict) -> str:
text = str(receipt) # (1)
return hashlib.sha256((SECRET_KEY + text).encode()).hexdigest() # (2)
def verify(receipt: dict, signature: str) -> bool:
return sign_receipt(receipt) == signature # (3)You are asked to: (a) identify the problems in the three marked lines; (b) state which security property the author believes he is achieving and which one he actually achieves; (c) explain why this design does not work for the requirement that the clinic verify the receipt on its own; (d) propose the correct design and justify the primitive chosen.
Solutions
Exercise 1
| # | Primitive | Algorithm | Keys | Why not the alternatives |
|---|---|---|---|---|
| 1 | Key exchange | X25519 (ECDHE) inside TLS 1.3 | An ephemeral pair per session at each end | Direct asymmetric encryption would lose forward secrecy; symmetric cryptography cannot distribute itself |
| 2 | Digital signature | Ed25519 (or RSA-PSS if interoperability demands it) | Nimbus's private key signs; the public one verifies | A MAC does not give non-repudiation: Nimbus has the key and could have fabricated it |
| 3 | MAC | HMAC-SHA256 | A shared secret with the gateway | A signature would be needlessly expensive and adds nothing here: the two parties know each other |
| 4 | Hybrid encryption | The backup key encrypted with RSA-OAEP or over X25519, towards the consultancy's public key | The consultancy's public key encrypts; its private key decrypts | Sending the key in the clear by e-mail is the error of 03-01 ex. 1.2. And access must be temporary: rotate afterwards (03-06) |
| 5 | Digital signature | Ed25519 over the artefact | Nimbus's private key in the CI/CD; the public one distributed with the agent | A MAC would force the secret to be shared with every customer: anybody could sign. It is the SolarWinds lesson (02-06, 03-07) |
| 6 | Symmetric encryption | AES-256-GCM with the tenant_id in the AAD |
A data key protected by the KMS master key (envelope) | RSA cannot encrypt 18 MB. The asymmetric part only protects the key |
Exercise 2
(a) Answer to Marta. No: the traffic is encrypted just the same and nobody can read it today. "Forward secrecy" refers to something else — to what would happen in the future if somebody obtained the server's private key. Without forward secrecy, that key would allow every old conversation somebody had recorded to be decrypted retroactively; with it, each session uses an ephemeral secret that is destroyed when it ends, so the past is protected forever.
Put another way: without forward secrecy, the server key is a master key that also opens the historical archive; with forward secrecy, it opens only the doors from today onwards. That is why the audit flags it even though there is no visible problem right now.
(b) Scenario. An attacker records the mobile apps' traffic for eighteen months. Without forward secrecy, on obtaining the private key three years from now they decrypt all eighteen months: clinic diaries, e-mail addresses, telephone numbers and information that indirectly reveals health data. With forward secrecy, they do not decrypt a single session; they could only impersonate the server from that moment on, which is cut off by revoking the certificate and rotating the key (03-06).
(c) Configuration. Allow only suites with ECDHE and disable static RSA key exchange; in practice, require TLS 1.3 — which removed every mode without forward secrecy from the standard — and keep TLS 1.2 only with ECDHE suites while there are old clients. The configuration detail is in 03-05.
(d) Relation to "harvest now, decrypt later". It is the same pattern — record today, decrypt tomorrow — with a different enabler: there the stolen key, here a future quantum computer. Forward secrecy does not protect against the quantum threat on its own (the ephemeral exchange is itself vulnerable to Shor), but it eliminates the most likely and cheapest scenario, and it fits the same response: ephemeral keys, upgradable algorithms and an inventory of where each one is used.
Exercise 3
(a) Problems:
str(receipt)— non-canonical serialisation: it depends on the dictionary's insertion order and on Python'srepr. The same receipt can produce different strings and failed verifications with no attack at all. It is also not interoperable with any other language.sha256(SECRET_KEY + text)— this is the prefix-MAC construction, vulnerable to the length extension attack on SHA-2 style functions: an attacker can append data to the message and compute a valid signature without knowing the key. That is what HMAC exists for, and it is studied in 03-04. And the key, on top of that, is in the code and is a guessable string.==— a comparison of secrets that is not constant-time: it opens a timing side channel (03-01).hmac.compare_digestmust be used.
(b) The author believes he is achieving a signature with non-repudiation. At best he achieves a badly constructed MAC: fragile integrity and authenticity between parties that share the secret, without non-repudiation, and with the key within reach of anybody with access to the repository.
(c) Why it does not work. For the clinic to verify on its own, it would need SECRET_KEY. And as soon as it has it, it can fabricate receipts indistinguishable from Nimbus's. A shared-key scheme cannot provide public verification: that is the structural difference between a MAC and a signature.
(d) Correct design. An Ed25519 signature with Nimbus's private key held in the secrets manager or KMS, over a canonical serialisation (json.dumps(..., sort_keys=True, separators=(",", ":"))), including in the receipt itself a key identifier (kid) and the issue date, and publishing the public key at a verifiable endpoint. That way any clinic verifies without being able to forge, key rotation is possible thanks to the kid, and the receipt has technical evidential value — with the legal validation noted in section 9.
Conclusion
You have seen the idea that solved the problem the previous lesson closed with. A key pair related by a trapdoor function — easy in one direction, infeasible in the other — allows one half to be published without losing anything, and with that the need to distribute prior secrets disappears. You have the most-confused table nailed down: you encrypt with the public key and decrypt with the private one; you sign with the private key and verify with the public one. You know RSA and its hard problem, its current sizes — 2048 as a minimum, 3072 as the recommendation — and its two structural limits, slowness and the impossibility of encrypting large data; and you know elliptic curve cryptography, where 256 bits are equivalent to 3072 of RSA, with Ed25519 for signing and X25519 for exchanging as the default recommendation for new code.
You have understood Diffie-Hellman through the paint analogy and its ephemeral version, ECDHE, from which comes the most valuable property of the lot: forward secrecy, which means that stealing the server's private key three years from now does not allow anything recorded up to today to be read. You have seen hybrid encryption — a symmetric key for the data, asymmetric cryptography only to protect that key — which is the real pattern behind TLS and behind the envelope encryption of Nimbus's attachments. And you have precisely separated signature and MAC: both give integrity and authenticity, but only the signature gives non-repudiation, because the verifier cannot fabricate it. You have practised it by signing a booking receipt with Ed25519 in 64 bytes, checking that altering the amount invalidates the signature, and learning the detail that breaks most real integrations: canonical serialisation.
But the lesson ends with the same honesty as the previous one. Asymmetric cryptography does not eliminate the trust problem: it moves it. You no longer have to distribute secrets, but there is still a question to be answered, without which everything above collapses in the face of an attacker in the middle: this public key I have just received — does it really belong to Nimbus?
In Hash Functions, HMAC and Password Storage (03-04) we take a step back to the primitive that has already come up twice without explanation — the hash that is signed instead of the message — and we study it in depth: what properties it has, why MD5 and SHA-1 are broken, how an HMAC is correctly built to validate the payment gateway webhook, and, at last, how Nimbus should store its users' passwords with Argon2id, salt and calibrated parameters. The answer to the public key question will have to wait for 03-06.
Fundamentals of Information Security Course
Module 1: Introduction to Information Security
- Basic Concepts of Information Security
- Types of Threats and Vulnerabilities
- Principles of Information Security
- Assets, Attack Surface and Threat Actors
Module 2: Cybersecurity
- Definition and Scope of Cybersecurity
- Types of Cyber Attacks
- Social Engineering and Phishing
- Protection Measures in Cybersecurity
- Identity, Authentication and Access Control
- Cybersecurity Incident Case Studies
Module 3: Cryptography
- Introduction to Cryptography
- Symmetric Cryptography
- Asymmetric Cryptography
- Hash Functions, HMAC and Password Storage
- Cryptographic Protocols
- Key Management, Certificates and PKI
- Applications of Cryptography
Module 4: Risk Management and Protection Measures
- Risk Assessment
- Security Policies
- Security Controls
- Third-Party and Supply Chain Risk
- Incident Response Plan
- Disaster Recovery and Business Continuity
Module 5: Security Tools and Techniques
- Vulnerability Analysis Tools
- Monitoring and Detection Techniques
- Penetration Testing
- Network Security
- Application Security
- System Hardening and Endpoint Security
- Cloud and Container Security
Module 6: Best Practices and Regulations
- Best Practices in Information Security
- Security Regulations and Standards
- Personal Data Protection and GDPR in Practice
- Compliance and Auditing
- Training and Awareness
- Ethics, Legal Aspects and Responsible Disclosure
