10  Public Key Cryptography

10.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Explain Public Key Cryptography: Describe how two mathematically related keys enable secure communication without pre-shared secrets
  • Apply RSA Encryption: Calculate RSA key generation parameters and select proper key sizes for IoT key exchange and signatures
  • Implement Diffie-Hellman: Configure DH key exchange to establish shared secrets over insecure channels
  • Evaluate Digital Signatures: Verify message authenticity and non-repudiation using public-key signature schemes
In 60 Seconds

Asymmetric encryption uses mathematically linked key pairs — a public key to encrypt and a private key to decrypt — enabling secure key exchange and digital signatures without sharing secrets in advance.

Asymmetric encryption uses two different keys: a public key (like your mailing address that anyone can know) and a private key (like the key to your mailbox that only you have). Anyone can encrypt a message with your public key, but only your private key can decrypt it. This solves the key-sharing problem of symmetric encryption.

Why it matters for IoT: When you set up a new smart device, it can use the server’s public key to securely establish a connection – no need to pre-share secrets during manufacturing.

“I have a puzzle,” Sammy the Sensor said. “How can I send Max a secret message if we have never met and cannot safely share a key?”

Max the Microcontroller pulled out two keys. “With public key cryptography! I create a key PAIR – a public key and a private key. I share the public key with everyone, like posting my mailing address. But the private key stays secret with me, like the key to my mailbox. Anyone can drop a letter in, but only I can open it and read it.”

“The cool part is digital signatures work in reverse,” Lila the LED added. “When Max signs a message, he uses his PRIVATE key. Then anyone with his PUBLIC key can verify the signature is real. It proves the message truly came from Max and nobody changed it. It is like a wax seal that only Max’s ring can make, but anyone can check if the seal is genuine.”

“For IoT, we love Elliptic Curve Cryptography – ECC,” Bella the Battery said. “It gives the same security as RSA but with much smaller keys. A 256-bit ECC key is as strong as a 3072-bit RSA key! That means faster processing and less energy usage, which is perfect for tiny devices like us that need to save every drop of battery power.”

10.2 How Asymmetric Encryption Works

Asymmetric encryption (public-key cryptography) uses two related keys: a public key for encryption and a private key for decryption.

Asymmetric encryption flow diagram showing sender encrypting plaintext message with recipient's public key to create ciphertext, transmitting ciphertext over insecure channel, and recipient decrypting ciphertext with their private key to recover original plaintext message
Figure 10.1: Asymmetric encryption: encrypt with public key, decrypt with private key

Mathematical Relationship:

  • Keys are mathematically related but computationally infeasible to derive one from the other
  • Encryption with public key -> decryption with private key
  • Signing with private key -> verification with public key

Advantages:

  • Solves key distribution problem
  • No secure channel needed for public key exchange
  • Digital signatures enable authentication
  • Non-repudiation support

Disadvantages:

  • 100-1000x slower than symmetric encryption
  • Not suitable for bulk data encryption
  • Public keys need authentication (certificates)
  • Key loss means permanent data loss

10.3 RSA Cryptosystem

History: Developed by Rivest, Shamir, and Adleman, published in 1978.

RSA relies on a mathematical “one-way door” – operations that are easy in one direction but nearly impossible to reverse:

Direction Difficulty Example
Forward (easy) Milliseconds Multiply 2 large primes: 61 x 53 = 3,233
Backward (hard) Years Factor 3,233 into primes: ? x ? = 3,233

For small numbers, factoring is trivial. But RSA uses 617-digit numbers (2048 bits). Factoring those would take all the world’s computers billions of years.

The Elegant Trick:

  1. You know a secret shortcut - If you chose the original primes (p and q), you can easily compute the private key
  2. Attackers see only the product - They would need to factor the huge number to find your primes
  3. Math guarantees it works - Euler’s theorem ensures that encrypting then decrypting returns the original message

The Catch: Quantum Computers

Future quantum computers could factor large numbers quickly using Shor’s algorithm. That’s why post-quantum cryptography is being developed. For now, RSA-2048+ remains secure against classical computers.

10.3.1 RSA Key Generation

  1. Choose two large prime numbers \(p\) and \(q\)
  2. Compute \(n = p \times q\) (modulus)
  3. Compute \(\phi(n) = (p-1)(q-1)\) (Euler’s totient)
  4. Choose public exponent \(e\) (typically 65537)
  5. Compute private exponent \(d\) such that \(ed \equiv 1 \pmod{\phi(n)}\)

Public key: \((n, e)\) Private key: \((n, d)\)

Encryption: \(c = m^e \mod n\) Decryption: \(m = c^d \mod n\)

