TLS 1.3 IoT: Handshake, 0-RTT, mTLS and Performance Costs on Constrained Devices (2026)
A battery-powered sensor that reports once an hour has a surprisingly lopsided budget. The payload is perhaps forty bytes of telemetry, but the security wrapper around it, if the device opens a fresh connection every time, can be fifty to a hundred times larger and cost more energy than the measurement itself. That ratio, not the cipher strength, is the real engineering problem when you adopt TLS 1.3 IoT designs in the field.
TLS 1.3 (RFC 8446) is a better fit for small devices than TLS 1.2 in several ways: one fewer round trip, no legacy cipher baggage, encrypted certificates, and a clean pre-shared-key resumption path. It also introduces a feature, 0-RTT early data, that is tempting on a device with a 200 ms radio wake-up and dangerous if you misunderstand replay. This article walks the handshake byte by byte, prices it in flash, RAM, time and joules (with clearly labelled illustrative numbers), and explains mutual authentication, resumption, DTLS 1.3 for UDP, and the failure modes that actually take fleets offline.
What this covers: the TLS 1.3 handshake message flow, what it costs on a microcontroller, 0-RTT and its replay limits, session tickets and PSK resumption, mTLS provisioning and rotation, certificate and key-type choices, DTLS 1.3 with CoAP, MQTT over TLS, post-quantum hybrid status, and a failure-mode catalogue with a recommended baseline.
Context and Background
For about a decade the embedded world ran TLS 1.2 (RFC 5246) with a hand-picked cipher suite, often TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 or an ECDHE-RSA-AES-GCM variant, following the profile in RFC 7925, the IETF’s “TLS/DTLS Profiles for the Internet of Things”. That profile made sense: it told implementers which of TLS 1.2’s many options to ignore. TLS 1.3 solves the same problem differently by deleting the options. Static RSA key transport, CBC modes, RC4, compression, renegotiation and custom Diffie-Hellman groups are simply gone. What remains is a short list of AEAD cipher suites and an ephemeral key exchange that provides forward secrecy on every connection.
The protocol was finalised in August 2018. Current best-practice guidance for TLS deployments is in RFC 9325 (BCP 195), which tells operators to prefer TLS 1.3 and, where TLS 1.2 must remain, to restrict it to forward-secret AEAD suites. For constrained devices the headline is that TLS 1.3 is not heavier than TLS 1.2 in code size when you strip the legacy options, and is lighter on the wire in the common case. The cost shifts: you pay for an elliptic-curve key agreement and a signature on every full handshake, and you must plan for the new resumption model.
Three realities frame every decision in this space. First, device classes differ by orders of magnitude. An Arm Cortex-M0+ with 32 KB of RAM and 256 KB of flash is not a Cortex-M33 with 512 KB of RAM, which is not a Linux gateway. The IETF constrained-node terminology in RFC 7228 classifies devices by roughly 10 KB and 100 KB of RAM and flash boundaries; most real TLS targets sit in Class 2 or above. Second, the network is the expensive part. On NB-IoT, LTE-M or LoRaWAN-backhaul links, every byte costs airtime, and airtime costs battery. Third, identity is the product. A device that proves who it is to a broker with a certificate whose private key lives in a secure element is a much stronger asset than one that sends a shared password over an encrypted pipe.
This article sits inside a broader security chain. The private key that signs the TLS handshake is only as trustworthy as the boot chain that guards it, which is the subject of our guide to TPM 2.0 secure boot, measured boot and remote attestation for IoT fleets. On the industrial side, OPC UA has its own certificate and security-policy machinery, covered in the OPC UA security certificates, GDS and security policies hardening guide; the lessons about trust lists and rotation transfer directly.
One more piece of vocabulary. TLS secures a reliable byte stream (normally TCP); DTLS is the datagram adaptation for UDP. DTLS 1.3 is specified in RFC 9147 (April 2022) and shares the TLS 1.3 handshake design. We treat it in its own section because CoAP, LwM2M and many cellular IoT stacks depend on it.
The TLS 1.3 Handshake, Message by Message
Direct answer: a full TLS 1.3 handshake completes in one round trip. The client sends a ClientHello with a key share; the server answers with its own key share, then sends the rest of its flight (certificate, signature, Finished) encrypted. The client verifies, sends Finished, and application data flows. Resumption with a pre-shared key skips the certificate exchange entirely.