RSA security relies on the computational hardness of factoring large composite numbers \(N = p \times q\) into prime factors.

\[N = p \times q, \quad \phi(N) = (p-1)(q-1), \quad e \cdot d \equiv 1 \pmod{\phi(N)}\]

Working through an example:

Given: RSA-2048 with 617-digit modulus \(N\), public exponent \(e = 65537\), private exponent \(d\).

Step 1: Key generation (simplified example with small primes) - Choose \(p = 61\), \(q = 53\) - Compute \(N = 61 \times 53 = 3233\) - Compute \(\phi(N) = (61-1)(53-1) = 60 \times 52 = 3120\)

Step 2: Select public exponent - Choose \(e = 17\) (must be coprime with \(\phi(N) = 3120\)) - Verify: \(\gcd(17, 3120) = 1\)

Step 3: Compute private exponent - Find \(d\) such that \(e \cdot d \equiv 1 \pmod{3120}\) - Using extended Euclidean algorithm: \(d = 2753\) - Verify: \((17 \times 2753) \mod 3120 = 46801 \mod 3120 = 1\)

Result: Public key \((N=3233, e=17)\), Private key \((N=3233, d=2753)\). For RSA-2048 production keys, \(p\) and \(q\) are each 1024-bit primes (309 digits), making \(N\) a 617-digit number.

In practice: Factoring RSA-2048’s 617-digit \(N\) requires \(\approx 2^{112}\) operations (vs AES-128’s \(2^{128}\) brute force). Best known factoring algorithms take centuries on supercomputers – but Shor’s quantum algorithm could factor in polynomial time, driving post-quantum migration.

10.3.2 RSA Key Size Recommendations

Key Size Security Level Use Case
RSA-2048 ~112-bit Minimum acceptable, sufficient until ~2030
RSA-3072 ~128-bit Recommended for new deployments (10+ year lifespan)
RSA-4096 ~152-bit Maximum security, rarely needed (very slow)
Never Use RSA-1024

RSA-1024 has been deprecated since 2010 and is considered insecure. Academic attacks have demonstrated factorization of 768-bit RSA (2009). Always use RSA-2048 or higher.

Objective: Generate an RSA key pair and use it to encrypt and decrypt a sensor reading.

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization

# Step 1: Generate RSA-2048 key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# Step 2: Encrypt a sensor command with the public key
message = b"CMD:unlock_door,device_id=ESP32_001"
ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)
print(f"Plaintext:  {message.decode()}")
print(f"Ciphertext: {ciphertext[:32].hex()}... ({len(ciphertext)} bytes)")

# Step 3: Decrypt with the private key
decrypted = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)
print(f"Decrypted:  {decrypted.decode()}")
print(f"Match: {message == decrypted}")

What to Observe:

  1. The ciphertext is always 256 bytes (2048 bits) regardless of message length
  2. Each encryption produces different ciphertext (OAEP adds randomness)
  3. Only the private key can decrypt – the public key cannot reverse the operation
Try It: RSA Encryption & Decryption Calculator

10.4 Digital Signatures

Digital signatures prove that a message came from a specific sender and has not been modified.

Digital signature verification process for firmware updates showing manufacturer hashing firmware file, signing hash with private key, and device verifying signature by decrypting with manufacturer's public key and comparing to computed hash of received firmware
Figure 10.2: Digital signature verification for firmware updates

Signature Process:

  1. Hash the data - Create a fixed-size fingerprint with SHA-256
  2. Sign the hash - Apply the private key operation to create a signature
  3. Send data + signature - Recipient receives both
  4. Verify - Apply public key operation to signature, compare result to independently computed hash

IoT Use Cases:

  • Firmware update verification
  • Command authentication
  • Device attestation
  • Non-repudiation for transactions

Objective: Sign a firmware hash and verify it, simulating how IoT devices validate updates.

from cryptography.hazmat.primitives.asymmetric import rsa, padding, utils
from cryptography.hazmat.primitives import hashes
import hashlib

# Manufacturer generates key pair (done once)
manufacturer_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
manufacturer_public = manufacturer_key.public_key()

# Simulate firmware binary
firmware = b"ESP32_firmware_v2.1_binary_data_here..." * 100

# Manufacturer signs the firmware
firmware_hash = hashlib.sha256(firmware).digest()
signature = manufacturer_key.sign(
    firmware_hash,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    utils.Prehashed(hashes.SHA256())
)
print(f"Firmware size: {len(firmware)} bytes")
print(f"SHA-256: {firmware_hash.hex()[:32]}...")
print(f"Signature: {signature[:32].hex()}...")

# Device verifies signature
try:
    manufacturer_public.verify(
        signature,
        firmware_hash,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        utils.Prehashed(hashes.SHA256())
    )
    print("\n[VERIFIED] Firmware is authentic - safe to install")
except Exception:
    print("\n[REJECTED] Signature invalid - firmware tampered!")

# Simulate tampered firmware
tampered_firmware = firmware + b"MALICIOUS_CODE"
tampered_hash = hashlib.sha256(tampered_firmware).digest()
try:
    manufacturer_public.verify(
        signature,
        tampered_hash,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        utils.Prehashed(hashes.SHA256())
    )
    print("[VERIFIED] Firmware is authentic")
except Exception:
    print("[REJECTED] Tampered firmware detected!")

What to Observe:

  1. The signature is created with the private key (only the manufacturer has this)
  2. Verification uses only the public key (embedded in every device)
  3. Even a single byte change in firmware causes signature verification to fail
Try It: Digital Signature Simulator

10.5 Diffie-Hellman Key Exchange

Diffie-Hellman allows two parties to establish a shared secret over an insecure channel without transmitting the secret itself.

Diffie-Hellman key exchange protocol diagram showing Alice and Bob establishing a shared secret over insecure channel by exchanging public values computed from private secrets and public parameters, with mathematical formulas showing modular exponentiation operations
Figure 10.3: Diffie-Hellman: establishing shared secrets over insecure channels

Mathematical Foundation:

Given public \(p\) (prime) and \(g\) (generator):

  1. Alice chooses secret \(a\), computes \(A = g^a \mod p\)
  2. Bob chooses secret \(b\), computes \(B = g^b \mod p\)
  3. They exchange \(A\) and \(B\) publicly
  4. Alice computes \(s = B^a \mod p = g^{ab} \mod p\)
  5. Bob computes \(s = A^b \mod p = g^{ab} \mod p\)

Both arrive at the same shared secret \(s = g^{ab} \mod p\).

Security: Even seeing p, g, A, and B, an attacker cannot compute s without solving the discrete logarithm problem (computationally infeasible for large primes).

Objective: Watch two IoT devices establish a shared secret over an insecure channel.

from cryptography.hazmat.primitives.asymmetric import dh
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes

# Generate shared parameters (published openly)
parameters = dh.generate_parameters(generator=2, key_size=2048)

# Device A generates its key pair
device_a_private = parameters.generate_private_key()
device_a_public = device_a_private.public_key()

# Device B generates its key pair
device_b_private = parameters.generate_private_key()
device_b_public = device_b_private.public_key()

# Exchange public keys over insecure channel
# (An eavesdropper sees both public keys but cannot compute the shared secret)

# Device A computes shared secret using B's public key
shared_a = device_a_private.exchange(device_b_public)

# Device B computes shared secret using A's public key
shared_b = device_b_private.exchange(device_a_public)

print(f"Device A shared secret: {shared_a[:16].hex()}...")
print(f"Device B shared secret: {shared_b[:16].hex()}...")
print(f"Secrets match: {shared_a == shared_b}")

# Derive a usable AES key from the shared secret
aes_key = HKDF(
    algorithm=hashes.SHA256(), length=32,
    salt=None, info=b"iot-session-key"
).derive(shared_a)
print(f"\nDerived AES-256 key: {aes_key.hex()[:32]}...")
print("Both devices now share an AES key for symmetric encryption!")

What to Observe:

  1. Both devices independently compute the same shared secret
  2. Only public values are exchanged – the private keys never leave the device
  3. The raw shared secret is derived into a proper AES key using HKDF
Try It: Diffie-Hellman Key Exchange Step-by-Step

10.6 Algorithm Comparison

Comparison table of asymmetric cryptography algorithms showing RSA, ECDH, ECDSA, and Ed25519 with their key sizes, performance characteristics, and recommended use cases for IoT deployments
Figure 10.4: Asymmetric algorithm comparison
Algorithm Key Size for 128-bit Security Speed Best For
RSA 3072 bits Slow Legacy systems, wide compatibility
ECDH 256 bits Fast IoT key exchange
ECDSA 256 bits Fast IoT digital signatures
Ed25519 256 bits Very Fast Modern signatures

Objective: Elliptic Curve Diffie-Hellman (ECDH) provides the same security as classical DH with dramatically smaller keys. A 256-bit ECC key offers security equivalent to a 3072-bit RSA key, making ECDH the preferred key exchange for resource-constrained IoT devices.

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes, serialization
import time