Figure 1: TLS 1.3 handshake between a constrained device and a broker, showing the key share exchange, encrypted server flight, optional client certificate for mTLS, and where 0-RTT early data sits.
The diagram shows the flow between a device (client) and a broker or gateway (server). The long-description version: the client opens with ClientHello; the server replies with ServerHello and, from that point forward, every message is encrypted under handshake traffic keys; the server sends EncryptedExtensions, optionally a CertificateRequest if it wants mutual authentication, then Certificate, CertificateVerify and Finished; the client answers with its own Certificate and CertificateVerify when asked, then Finished; both sides switch to application traffic keys.
The ClientHello and the guess
In TLS 1.2 the client first announced what it supported and waited to learn what the server picked, then performed the key exchange in a second round trip. TLS 1.3 removes that wait by having the client guess. The ClientHello carries a key_share extension containing a public value for one or more groups the client thinks the server will accept, most commonly X25519 (32 bytes) or secp256r1 (65 bytes uncompressed). RFC 8446 makes secp256r1 mandatory to implement and strongly recommends X25519, so a device that offers X25519 first and also supports P-256 is on safe ground.
If the server does not like the guess, it sends a HelloRetryRequest naming the group it wants, and the client tries again. That costs a full extra round trip, which is precisely what you do not want on a high-latency link. The practical rule for a fleet you control: pin the device’s offered key share to the group your broker prefers, and verify in testing that no HelloRetryRequest occurs. A typical minimal ClientHello from an embedded stack with X25519, a short cipher list, supported_versions, signature_algorithms, supported_groups and a server name extension runs a few hundred bytes. (Exact sizes depend on the library and the extension set; measure yours with a packet capture rather than trusting a figure from an article.)
Key schedule in one paragraph
Everything after the hellos is encrypted because TLS 1.3 derives keys through a staged HKDF (RFC 5869) schedule. It begins with an Early Secret derived from a PSK (all zeros if there is none), mixes in the ECDHE shared secret to produce the Handshake Secret, then mixes in a zero value to produce the Master Secret. At each stage, labelled Derive-Secret calls over the running transcript hash produce the traffic secrets. Two consequences matter for embedded work. The transcript hash covers all handshake messages, so any tampering by a middlebox surfaces at Finished. And because the schedule is a chain of HMAC operations on a 32-byte hash (for SHA-256 suites), the symmetric part is cheap; the elliptic-curve scalar multiplications dominate CPU time.
The encrypted server flight and why it matters for privacy
Immediately after ServerHello the server sends EncryptedExtensions, optionally CertificateRequest, then Certificate, CertificateVerify and Finished. CertificateVerify is a signature over the transcript hash (with a context string prepended), proving the server holds the private key matching the certificate. Finished is an HMAC over the transcript using a key derived from the handshake secret, proving the server saw the same conversation as the client.
Because these messages are encrypted, a passive observer no longer sees the server certificate. More relevant for IoT, in mutual authentication the client certificate is also encrypted. In TLS 1.2 the client certificate crossed the network in cleartext, so anyone on the path could read the device’s subject name, serial number or organisation fields and track it. TLS 1.3 closes that leak for free. (The server name in the ClientHello is still visible unless Encrypted Client Hello is used, which is not yet common on embedded stacks.)
What a full handshake costs on the wire
Illustrative byte accounting for an ECDSA P-256 setup with a two-certificate server chain and no client certificate: ClientHello around 250 to 300 bytes upstream; ServerHello around 90 to 130 bytes plus the encrypted flight of roughly 1 to 1.5 KB downstream (dominated by the certificate chain at 400 to 600 bytes per DER-encoded certificate, plus a 70-byte signature and 32-byte Finished); client Finished of about 50 bytes upstream, including record overhead. Add TCP/IP headers and the TCP three-way handshake on top. Label these as illustrative: certificate size varies enormously with the subject fields, extensions and key type, which is why the later section on certificate diet exists.
Two properties of this flow are worth underlining. The client can send application data right after its own Finished, and the server can send application data right after its Finished (before it has received the client’s), the so-called half-RTT data. Servers must not trust that data’s recipient yet, since the client has not authenticated, but for a broker pushing a configuration message after a fully authenticated server handshake this can cut perceived latency.
What changed from TLS 1.2 in handshake terms
TLS 1.2 had two round trips for a full handshake with ECDHE (ClientHello, ServerHello through ServerHelloDone, then ClientKeyExchange, ChangeCipherSpec, Finished, and the server’s ChangeCipherSpec, Finished). TLS 1.3 halves it. TLS 1.2 session resumption used either session IDs (server-side state) or RFC 5077 session tickets (client-held state encrypted by the server); TLS 1.3 folds both into a single mechanism based on PSKs delivered in a NewSessionTicket message. TLS 1.2 allowed negotiating compression and renegotiating mid-connection; TLS 1.3 forbids both, removing the CRIME and renegotiation-attack families. TLS 1.2 had a ChangeCipherSpec record for state transitions; TLS 1.3 retains a dummy one only for middlebox compatibility, which a constrained implementation may emit or ignore as the specification describes.
Pricing the Handshake: Flash, RAM, CPU and Energy on a Microcontroller
A handshake has four cost dimensions on a small device: code footprint in flash, peak RAM, wall-clock CPU time, and energy. They trade against each other, and the right optimisation depends on which one your product is short of.