# ── Compare key sizes: RSA vs ECC ──
print("=== Key Size Comparison (equivalent security) ===")
sizes = [
    ("RSA-2048 / ECC-224",  "112-bit", 2048, 224),
    ("RSA-3072 / ECC-256",  "128-bit", 3072, 256),
    ("RSA-7680 / ECC-384",  "192-bit", 7680, 384),
    ("RSA-15360 / ECC-521", "256-bit", 15360, 521),
]
print(f"  {'Match':<25} {'Security':<12} {'RSA bits':<10} {'ECC bits':<10} {'Ratio'}")
for name, sec, rsa_bits, ecc_bits in sizes:
    print(f"  {name:<25} {sec:<12} {rsa_bits:<10} {ecc_bits:<10} {rsa_bits/ecc_bits:.0f}x")

# ── ECDH Key Exchange between two IoT devices ──
print("\n=== ECDH Key Exchange (SECP256R1 / P-256) ===")

# Device A (e.g., temperature sensor) generates ephemeral key pair
start = time.perf_counter()
device_a_key = ec.generate_private_key(ec.SECP256R1())
keygen_time = (time.perf_counter() - start) * 1000
device_a_public = device_a_key.public_key()

# Device B (e.g., gateway) generates ephemeral key pair
device_b_key = ec.generate_private_key(ec.SECP256R1())
device_b_public = device_b_key.public_key()

# Show public key sizes (what gets transmitted over the wire)
a_pub_bytes = device_a_public.public_bytes(
    serialization.Encoding.X962,
    serialization.PublicFormat.CompressedPoint
)
print(f"  Public key size (compressed): {len(a_pub_bytes)} bytes")
print(f"  Key generation time: {keygen_time:.1f} ms")

# Each device computes the shared secret using the other's public key
start = time.perf_counter()
shared_a = device_a_key.exchange(ec.ECDH(), device_b_public)
exchange_time = (time.perf_counter() - start) * 1000

shared_b = device_b_key.exchange(ec.ECDH(), device_a_public)

print(f"  Key exchange time: {exchange_time:.1f} ms")
print(f"  Raw shared secret: {shared_a[:16].hex()}...")
print(f"  Secrets match: {shared_a == shared_b}")

# Derive a usable AES-256 key from the raw shared secret
aes_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=b"iot-sensor-session-v1"
).derive(shared_a)
print(f"  Derived AES-256 key: {aes_key[:16].hex()}...")
print(f"\n  Both devices now share an AES key for symmetric encryption")
print(f"  Only 33 bytes were sent over the air (compressed public key)")

What to Observe:

  1. ECC-256 provides 128-bit security with keys 12x smaller than RSA-3072 – critical for bandwidth-constrained IoT
  2. The compressed public key is only 33 bytes, small enough for a single BLE advertisement packet
  3. Key generation and exchange complete in milliseconds even on general-purpose hardware
  4. The raw ECDH output is passed through HKDF to derive a proper AES key with domain separation
  5. An eavesdropper seeing both public keys cannot compute the shared secret (Elliptic Curve Discrete Logarithm Problem)
Try It: ECC vs RSA Key Size and Bandwidth Explorer

10.7 Hybrid Encryption: Best of Both Worlds

In practice, IoT systems use both symmetric and asymmetric encryption together:

Hybrid encryption architecture diagram showing asymmetric cryptography used for secure session key exchange followed by symmetric encryption with AES for fast bulk data encryption, combining security of public key cryptography with performance of symmetric ciphers
Figure 10.5: Hybrid encryption: asymmetric for key exchange, symmetric for data

Why Hybrid?

  1. Asymmetric (RSA/ECDH): Securely exchange session keys (slow, but only once)
  2. Symmetric (AES): Encrypt all data with session key (fast, continuous)

This is exactly how TLS/HTTPS works – and why it is the standard for IoT security.

10.8 Certificate Chains and Trust

Certificate chains establish trust hierarchies for authenticating IoT devices and servers.

Objective: Verify a certificate chain like an IoT device validates a server’s identity during TLS handshake. This demonstrates Root CA -> Intermediate CA -> End-Entity certificate trust.

from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
import datetime

# ── Step 1: Create a self-signed Root CA ──
root_key = ec.generate_private_key(ec.SECP256R1())
root_name = x509.Name([
    x509.NameAttribute(NameOID.ORGANIZATION_NAME, "IoT Manufacturer CA"),
    x509.NameAttribute(NameOID.COMMON_NAME, "Root CA"),
])
root_cert = (
    x509.CertificateBuilder()
    .subject_name(root_name)
    .issuer_name(root_name)
    .public_key(root_key.public_key())
    .serial_number(x509.random_serial_number())
    .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
    .not_valid_after(datetime.datetime.now(datetime.timezone.utc)
                     + datetime.timedelta(days=3650))
    .add_extension(x509.BasicConstraints(ca=True, path_length=1),
                   critical=True)
    .sign(root_key, hashes.SHA256())
)
print("=== Certificate Chain Verification ===")
print(f"  Root CA: {root_cert.subject.rfc4514_string()}")
print(f"  Valid: {root_cert.not_valid_before_utc.date()} to "
      f"{root_cert.not_valid_after_utc.date()}")