Figure 2: Cost map for a TLS 1.3 session on a constrained device, showing which phases of the connection consume flash, peak RAM, CPU time and radio energy, and which knobs reduce each.
Flash and library choices
The mainstream open-source TLS libraries for microcontrollers are Mbed TLS (now maintained under the TrustedFirmware.org umbrella), wolfSSL, and, on platforms that ship it, vendor stacks built on them. BearSSL targets small footprints but its TLS 1.3 support has long been described as absent or experimental, so verify before choosing it for a TLS 1.3 requirement. Each library lets you compile out unused features through configuration headers, and the largest lever on flash is the set of algorithms you enable. A build with ECDHE on one curve, ECDSA verification, AES-GCM or ChaCha20-Poly1305 and SHA-256 is far smaller than one that retains RSA, multiple curves, SHA-384/512 and X.509 features such as name constraints and CRL parsing.
I deliberately give no kilobyte figures here. Footprints published by library vendors are measured on specific toolchains, optimisation levels and feature sets, and they move with every release. The honest method is to build your exact configuration for your exact target, link with -Os and section garbage collection, and read the map file. Expect the X.509 parser and big-number arithmetic to be significant contributors, and expect hardware crypto offload (an AES engine, a public-key accelerator, or a secure element doing ECDSA) to shrink both code size and CPU time at the cost of driver code and a more constrained algorithm menu.
Peak RAM is dominated by buffers, not keys
TLS records can be up to 16,384 bytes of plaintext. A naive stack allocates a 16 KB input buffer and a 16 KB output buffer, which alone exceeds the RAM of many Class 1 devices. Three standards-based mechanisms reduce this. The max_fragment_length extension (RFC 6066) lets a client negotiate fragments of 512, 1024, 2048 or 4096 bytes. The newer record_size_limit extension (RFC 8449), which is the TLS 1.3 successor designed to address limitations in the older one, lets each endpoint advertise the largest record it is willing to receive. And library-specific options (for example, asymmetric buffer sizes where the input buffer must hold a full incoming certificate message but the output buffer can be small) trim more.
The handshake also has a transient peak that steady-state does not. The certificate chain must be received, possibly across several records, parsed, and verified while elliptic-curve scratch memory is live. Streaming parsers that verify one certificate at a time and discard it help. Devices that cannot afford a general X.509 path-validation engine should consider pinning: a device that trusts exactly one issuing CA public key (or a short list) can use a very small verifier. The price is operational, because rotating that CA means a firmware update or an out-of-band trust anchor update.
CPU time: the elliptic curve is the bill
For a full handshake with ECDHE and ECDSA the dominant operations on the device are: one scalar multiplication to generate the ephemeral key share, one to compute the shared secret, one signature verification (two scalar multiplications) per certificate in the chain checked, and, with mTLS, one signature generation. On a Cortex-M4 class core at around 100 MHz running portable C, P-256 scalar multiplication is commonly in the tens to low hundreds of milliseconds; with optimised assembly or a hardware accelerator it drops by an order of magnitude. (These are order-of-magnitude statements consistent with published ECC benchmarks for these cores. Do not cite a single number without measuring on your part.) X25519 is typically faster than P-256 in software on small cores because of its curve structure, and Ed25519 verification is competitive, though the library and curve support you actually have matters more than theory.
The symmetric parts, AES-GCM or ChaCha20-Poly1305 on records, are negligible next to this. If your MCU lacks an AES instruction set or accelerator, ChaCha20-Poly1305 is usually the better software choice. If you have an AES accelerator, AES-128-GCM or AES-128-CCM is better. RFC 8446 requires TLS_AES_128_GCM_SHA256 for compliant implementations; it also defines TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_CCM_SHA256 and TLS_AES_128_CCM_8_SHA256. The CCM_8 variant truncates the authentication tag to 8 bytes to save bandwidth; the specification lists it as a defined suite but it carries a weaker forgery bound, so use it only with eyes open and only where bytes truly dominate. Note that the CCM suites are the ones with the most widespread hardware support on radio SoCs.
Energy: a worked example (illustrative)
Take a cellular LTE-M or NB-IoT sensor. Use deliberately simple, illustrative figures: a 3.6 V supply, 200 mA average draw during transmission at an effective 20 kbit/s, so about 0.29 mJ per transmitted byte. Reception is cheaper; assume 0.1 mJ per received byte. Assume the 3 KB handshake carries roughly 1.2 KB upstream and 2.5 KB downstream, and that cryptography adds about 0.1 J of CPU and accelerator energy. Then one full handshake costs approximately:
- Upstream: 1,200 bytes × 0.29 mJ = 0.35 J
- Downstream: 2,500 bytes × 0.1 mJ = 0.25 J
- Crypto: 0.10 J
- Total: about 0.7 J per full handshake (illustrative)
A 2 Ah battery at 3.6 V stores about 25,900 J. One full handshake per day for ten years is 3,650 handshakes, about 2.6 kJ, roughly a tenth of the battery. One per hour for ten years is 87,600 handshakes, around 61 kJ, which is more than twice the battery. The conclusion does not depend on the exact constants: reconnecting for every message does not survive a ten-year battery life. You either keep the session alive (accepting the radio power-saving constraints, which are their own topic), use resumption to cut the per-connect cost, batch messages, or move to a datagram design with a connection identifier that survives network changes.
Resumption in the same model skips the certificate chain and the signature verification, so the downstream volume falls to a few hundred bytes. Say 0.4 KB up and 0.3 KB down, plus one ECDHE computation: roughly 0.12 J + 0.03 J + 0.03 J, about 0.2 J. That is roughly a third of the full handshake, still not free. Resumption is a mitigation, not a substitute for a good connection-lifetime strategy.
0-RTT Early Data and the Replay Problem
Direct answer: 0-RTT lets a client that holds a resumption PSK send application data in its very first flight, saving a round trip. The price is that early data is not protected against replay: an attacker can capture and resend it, and the server may process it twice. Use it only for idempotent operations, and never for commands that change state.
TLS 1.3 gives a device with a previously issued ticket a way to start talking immediately. The ClientHello includes the early_data extension and a pre_shared_key extension; right after it, the client sends application data encrypted under an early traffic secret derived from the resumption PSK. The server, if it accepts, decrypts and can process that data before the handshake even finishes.
For a sensor on a slow link the appeal is obvious: the telemetry record rides in the first packet, and the device can go back to sleep a full round trip earlier. For a link with a 300 ms RTT that is an energy saving of a few percent of the radio-on time per transaction. The danger is in what the guarantees are, and what they are not.
What 0-RTT does and does not guarantee
Early data is encrypted and authenticated with keys derived from the PSK, so a passive eavesdropper cannot read it and an attacker cannot modify it. It does not have forward secrecy with respect to the PSK (compromise of the ticket encryption key or the resumption secret exposes it) and, critically, it has no freshness guarantee. RFC 8446 section 8 states this plainly: 0-RTT data can be replayed by an attacker, either to the same server or, in a multi-server deployment sharing ticket keys, to a different server instance that cannot see the first one’s state.
Replay differs from a normal network duplicate. A TCP retransmission is handled below TLS. A TLS replay is an attacker (or a buggy middlebox) re-sending the captured ClientHello with its early data on a new connection. The server sees a valid ticket and a valid binder and decrypts a valid record. Nothing at the protocol layer says “you have seen this before” unless the server implements an anti-replay mechanism.