# ── Step 2: Issue Intermediate CA signed by Root ──
inter_key = ec.generate_private_key(ec.SECP256R1())
inter_name = x509.Name([
    x509.NameAttribute(NameOID.ORGANIZATION_NAME, "IoT Manufacturer CA"),
    x509.NameAttribute(NameOID.COMMON_NAME, "Device Signing CA"),
])
inter_cert = (
    x509.CertificateBuilder()
    .subject_name(inter_name)
    .issuer_name(root_name)
    .public_key(inter_key.public_key())
    .serial_number(x509.random_serial_number())
    .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
    .not_valid_after(datetime.datetime.now(datetime.timezone.utc)
                     + datetime.timedelta(days=1825))
    .add_extension(x509.BasicConstraints(ca=True, path_length=0),
                   critical=True)
    .sign(root_key, hashes.SHA256())  # Signed by Root CA
)
print(f"  Intermediate: {inter_cert.subject.rfc4514_string()}")

# ── Step 3: Issue device certificate signed by Intermediate ──
device_key = ec.generate_private_key(ec.SECP256R1())
device_name = x509.Name([
    x509.NameAttribute(NameOID.COMMON_NAME, "ESP32-SENSOR-00142"),
])
device_cert = (
    x509.CertificateBuilder()
    .subject_name(device_name)
    .issuer_name(inter_name)
    .public_key(device_key.public_key())
    .serial_number(x509.random_serial_number())
    .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
    .not_valid_after(datetime.datetime.now(datetime.timezone.utc)
                     + datetime.timedelta(days=365))
    .sign(inter_key, hashes.SHA256())  # Signed by Intermediate
)
print(f"  Device cert: {device_cert.subject.rfc4514_string()}")

# ── Step 4: Verify the chain (device -> intermediate -> root) ──
print("\n  Verifying chain:")

# Verify device cert was signed by intermediate
try:
    inter_key.public_key().verify(
        device_cert.signature,
        device_cert.tbs_certificate_bytes,
        ec.ECDSA(hashes.SHA256())
    )
    print("    [OK] Device cert signed by Intermediate CA")
except Exception:
    print("    [FAIL] Device cert signature invalid")

# Verify intermediate cert was signed by root
try:
    root_key.public_key().verify(
        inter_cert.signature,
        inter_cert.tbs_certificate_bytes,
        ec.ECDSA(hashes.SHA256())
    )
    print("    [OK] Intermediate CA signed by Root CA")
except Exception:
    print("    [FAIL] Intermediate cert signature invalid")

print("    [OK] Chain verified: Device -> Intermediate -> Root")
print(f"\n  Chain depth: 3 certificates")
print(f"  All using ECC P-256 (33-byte public keys)")

What to Observe:

  1. The Root CA is self-signed (issuer == subject) and has ca=True – this is the trust anchor embedded in devices
  2. Each certificate is signed by its parent’s private key, creating a verifiable chain
  3. Verification only needs the parent’s public key – private keys stay in their respective HSMs
  4. If the Intermediate CA key is compromised, revoke it without replacing the Root CA
  5. All certificates use ECC P-256, keeping the entire chain under 1 KB – suitable for constrained devices
Try It: Certificate Chain Trust Simulator

Scenario: You manufacture 10,000 smart locks deployed in hotels worldwide. You need to push a critical security patch that fixes a vulnerability in the Bluetooth pairing process. The firmware update must be cryptographically signed to prevent attackers from uploading malicious firmware.