Figure 3: How a captured 0-RTT flight is replayed against a broker, and where each defence (single-use tickets, a ticket-age and freshness window, idempotent handlers) blocks or neutralises it.
The mitigations the specification offers
RFC 8446 describes three server-side strategies. Single-use tickets: the server records each ticket (or its identity) when first used and rejects any second use. This is exact but requires state shared across every server instance that can accept the ticket, which in a load-balanced broker cluster means a distributed store with its own latency and failure modes. ClientHello recording: the server keeps a record of recently seen ClientHellos (or their binder values) within an acceptance window and rejects duplicates within it; combined with the ticket-age check, a server need only remember what falls inside the window. Freshness checks: the client includes an obfuscated ticket age; the server compares the client’s reported age to the elapsed time since issuing the ticket and rejects early data outside a small tolerance. That narrows the replay window but does not close it: a replay inside the window still succeeds unless something records it.
The server can also simply decline early data. When it rejects 0-RTT, the client must resend the data after the handshake completes, which is correct behaviour and simply costs the round trip you were trying to save. The server advertises its willingness through max_early_data_size in the ticket; a value of zero means “no early data.”
Application-layer rules for IoT
If you enable 0-RTT on a broker, define in writing which message types are allowed in early data. Idempotent, naturally-deduplicated telemetry (a reading with a sequence number and device timestamp that the ingest pipeline upserts by key) is a reasonable candidate. A command such as “open valve,” “unlock,” “decrement stock,” or a billing event is not. In HTTP-based deployments RFC 8470 defines the Early-Data header and the 425 (Too Early) status so an origin can tell a gateway to retry a request after the handshake completes. MQTT has no equivalent. An MQTT PUBLISH at QoS 1 replayed in early data would be delivered twice, and a CONNECT with clean-session semantics could reset state. Most MQTT brokers therefore either do not offer 0-RTT at all or never advertise max_early_data_size, and that is the safe default to preserve.
My opinion, and a position some teams will dispute: on constrained fleets, 0-RTT is rarely worth the audit burden. The better first moves are connection reuse and PSK resumption (1-RTT), which preserve forward secrecy when combined with ECDHE and have no replay exposure. Reach for 0-RTT only after measuring that the saved round trip is a meaningful fraction of your radio-on energy, and after proving that every early-data message type is safe to process twice.
Session Resumption: Tickets, PSKs and the Forward-Secrecy Choice
After a full handshake the server can send one or more NewSessionTicket messages. Each contains a ticket (an opaque blob), a lifetime in seconds (capped at seven days by the specification), a ticket_age_add value used to obfuscate the client’s reported age, and optionally an early_data extension. The client derives a resumption PSK from the connection’s resumption master secret and a per-ticket nonce, so each ticket maps to a distinct PSK.
The ticket itself is typically the server’s session state encrypted under a server-side ticket key (stateless server), or an identifier pointing at a server-side cache entry (stateful server). The stateless form scales well but has a notable operational property: the ticket encryption key becomes a high-value secret. Anyone who obtains it can decrypt all outstanding tickets and recover their resumption secrets. Rotate ticket keys frequently, at least daily for most deployments, and keep a short overlap so recently issued tickets still validate.
Two resumption modes
When resuming, the client lists the PSK key exchange modes it accepts in the psk_key_exchange_modes extension. psk_ke uses the PSK alone, with no Diffie-Hellman, which is the cheapest in CPU and bytes but loses forward secrecy: if the resumption secret is later compromised, past sessions that used it can be decrypted. psk_dhe_ke mixes in a fresh ECDHE exchange, so the session keys are forward-secret even if the PSK leaks later. It costs two scalar multiplications and a 32-byte key share each way.
For most IoT systems, psk_dhe_ke is the right default. The extra cost is a few tens of milliseconds on a mid-range MCU and a hundred or so bytes, against a stronger guarantee that a stolen device (and its stored ticket) cannot unlock yesterday’s traffic captured off the air. Reserve psk_ke for the narrow case where a sub-hundred-millisecond CPU budget on an unusually weak core is decisive and the data is of low sensitivity.
What resumption skips, and what it does not
A resumed handshake omits the Certificate, CertificateVerify and CertificateRequest messages, because authentication is inherited from the original session via the PSK. The result: smaller flights, no signature verification, and no X.509 parsing on the device. That is the large saving. It also means that certificate revocation and expiry are not re-evaluated on resumption. If a device certificate is revoked at 10:00 and the broker has an outstanding ticket valid until midnight, the device can resume until the ticket expires. Brokers handling revocation should cap ticket lifetime to match their revocation latency tolerance, or explicitly invalidate tickets for revoked identities. Likewise, an expired certificate on a device does not prevent resumption while the ticket is live, which can mask the problem until the ticket finally lapses.
Storing tickets on a device
The device must persist the ticket and its resumption secret across reboots and deep sleep to benefit. That makes the ticket a credential. On a device with a secure element or protected flash, store it there; on one without, accept that extracting flash yields the ability to impersonate the device until the ticket expires (and only for that server). Keep ticket lifetimes shorter than your threat model’s tolerance for physical extraction, and bind the ticket lifetime to the device’s expected wake interval: a ticket valid for a week is useless to a sensor that reports every ten days.
Mutual TLS for IoT: Identity, Provisioning and Rotation
Server-only TLS tells the device it is talking to the right broker. mTLS IoT designs add the reverse: the broker learns, cryptographically, which device is connecting. This replaces shared passwords and API keys with per-device private keys that never leave the device. Done well, a compromised broker credential database leaks only public certificates, and a lost device can be cut off by revoking one identity.
The cost is lifecycle complexity. A device certificate must be created, delivered, renewed and eventually revoked, and each step needs a trust path that works at scale and offline.