Implementation:

  1. Key Generation (performed once, secured in HSM):

    # Generate manufacturer RSA-2048 key pair
    openssl genrsa -out manufacturer_key.pem 2048
    openssl rsa -in manufacturer_key.pem -pubout -out manufacturer_pubkey.pem
    
    # Key sizes:
    # Private key: 2048 bits (256 bytes), stored in Hardware Security Module
    # Public key: 2048 bits, embedded in every lock's firmware during manufacturing
  2. Signing the Firmware (performed by CI/CD during release):

    # Compute SHA-256 hash of firmware binary
    openssl dgst -sha256 -binary firmware_v2.1.bin > firmware.hash
    
    # Sign the hash with manufacturer's private key (RSA-PSS padding)
    openssl pkeyutl -sign -inkey manufacturer_key.pem \
      -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:32 \
      -in firmware.hash -out firmware.sig
    
    # Signature size: 256 bytes
  3. Verification on Device (ESP32 lock firmware):

    bool verifyFirmwareUpdate(const uint8_t* firmware, size_t len,
                               const uint8_t* signature, size_t sig_len) {
      mbedtls_pk_context pk;
      mbedtls_pk_init(&pk);
    
      // Load embedded manufacturer public key (512 bytes)
      const uint8_t manufacturer_pubkey[] = {0x30, 0x82, 0x01, ...};
      mbedtls_pk_parse_public_key(&pk, manufacturer_pubkey, 512);
    
      // Compute SHA-256 hash of downloaded firmware
      uint8_t hash[32];
      mbedtls_sha256(firmware, len, hash, 0);
    
      // Verify signature matches hash
      int ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA256, hash, 32,
                                   signature, sig_len);
    
      mbedtls_pk_free(&pk);
      return (ret == 0);  // 0 = signature valid, -0x4E00 = invalid
    }
  4. Update Process:

    • Cloud server sends: firmware_v2.1.bin (128 KB) + firmware.sig (256 bytes)
    • Lock downloads both files
    • Lock verifies signature before writing to flash
    • If signature invalid -> reject update, log security event
    • If signature valid -> install update, reboot

Performance Measurements (ESP32 @ 240 MHz): - SHA-256 hashing: 128 KB firmware @ 15 MB/s = 8.5 ms - RSA-2048 signature verification: 42 ms - Total verification time: 50.5 ms - Power consumption: 0.08 mAh (negligible)

Security Analysis:

  • Attacker capabilities: Reverse-engineer one lock’s firmware -> extract public key
  • Attack scenario: Attacker creates malicious firmware, tries to sign with guessed private key
  • Why it fails: Factoring the 2048-bit RSA modulus requires 2^112 operations (impossible even with all computers on Earth)
  • Result: Only the manufacturer (possessing private key in HSM) can sign valid firmware

Real-World Impact: In 2019, a smart lock competitor used unsigned firmware updates. Attackers uploaded malicious firmware that disabled all locks, causing $12M in hotel lockout costs (4,500 hotels, emergency locksmith fees). With RSA-2048 signature verification, this attack is cryptographically impossible.

Cost-Benefit:

  • Implementation cost: 50 ms verification time (acceptable for background update)
  • Security benefit: Prevents $12M+ losses from malicious firmware
  • Hardware cost: $0 (RSA verification uses existing ESP32 CPU)
Try It: Firmware Signing and Verification Flow

10.9 Knowledge Check

An IoT manufacturer wants to push signed firmware updates to 100,000 deployed devices. Which key should they use to SIGN the firmware, and which key should devices use to VERIFY it?

Options:

    1. Sign with public key, verify with private key
    1. Sign with manufacturer’s private key, verify with manufacturer’s public key embedded in devices
    1. Use symmetric encryption – same key for sign and verify
    1. Sign with each device’s private key, verify with device’s public key

Correct: B

The manufacturer signs firmware with their private key (kept secret). Each device has the manufacturer’s public key pre-installed and uses it to verify signatures. Since only the manufacturer has the private key, only they can create valid signatures. This is how secure boot and firmware verification work.

During a Diffie-Hellman key exchange, an attacker intercepts the public values A and B exchanged between Alice and Bob over an insecure channel. Can the attacker compute the shared secret?

Options:

    1. Yes, because A and B together reveal the shared secret
    1. No, computing the shared secret from A and B requires solving the discrete logarithm problem
    1. Yes, if they also know the public parameters p and g
    1. No, but only if the channel uses TLS encryption

Correct: B

This is the mathematical foundation of Diffie-Hellman security. The attacker sees p, g, A (g^a mod p), and B (g^b mod p), but computing a or b from these values is the discrete logarithm problem – computationally infeasible for sufficiently large primes. The parameters p and g are intentionally public (published in RFC 3526). Only Alice and Bob can compute s = g^ab mod p.

Algorithm Comparison for Equivalent 128-bit Security Level:

Feature RSA-3072 ECC P-256 (ECDSA) Ed25519 Best For
Key Size 3072 bits (384 bytes) 256 bits (32 bytes) 256 bits (32 bytes) ECC/Ed25519: 12x smaller
Signature Size 384 bytes 64 bytes 64 bytes ECC/Ed25519: 6x smaller
Sign Speed 15 ms 8 ms 2 ms Ed25519: 7x faster
Verify Speed 0.5 ms 16 ms 5 ms RSA: fastest verify
TLS/DTLS Support Full support Full support Full (TLS 1.3+) All: good compatibility
FIPS Approved Yes (FIPS 186-5) Yes (FIPS 186-5) Yes (FIPS 186-5, EdDSA) All three: regulatory compliance
Quantum Resistance None None None All three vulnerable to Shor’s algorithm
Battery Impact (1000 ops) 2.5 mAh 1.3 mAh 0.4 mAh Ed25519: lowest power

Decision Tree:

  1. Is regulatory compliance required? (FIPS 140-2/3, Common Criteria)
    • YES -> Use ECDSA P-256 (NIST-approved) or RSA-3072 (if >128 KB RAM available)
    • NO -> Consider Ed25519 for better performance
  2. What is the primary use case?
    • Firmware signatures (verify often, sign rarely) -> RSA-3072 (0.5 ms verify is fastest)
    • Device authentication (mutual TLS handshake) -> ECDSA P-256 (8x smaller certificates than RSA)
    • Message signing (continuous operation) -> Ed25519 (7x faster signing saves battery)
  3. How much RAM is available?
    • >128 KB RAM -> Any algorithm works, choose based on use case
    • 64-128 KB RAM -> Use ECC P-256 or Ed25519 (RSA-3072 stack usage is problematic)
    • <64 KB RAM -> Must use Ed25519 (smallest stack footprint)
  4. What is the network bandwidth constraint?
    • BLE, Zigbee, LoRaWAN (constrained bandwidth) -> Ed25519 (64-byte signatures vs 384-byte RSA)
    • Wi-Fi, Ethernet (ample bandwidth) -> Any algorithm acceptable
    • Cellular (pay-per-byte) -> Ed25519 saves data costs (6x smaller signatures)
  5. Is quantum computing a concern? (10+ year device lifespan)
    • YES -> Plan migration to ML-KEM and ML-DSA (NIST post-quantum standards)
    • NO -> Current algorithms sufficient

Best Practices:

  • Default recommendation: ECC P-256 (ECDSA) for device certificates and TLS/DTLS (widest compatibility)
  • Optimize for performance: Ed25519 for high-frequency signing operations (logging, telemetry)
  • Optimize for verification: RSA-3072 for firmware signatures (verify on device, sign on server)
  • Never use RSA-1024: Factored in academic research, considered broken
Common Mistake: Using the Same RSA Key Pair for Encryption AND Signing

What Developers Do Wrong: To simplify key management, developers generate one RSA-2048 key pair per device and use it for BOTH encrypting data (with public key) and signing firmware updates (with private key). This seems efficient – one key pair handles all cryptographic needs.

Why It Fails: Using the same RSA key for encryption and signing creates a cryptographic cross-protocol attack surface:

  1. Mathematical relationship exploitation: RSA signatures and encryption use the same modular exponentiation operation. An attacker can sometimes trick a device into signing a specially crafted “message” that is actually an encrypted session key, allowing the attacker to decrypt communications.

  2. Bleichenbacher’s attack variant: If the device accepts signature requests without validating the message format, an attacker can submit ciphertexts as “messages to sign.” The resulting signature leaks information about the private key exponent through padding oracle attacks.

  3. Key compromise blast radius: If the signing key is compromised (e.g., through firmware reverse engineering), the encryption key is simultaneously compromised, exposing ALL encrypted data and future signatures.

Real-World Example: In 2017, a medical IoT device manufacturer used one RSA-2048 key pair per device for both encrypting patient vitals (data confidentiality) and signing control commands (authentication). Security researchers discovered they could: - Capture encrypted patient data from the device - Submit the ciphertext to the device’s “sign this command” API - The device signed the ciphertext (thinking it was a valid command) - Researchers used the signature to compute parts of the private key - After collecting 1,200 signatures, they recovered the full private key - Result: HIPAA breach affecting 23,000 patients, $1.2M fine

Correct Approach: Always use separate key pairs for different cryptographic purposes:

// CORRECT: Two distinct key pairs
RSA_KeyPair encryption_keypair;   // For encrypting session keys
RSA_KeyPair signing_keypair;      // For signing firmware/messages

// Generate separate keys during manufacturing
generate_rsa_keypair(&encryption_keypair, 2048);
generate_rsa_keypair(&signing_keypair, 2048);

// Use encryption key ONLY for encryption
encrypt_data(plaintext, encryption_keypair.public_key);

// Use signing key ONLY for signatures
sign_message(message, signing_keypair.private_key);

Even Better: Use Different Algorithms:

  • Encryption: Use ECDH (Elliptic Curve Diffie-Hellman) for key exchange -> derive AES session keys
  • Signing: Use ECDSA or Ed25519 for digital signatures