Figure 4: Lifecycle of a device identity for mTLS: key generation in secure hardware, initial credential at manufacturing, enrollment of an operational certificate, TLS 1.3 client authentication at the broker, renewal before expiry, and revocation.
Key generation and storage
The central rule is that the private key should be generated inside the device’s secure element, TPM, or MCU key-storage peripheral and never exported. The device signs the CertificateVerify message through an opaque handle. Mbed TLS’s PSA Crypto integration and wolfSSL’s PKCS#11 and vendor-engine support both allow a TLS stack to delegate signing to a secure element, which is why you can have a certificate-authenticated TLS 1.3 session without the private key ever being present in RAM. If your devices have a TPM or secure element, the attestation story is what ties the key to a trustworthy boot state; see our TPM 2.0 guide for how measured boot and attestation can gate certificate issuance.
Two-tier identity: birth certificate and operational certificate
A common pattern, used in standards such as IEEE 802.1AR (Secure Device Identity, with its IDevID and LDevID concepts), separates a long-lived manufacturer-installed identity from short-lived operational credentials. The factory provisions an initial device identifier (IDevID): a key pair and certificate signed by the manufacturer CA, with the serial number in the subject. At deployment the device authenticates with the IDevID to an enrolment service and receives a locally significant certificate (LDevID) issued by the operator’s CA, with a lifetime of weeks to a year. The broker trusts only the operator CA. This keeps the manufacturer’s long-lived key off the operational path and lets the operator revoke without involving the manufacturer.
Enrollment protocols that fit this model include EST (Enrollment over Secure Transport, RFC 7030), which carries CSRs over a TLS-protected HTTP channel, and BRSKI (RFC 8995) for zero-touch onboarding, which uses a voucher issued by the manufacturer’s service to let the device accept the local domain’s registrar. For tiny devices there is also CoAP-based EST (RFC 9148, EST-coaps). Cloud IoT platforms layer their own variants on top: AWS IoT Core, for example, supports X.509 client certificates with mutual TLS, and Azure IoT Hub supports X.509 authentication, each with its own just-in-time registration or provisioning service. The protocol detail differs per vendor; check each platform’s current documentation rather than assuming parity.
Rotation without bricking devices
Renewal is where fleets get hurt. A certificate with a one-year lifetime that nobody renews will fail all at once, a year after the batch was commissioned, and devices that can only reach the broker over the very TLS channel that now fails cannot fetch a fresh certificate. The defences are procedural and architectural:
- Renew early: start renewal at roughly two-thirds of the lifetime, and retry with backoff, so a transient failure leaves ample slack.
- Renew over the existing session: use EST re-enrolment authenticated by the still-valid certificate, so the device never needs a “break glass” path.
- Keep an out-of-band recovery trust anchor: for example a long-lived bootstrap credential that can only reach an enrolment endpoint, never the data plane.
- Stagger: issue certificates with randomised lifetimes within a window so the entire fleet does not expire the same hour, which also avoids thundering-herd renewal against the CA.
- Overlap trust: when rotating the issuing CA, publish both old and new CA certificates in device trust stores for at least one full certificate lifetime.
Revocation on constrained devices
Classic revocation (CRLs, RFC 5280; OCSP, RFC 6960) is a poor fit for the device’s view of the broker, because a device with an intermittent radio cannot fetch a CRL reliably. In practice most IoT deployments use short-lived certificates so that revocation is largely expiry, plus server-side enforcement: the broker keeps a deny list of device identities and rejects them after the handshake, or at the policy layer via the authenticated subject. This is the asymmetry to design around: the broker can check device revocation cheaply; the device mostly cannot check broker revocation. Mitigate the latter by pinning the broker’s issuing CA and rotating it deliberately.
Reading the identity on the server
Once the handshake completes, the broker maps the client certificate to an application identity: commonly the subject common name, a SAN URI such as urn:dev:ops:..., or the certificate fingerprint. Take care to bind authorisation to the verified, stable identifier, not to a free-text field that a registration flow lets the device choose. Authorise topics and resources from the identity (for example, an MQTT broker ACL keyed on the certificate’s SAN), and log the certificate fingerprint for audit.
Certificate Diet: Key Types, Sizes and Compression
Certificates are the largest single component of a full handshake, so shrinking them is the most effective wire-level optimisation after avoiding handshakes entirely.
ECDSA P-256 versus Ed25519 versus RSA
A P-256 public key is 65 bytes uncompressed (33 compressed) and an ECDSA signature is about 70 to 72 bytes in DER. An Ed25519 public key is 32 bytes and its signature is exactly 64 bytes. RSA-2048 has a 256-byte signature and a public key over 256 bytes. Over a chain of two certificates plus the CertificateVerify, the RSA path adds several hundred bytes compared with ECC, and RSA verification is cheap while RSA signing is slow on small cores, which matters for the mTLS client side.
| Property | RSA-2048 | ECDSA P-256 | Ed25519 |
|---|---|---|---|
| Public key size | 256+ bytes | 65 bytes (33 compressed) | 32 bytes |
| Signature size | 256 bytes | about 70-72 bytes (DER) | 64 bytes |
| Signing speed on MCU | slowest | moderate | usually fast |
| Verification speed on MCU | fast | moderate | moderate to fast |
| TLS 1.3 support | rsa_pss_rsae_* required in practice | mandatory (ecdsa_secp256r1_sha256) | optional, library-dependent |
| Hardware support in secure elements | common | very common | patchy |
| Hazard | large messages | nonce-reuse failure if RNG is bad | CA ecosystem support still uneven |
The last row of that table matters. ECDSA requires a fresh secret nonce per signature; a weak random number generator on a freshly booted MCU has caused real-world key recovery. Deterministic ECDSA (RFC 6979) removes the dependency on the RNG at signing time. Ed25519 is deterministic by design, which is an engineering advantage on devices with poor entropy, but support in public CAs, HSMs and secure elements is less universal than P-256, so check the entire chain, including the private CA software and the hardware that holds the key, before committing. For a private PKI where you control both ends, Ed25519 is a defensible choice. For anything that must interoperate with third-party clouds and secure elements today, P-256 is the lowest-friction path.
Trim the certificate itself
For a private IoT PKI you control the contents. Use a minimal subject (a serial number, not a long organisational DN), omit unnecessary extensions, avoid embedding policy text or URLs the device never uses, keep the chain to a single intermediate, and let the server send only the leaf plus intermediate, not the root. A well-trimmed ECDSA leaf can be around 300 to 400 bytes versus 700 or more for a verbose one (illustrative; measure with your own tooling).
Standards add more. TLS Certificate Compression (RFC 8879) lets endpoints compress the Certificate message using zlib, Brotli or Zstandard; it saves bytes on large chains but needs a decompressor and extra RAM, which is rarely a net win on a Class 1 device, though a gateway talking to a cloud can enable it. Raw Public Keys (RFC 7250) let the server and client skip X.509 entirely and exchange just a SubjectPublicKeyInfo, 91 bytes for P-256 in DER, with trust established out of band (pinning each device’s key). This removes the parser, the chain and most of the bytes, and is attractive for closed fleets. The trade-off is that you lose standard PKI tooling and must build your own key registry. Cached Information (RFC 7924) was proposed for the same goal but has seen little adoption.
DTLS 1.3, CoAP and MQTT: Matching the Protocol to the Transport
DTLS 1.3 for UDP and CoAP
Many constrained devices speak CoAP over UDP, where the secure transport is DTLS. DTLS 1.3 (RFC 9147) keeps the TLS 1.3 handshake and adapts it to unreliable delivery: handshake messages carry sequence numbers and fragment offsets, the endpoints retransmit with timers, and each handshake message is acknowledged through a dedicated ACK content type, a change from DTLS 1.2, where retransmission was driven by the next flight. A stateless cookie exchange, the same idea as HelloRetryRequest, defends against amplification and spoofed-source denial of service.
For IoT the most valuable addition is the Connection ID (RFC 9146). Without it, a DTLS association is identified by the IP address and port 4-tuple. NAT rebinding, which is routine on cellular and Wi-Fi links after an idle period, silently breaks the association and forces a full handshake. A connection ID lets the server recognise the session from a field in the record header, so a device whose source port changes keeps its keys. DTLS 1.3 also compresses record headers, using a unified header with a variable-length sequence number, which helps on 6LoWPAN-sized frames. CoAP over DTLS uses the coaps scheme on UDP port 5684 by default. CoAP over TCP and TLS is defined in RFC 8323.
DTLS has its own 0-RTT replay story, and the same advice applies: early data can be replayed, and datagram transports additionally face reordering and duplication, so make handlers idempotent regardless.
An alternative: object security
If your traffic crosses proxies or you have a deeply constrained link, protecting the message rather than the channel can be cheaper. OSCORE (RFC 8613) encrypts CoAP messages end to end using a pre-established security context, and EDHOC (RFC 9528) provides a lightweight authenticated key exchange designed to produce that context in a fraction of the bytes of a TLS handshake. They suit LPWAN-class devices; they are a different trade-off (less ecosystem tooling, no direct reuse of TLS offload in cloud load balancers), not a drop-in replacement.
MQTT over TLS
MQTT brokers conventionally listen for TLS on TCP port 8883. The TLS handshake completes before the MQTT CONNECT packet, so authentication has two layers: transport identity from the client certificate, and optional MQTT username and password. With mTLS you can rely on the certificate and leave the username field for identity binding or leave it empty, depending on broker support. The MQTT keep-alive interval interacts with NAT and carrier idle timers: if the mapping expires before the next keep-alive, the TCP connection dies silently and the device pays a full reconnect, handshake included. Tune keep-alive below the shortest idle timer on your path, and measure it, because carrier NAT timeouts vary widely.
The OPC UA world solves a related problem with its own security layers over UADP and MQTT; our comparison of OPC UA PubSub over MQTT versus UADP shows where transport security stops and message-level security starts. If you instead want a network-layer tunnel across a heterogeneous fleet, see how WireGuard, Tailscale, NetBird and Headscale compare for edge networking: a tunnel can protect legacy protocols that cannot do TLS 1.3 themselves, though it moves identity from per-application certificates to per-node keys.
Post-Quantum Hybrids: Status and Impact on Constrained Devices
An attacker who records traffic today could decrypt it later with a quantum computer (“harvest now, decrypt later”). Defences focus on the key exchange, since that is what protects confidentiality. The IETF TLS working group has been standardising hybrid key exchange, combining a classical ECDHE with the NIST-standardised ML-KEM (FIPS 203, August 2024). The relevant draft, draft-ietf-tls-ecdhe-mlkem, defines named groups including X25519MLKEM768, SecP256r1MLKEM768 and SecP384r1MLKEM1024. Its datatracker page showed revision 05 dated May 2026 in my last check; treat it as a draft that may have progressed since, and verify the current status before relying on it. Browsers and large CDNs have deployed X25519MLKEM768, so server-side support is increasingly common.
The cost for a constrained device is size. An ML-KEM-768 encapsulation key is 1,184 bytes and its ciphertext is 1,088 bytes, so the hybrid X25519MLKEM768 key share is about 1,216 bytes from the client and 1,120 bytes from the server, versus 32 bytes each for X25519 alone. That more than doubles a typical ClientHello and may push it past a single TCP segment or a 6LoWPAN fragment boundary. Computation is not the problem (ML-KEM is fast, often faster than ECDH on larger cores), but buffers and airtime are. Post-quantum signatures are heavier still: ML-DSA-44 signatures are 2,420 bytes (FIPS 204), so PQ certificate chains are a much larger problem than PQ key exchange and remain an open item for IoT.
A pragmatic stance for 2026: make the long-lived confidentiality case explicit. For telemetry of no value in ten years, classical ECDHE remains reasonable. For firmware, keys, medical or industrial IP that must stay confidential for decades, plan hybrid key exchange on gateways first (where size is a non-issue), and keep device firmware able to switch groups through an update. Whether specific MCU libraries ship hybrid groups changes by release; check each library’s current release notes.
Failure Modes, Gotchas and What Goes Wrong
Most TLS incidents in IoT are operational, not cryptographic. These are the ones that recur.
The clock is not set. X.509 validity checks need the current time. A device that boots from a cold RTC may believe it is 1970 and reject every certificate as “not yet valid,” or be set far in the future and call them expired. If the time source (NTP, SNTP, network time from the cellular modem) itself depends on the TLS-protected channel, you have a chicken-and-egg loop. Mitigations: a battery-backed RTC, a trusted time source from the modem, and a defined fallback that accepts a “time not set” state only for the narrow purpose of fetching time from a pinned endpoint. Do not disable validity checking globally as a workaround; that quietly turns your authentication into encryption without identity.
Certificates expire while the device is offline. A device in a warehouse powered off for fourteen months wakes with an expired leaf and cannot reach the enrolment service. This is the cold-storage variant of the renewal problem. Provide a recovery path that uses a separate long-lived bootstrap credential and a restricted endpoint.
HelloRetryRequest and middlebox surprises. A mismatched offered group causes an extra round trip. Some corporate proxies and old load balancers mishandle TLS 1.3 and drop or reset connections, which is why the protocol includes the legacy compatibility mode. If a fleet works over cellular but fails inside a customer network, suspect a TLS-inspecting middlebox before suspecting your code.
Silent downgrade to TLS 1.2. If both endpoints still allow TLS 1.2, a misconfiguration can leave you running it without noticing. TLS 1.3 includes downgrade-protection sentinel values in the ServerHello random field, and a correct client aborts when it sees them while expecting a higher version, but only if it implements the check. Set a minimum version of 1.3 on a fleet you fully control, and log the negotiated version at the broker.
Resumption hides revocation and expiry. Covered above, and worth repeating: a live ticket outlasts a revoked certificate.
Weak entropy at first boot. Ephemeral key shares and ECDSA nonces both depend on the random number generator. A device that generates its first key before the entropy pool is seeded can produce predictable keys. Use a hardware true random number generator, wait for seeding, and test it.
Handshake storms. After a broker restart or regional outage, every device reconnects at once, and a full handshake costs the server two or more public-key operations per client. A fleet of one million devices retrying with fixed intervals can saturate the broker’s CPU. Use exponential backoff with jitter on the device, and size the TLS terminators for the reconnect peak, not the steady state.
Hard-coded trust stores. A root CA baked into firmware that expires in five years leaves devices unable to connect to anything. Plan CA lifetimes and over-the-air trust updates before shipping, and test the update path regularly.
Side channels and fault injection. Physical access changes the threat model. Constant-time implementations and hardware that protects against power analysis and glitching matter for devices deployed in the open. A software TLS library on an unprotected MCU does not defend against an attacker with a probe, which is the reason to keep the key in a secure element.
Practical Recommendations
For a new fleet starting in 2026, the following baseline is defensible and conservative. It is a starting point, not a substitute for a threat model.
Use TLS 1.3 only, with TLS_AES_128_GCM_SHA256 (or TLS_CHACHA20_POLY1305_SHA256 on cores without AES acceleration), ECDHE over X25519 with P-256 as fallback, and ECDSA P-256 certificates unless your whole chain supports Ed25519. Authenticate devices with mTLS, with the private key held in a secure element or equivalent. Use a two-tier identity with a short-lived operational certificate, renewed automatically at two-thirds of its lifetime. Prefer long-lived connections or psk_dhe_ke resumption over repeated full handshakes. Leave 0-RTT off unless you can show every early-data message is safe to execute twice. For UDP, use DTLS 1.3 with Connection ID and define retransmission timers for your link.
A short checklist you can paste into a design review:
- Minimum protocol version is TLS 1.3 on both client and server; negotiated version is logged.
- Offered key share matches the broker’s preferred group; no HelloRetryRequest in a packet capture.
- Private keys are generated on-device in protected hardware and never exported.
- Certificate chain is minimal (leaf plus one intermediate), subject is short, and the byte size of the full server flight is measured.
- Time source is defined, and certificate validity checking is never disabled.
- Renewal starts at about two-thirds of lifetime, uses the existing session, staggers across the fleet, and has an out-of-band recovery path.
- Ticket lifetime is no longer than your revocation latency tolerance; ticket keys rotate at least daily.
- Reconnect logic uses exponential backoff with jitter; broker capacity is sized for a full-fleet reconnect.
- 0-RTT is disabled, or each early-data message type is documented as idempotent.
- Energy per connection is measured on real hardware, not estimated.
Frequently Asked Questions
Is TLS 1.3 too heavy for microcontrollers?
Not generally. Modern embedded libraries support TLS 1.3 on Cortex-M class cores, and a build limited to one cipher suite, one curve and ECDSA verification can fit in a modest flash budget. The real constraints are peak RAM during the handshake (record buffers and certificate parsing) and the cost of elliptic-curve operations. Devices under roughly 64 KB of RAM may need smaller record sizes, pinned trust anchors or raw public keys. Measure your exact configuration rather than relying on published footprints.
What is the difference between 0-RTT and session resumption in TLS 1.3?
Resumption uses a previously issued PSK to skip the certificate exchange while still completing a 1-RTT handshake, ideally with a fresh ECDHE exchange for forward secrecy. 0-RTT is an optional addition on top of resumption that lets the client send application data in its first flight, saving a round trip. Resumption is safe by default; 0-RTT data is replayable and has weaker forward secrecy, so it needs application-level idempotency.
Can a 0-RTT replay attack be fully prevented?
At the protocol level, only partly. Servers can enforce single-use tickets, record recent ClientHellos and apply ticket-age freshness checks, but each has costs in state or leaves a window, especially across clustered servers. The reliable defence is at the application layer: allow only idempotent operations in early data, or reject early data and make the client resend after the handshake. Many brokers simply disable it.
Should IoT devices use ECDSA or Ed25519 certificates?
ECDSA P-256 is the safest interoperable choice today, with broad support in secure elements, public and private CAs, and cloud IoT services. Ed25519 offers smaller keys, deterministic signatures that avoid nonce-reuse risk on weak-entropy devices, and good performance, but hardware and PKI support is less uniform. Choose Ed25519 only if every component in your chain, including the HSM or secure element holding the key, supports it.
How does mTLS differ from API keys for device authentication?
With mTLS each device holds a private key that never leaves it, and proves possession during the handshake; the server stores only public certificates. An API key is a shared secret that travels with every request and, if leaked from one server log or database, grants access until rotated. mTLS also gives you per-device revocation and short-lived credentials, at the cost of certificate lifecycle management and a more complex provisioning flow.
When should I use DTLS 1.3 instead of TLS 1.3?
Use DTLS 1.3 when your application protocol runs over UDP, as with CoAP, LwM2M or QUIC-less sensor links, and when you need datagram semantics without TCP overhead. Enable the Connection ID extension so that NAT rebinding does not force a full handshake. If your traffic is reliable, long-lived and already on TCP, as with MQTT, ordinary TLS 1.3 is simpler and better supported.
Further Reading
Internal guides on this site:
- TPM 2.0 secure boot, measured boot and remote attestation for IoT fleets: anchoring device keys in a trustworthy boot chain.
- OPC UA security: certificates, GDS and security policies hardening guide: trust lists and certificate management in industrial protocols.
- OPC UA PubSub: MQTT versus UADP architecture comparison: transport versus message-level security.
- WireGuard vs Tailscale vs NetBird vs Headscale for edge fleet networking: tunnel-based alternatives for fleet connectivity.
External primary sources:
- RFC 8446, The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 9147, The Datagram Transport Layer Security (DTLS) Protocol Version 1.3
- RFC 9325, Recommendations for Secure Use of TLS and DTLS
By Riju — about