This eliminates the possibility of cross-protocol attacks because the mathematical operations are completely different.

The Cost: In the medical IoT breach, the manufacturer saved $0.15 per device by using one key pair instead of two. The HIPAA fine ($1.2M) and remediation costs ($3.8M) resulted in a net loss of $5M – a 33 million times cost increase from the $0.15 “savings” per device.

Key Takeaway: RSA key pairs are NOT multipurpose tools. One key = one purpose. Violating this principle creates mathematical attack vectors that skilled attackers WILL exploit.

Concept Relationships
Concept Depends On Enables Trade-off
Public Key Cryptography Mathematical one-way functions Key exchange without pre-shared secrets 100-1000x slower than symmetric
RSA Prime factorization difficulty Encryption + signatures Large keys (2048+ bits)
Diffie-Hellman Discrete logarithm problem Secure key agreement Requires authentication to prevent MITM
Digital Signatures Hash functions + asymmetric crypto Authentication + non-repudiation Larger than MAC tags
ECC Elliptic curve discrete log RSA-equivalent security, 10x smaller keys Less widely understood than RSA
Hybrid Encryption Both symmetric and asymmetric Fast bulk encryption + secure key exchange Implementation complexity

Common Confusion: “Use RSA for all IoT encryption” – NO. RSA is too slow for bulk data. Use RSA/ECC for key exchange, then AES for data.

Key Concepts

  • Public Key: The freely shareable component of an asymmetric key pair; used to encrypt data or verify digital signatures.
  • Private Key: The secret component of an asymmetric key pair; used to decrypt data or create digital signatures. Must never be shared.
  • RSA: A widely used asymmetric algorithm based on the difficulty of factoring large numbers; key sizes of 2048+ bits are recommended.
  • ECC (Elliptic Curve Cryptography): An asymmetric algorithm based on elliptic curve mathematics; provides equivalent security to RSA with much smaller key sizes (e.g., 256-bit ECC ≈ 3072-bit RSA).
  • Digital Signature: A cryptographic value computed with a private key that allows anyone with the corresponding public key to verify authenticity and non-repudiation.
  • Key Exchange: A protocol by which two parties derive a shared secret over an insecure channel without transmitting the secret directly (e.g., ECDH, Diffie-Hellman).
  • PKI (Public Key Infrastructure): The ecosystem of policies, software, and hardware used to create, manage, distribute, and revoke digital certificates built on asymmetric cryptography.

Place the steps of a TLS-style hybrid encryption session in correct order:

Common Pitfalls

RSA and ECC operations are computationally expensive — 100–1000x slower than AES. Encrypting large sensor data streams with RSA will overwhelm constrained processors. Use asymmetric crypto only for key exchange; encrypt bulk data with AES.

Key generation requires high-quality randomness. Embedded devices booting from cold often have predictable entropy (same boot time, same sensor readings). Use a hardware RNG or seed the PRNG from multiple sources before generating keys.

Private keys stored in plaintext in flash can be extracted via physical attacks or firmware dumps. Use secure elements, TrustZone, or encrypted key stores with a device-unique root key.

Code that receives a signed message must check the verification result and reject invalid signatures. Ignoring or logging-only failures allows attackers to substitute arbitrary data.

10.10 Summary

  • Asymmetric encryption uses two keys: public (encrypt) and private (decrypt)
  • RSA is based on the difficulty of factoring large prime products – use RSA-2048 minimum
  • Diffie-Hellman enables shared secret establishment over insecure channels
  • Digital signatures prove authenticity: sign with private key, verify with public key
  • ECC provides equivalent security to RSA with 10x smaller keys (ideal for IoT)
  • Hybrid encryption combines both: asymmetric for key exchange, symmetric for bulk data
  • Never reuse the same key pair for encryption and signing – separate keys for separate purposes

10.11 What’s Next

Continue to Elliptic Curve Cryptography (ECC) to learn why ECC is the preferred choice for resource-constrained IoT devices, offering RSA-equivalent security with dramatically smaller keys and faster operations.

Direction Chapter Focus
Prerequisites Encryption Principles Symmetric encryption fundamentals
Prerequisites Hash Functions Digital signatures use hashes
Next Elliptic Curve Cryptography Modern alternative to RSA for IoT
Next Key Management Safely storing and rotating asymmetric keys
Related Key Renewal (E5) Using RSA/ECC for periodic key refresh
Advanced TLS/DTLS Security How hybrid encryption works in TLS
Advanced Post-Quantum Cryptography Future of asymmetric crypto