Securing CoAP for Constrained IoT: DTLS 1.3, OSCORE and EDHOC Compared (2026)

Securing CoAP for Constrained IoT: DTLS 1.3, OSCORE and EDHOC Compared (2026)

OSCORE EDHOC CoAP Security: DTLS 1.3 vs Object Security

Most teams pick a security layer for CoAP by asking which protocol is “strongest”. That is the wrong question, because DTLS 1.3, OSCORE and EDHOC are all built on modern authenticated encryption and none of them is weak. The question that actually decides the outcome is where trust has to end: at the next hop, at a gateway you own, or at a cloud service on the other side of three proxies and a cache.

This matters now because the pieces finally exist as published standards. OSCORE has been an RFC since 2019, DTLS 1.3 since 2022, and EDHOC since March 2024. Together they give a constrained device a complete, small, end-to-end path from key agreement to protected application traffic, and OSCORE EDHOC CoAP security is becoming the reference answer for sleepy, battery-powered nodes.

You will leave with a topology-first decision framework, byte-level handshake numbers taken from the specifications, a runnable OSCORE key-derivation check against the RFC test vector, and a list of the failure modes that bite in production: nonce reuse after reboot, plaintext fallbacks, stale freshness and proxy surprises.

What this covers: channel versus object security, the three protocols side by side, handshake sizes and round trips, proxies and caching, key management and rekeying, group communication, deployment on Zephyr, a decision matrix, failure modes and a checklist.

Context and Background

CoAP, the Constrained Application Protocol defined in RFC 7252, is a request and response protocol designed for devices with kilobytes of RAM and links that carry a few dozen bytes per frame. It runs mostly over UDP, mirrors HTTP semantics, and has no security of its own. RFC 7252 defines a binding to DTLS (the coaps scheme) with a pre-shared key, raw public key or certificate mode, and also a NoSec mode that sends everything in the clear. Our CoAP protocol deep dive covers the base protocol, and MQTT-SN versus MQTT versus CoAP covers when to choose it.

DTLS is a channel security protocol. It protects a point-to-point datagram association between two endpoints, so every intermediary that terminates the association sees plaintext. That is fine for a sensor talking straight to a gateway. It stops being fine when the path includes a CoAP proxy, an HTTP-to-CoAP translator or a cloud ingestion tier operated by someone else.

OSCORE, Object Security for Constrained RESTful Environments (RFC 8613), takes the other approach. It protects the CoAP message itself at the application layer using COSE, the CBOR Object Signing and Encryption format. The protection survives UDP, TCP and HTTP translation, and intermediaries can still route and cache what they are allowed to see. EDHOC, Ephemeral Diffie-Hellman Over COSE (RFC 9528), is the matching key exchange: a compact authenticated handshake whose main purpose is to establish an OSCORE security context.

The existing post on CoAP, OSCORE, EDHOC and DTLS 1.3 constrained device security architecture gives the layered overview. This article takes a narrower angle: it treats the choice as a topology and operations problem, and it spends its words on proxies, freshness, reboot behaviour, group traffic and the numbers that separate the options. For the TLS side of the same trade-off, see TLS 1.3 handshake, 0-RTT and mTLS performance on constrained IoT devices.

A note on method. Every protocol number in this article comes from the RFC text, the Zephyr documentation or a benchmark page I fetched while writing, and each is attributed. Anything I derive myself, such as fragment counts on a given link, is labelled as an estimate with its assumptions shown.

The Core Argument: Pick the Layer by Where Trust Ends

The short answer: use DTLS 1.3 when one hop is the entire security boundary and you want a well-understood channel; use OSCORE with EDHOC when messages cross proxies, caches or multicast, when the device sleeps between exchanges, or when handshake bytes dominate your energy budget. The deciding variable is the trust boundary, not the cipher.

OSCORE EDHOC CoAP security layers compared with DTLS 1.3 channel security across a proxy path

Figure 1: Channel security terminates at every hop that ends the DTLS association, while object security keeps the protected payload opaque from device to application server.

Figure 1 shows the same path twice. On the top path, the sensor and the gateway share a DTLS association, the gateway decrypts, and a second TLS or DTLS session carries the data onward. On the bottom path, the sensor and the application server share an OSCORE context, and the gateway and proxy forward ciphertext. The gateway still reads the outer CoAP fields it needs to route, but not the URI path, payload or most options.

Channel security: DTLS 1.3

DTLS 1.3 (RFC 9147, April 2022) is derived from TLS 1.3 and obsoletes DTLS 1.2 (RFC 6347). Its record header is variable length and unified: the first three bits are fixed at 001, followed by flags for a Connection ID, the sequence number size, a length field and the low two bits of the epoch. Only the low two bytes of the epoch go on the wire, and record sequence numbers are encrypted using a mask derived from the ciphertext, which hides traffic patterns that DTLS 1.2 exposed.

The handshake is one round trip in the common case. A server that wants return routability answers the first ClientHello with a HelloRetryRequest carrying a stateless cookie, which adds a round trip but protects against amplification: the specification tells servers to limit responses to three times the data received before the client address is verified. Handshake reliability, which DTLS 1.2 handled with retransmission alone, now uses an ACK content type so acknowledged messages are not retransmitted.

The Connection ID extension (RFC 9146) is the feature that makes DTLS tolerable for NAT-heavy cellular IoT. A device whose source port changes after a NAT rebinding can keep its association because the CID, not the 5-tuple, identifies it. Without it, a sleepy node that wakes on a new port must run a full handshake again.

Object security: OSCORE

OSCORE does not run a handshake. It assumes both ends already hold a Master Secret, an optional Master Salt and identifiers, and from those it derives a Sender Key, a Recipient Key and a Common IV with HKDF-SHA-256. The mandatory-to-implement AEAD is AES-CCM-16-64-128, COSE algorithm 10, with a 128-bit key and a 13-byte nonce, per RFC 8613.

Each message carries a Partial IV, the sender’s sequence number, and optionally a key identifier. The OSCORE option value starts with a flags byte: the low three bits give the Partial IV length, bit 4 signals that a kid is present, and bit 5 signals a kid context. Because the nonce is built from the sender ID, the Partial IV and the Common IV, the one hard rule is that a (key, nonce) pair must never repeat. The sender sequence number starts at 0 and must stay below 2^40 in the RFC, and the Zephyr documentation notes a tighter practical bound of 2^23 – 1 for AES-CCM-16-64-128 in that implementation.

The option class model is what makes OSCORE proxy-friendly. Class E options, such as Uri-Path, Uri-Query, Content-Format, ETag, Accept and the inner forms of Observe, Max-Age and Block, are encrypted and integrity protected. Class U options, including Uri-Host, Uri-Port, Proxy-Scheme, Proxy-Uri and the OSCORE option itself, stay visible so intermediaries can route. RFC 8613 defines no Class I (integrity-only) options today.

Key establishment: EDHOC

EDHOC is a SIGMA-I style protocol with ephemeral Diffie-Hellman keys. It provides mutual authentication, forward secrecy and identity protection, and it reuses COSE, CBOR and CoAP so a device that already speaks OSCORE adds little code. There are three mandatory messages and one optional fourth, with the initiator sending message_1 and the responder sending the even-numbered messages.

RFC 9528 defines four authentication methods, numbered 0 to 3: signature keys on both sides, signature for the initiator with static Diffie-Hellman for the responder, the reverse, and static Diffie-Hellman on both sides. The static DH methods trade the cost of signing for a cheaper operation, and they are the reason the smallest EDHOC runs are so small. After the handshake, an exporter function derives the OSCORE Master Secret and Master Salt, and the connection identifiers C_I and C_R chosen during the exchange become the OSCORE sender and recipient identifiers.

The strategic point: EDHOC is not a transport. It is a key-agreement step that hands off to OSCORE, which is why the pair is usually written together and why the combined cost, not either half alone, is what you compare with DTLS.

Deeper Analysis: Handshake Bytes, Round Trips and the OSCORE Context

DTLS 1.3 IoT handshake versus EDHOC three-message exchange handing off to an OSCORE context

Figure 2: EDHOC runs three messages over CoAP and exports keys for OSCORE, while DTLS 1.3 adds an optional cookie round trip and certificate flights before application data.

Figure 2 contrasts the two flows. EDHOC message_1 carries the method, supported cipher suites, the initiator’s ephemeral public key and its connection identifier. Message_2 returns the responder’s ephemeral key and an encrypted block that holds the responder’s identity credential reference and a MAC or signature. Message_3 does the same for the initiator, after which both sides can derive the OSCORE context. The optional message_4 only confirms key possession to the responder when the application needs that.

What the specifications say about size

RFC 9528 gives the message sizes for a minimal EDHOC run, in bytes, in its Table 1. The numbers depend on the authentication method and on whether the credential is referenced by a short key identifier (kid) or by a certificate hash (x5t):

Variant message_1 message_2 message_3 Total
Static DH keys, kid 37 45 19 101
Static DH keys, x5t 37 58 33 128
Signature keys, kid 37 102 77 216
Signature keys, x5t 37 115 90 242

These are the best cases: the peer’s credential is already known, so only a reference travels. When full certificates are sent, the totals grow with certificate size. A benchmark page published with the uoscore-uedhoc open-source implementation reports 808 bytes over the air for an EDHOC exchange that carries two 293-byte X.509 certificates, against 1,371 bytes for TLS 1.3 and 1,500 bytes for DTLS 1.3 in the same comparison. The same page flags a caveat that matters: the (D)TLS figures come from separate published papers rather than the same test rig, so treat the ratios as indicative, not as a controlled head-to-head.

The same page reports code and memory on an nRF52840 Cortex-M4: about 41 KB of flash for the uOSCORE-uEDHOC stack including its crypto and CBOR libraries, against roughly 52 KB for TLS 1.3 and 58 KB for DTLS 1.3, and a RAM footprint around 5.6 KB against roughly 21.7 KB and 22.0 KB. Again the (D)TLS numbers are quoted from other papers. Take the direction, a factor of about two to four in memory, and re-measure on your own toolchain before committing a flash budget.

Latency in that benchmark is computation only, with transmission emulated offline. For an EDHOC exchange including certificate handling it reports about 0.40 s on an ESP32, 1.17 s on an nRF52840 and 11.7 s on a 16 MHz nRF51822 Cortex-M0. The last figure is the one to remember: on a very small core, elliptic-curve operations, not radio bytes, can dominate the handshake. Hardware acceleration for P-256 or X25519 moves that number more than any protocol choice.

Translating bytes into frames

On IEEE 802.15.4 with 6LoWPAN, the physical frame is 127 bytes and link-layer security plus IPv6 and UDP header compression leave a smaller payload. The exact figure depends on addressing mode and security level, so the script below takes it as a parameter. The 80-byte default is an assumption for illustration, not a measured value.

"""Estimate how many 802.15.4 frames a handshake needs.
ILLUSTRATIVE: payload_per_frame is an assumption; measure your stack.
Handshake sizes come from RFC 9528 Table 1 and the uoscore-uedhoc benchmark page.
"""
import math

HANDSHAKES = {
    "EDHOC static-DH kid (RFC 9528 T1)":  [37, 45, 19],
    "EDHOC signature x5t (RFC 9528 T1)":  [37, 115, 90],
    "EDHOC 2x X.509 certs (benchmark)":   [808],          # total only
    "DTLS 1.3 with certs (benchmark)":    [1500],         # total only
}

def frames(sizes, payload_per_frame=80, coap_overhead=12):
    usable = payload_per_frame - coap_overhead
    return sum(math.ceil(s / usable) for s in sizes)

for name, sizes in HANDSHAKES.items():
    total = sum(sizes)
    print(f"{name:38s} {total:5d} B -> ~{frames(sizes):3d} frames")

With those assumptions the minimal static-DH EDHOC run needs about three frames, one per message, while the 1,500-byte DTLS figure needs roughly 23 because it must be fragmented, and every lost fragment triggers retransmission logic. Treat the ratio, not the absolute counts, as the takeaway: on lossy low-power links, fewer and smaller messages directly reduce retries and radio-on time.

Verifying the OSCORE derivation yourself

Interoperability bugs in OSCORE usually come from the key-derivation info structure. RFC 8613 derives each key with HKDF-SHA-256 over an info array of the form [id, id_context, alg, type, L], encoded in CBOR. The script below implements it with only the standard library and checks it against the RFC’s Appendix C.1.1 test vector: Master Secret 0x01 to 0x10, Master Salt 9e7ca92223786340, an empty Sender ID and a Recipient ID of 0x01. I ran it and it reproduced the published values.

import hmac, hashlib

def cbor_uint(n):  return bytes([n]) if n < 24 else bytes([0x18, n])
def cbor_bstr(b):  return bytes([0x40 + len(b)]) + b      # len < 24 only

def info(sender_id, alg, typ, length):
    # [ id, id_context (null), alg_aead, type, L ]
    return (bytes([0x85]) + cbor_bstr(sender_id) + b"\xf6" +
            cbor_uint(alg) + bytes([0x60 + len(typ)]) + typ.encode() +
            cbor_uint(length))

def hkdf_sha256(salt, ikm, inf, length):
    prk = hmac.new(salt, ikm, hashlib.sha256).digest()
    t, okm, i = b"", b"", 1
    while len(okm) < length:
        t = hmac.new(prk, t + inf + bytes([i]), hashlib.sha256).digest()
        okm += t; i += 1
    return okm[:length]

master_secret = bytes(range(1, 17))
master_salt   = bytes.fromhex("9e7ca92223786340")
AES_CCM_16_64_128 = 10

client_sender_key = hkdf_sha256(master_salt, master_secret,
                                info(b"", AES_CCM_16_64_128, "Key", 16), 16)
client_recip_key  = hkdf_sha256(master_salt, master_secret,
                                info(b"\x01", AES_CCM_16_64_128, "Key", 16), 16)
common_iv         = hkdf_sha256(master_salt, master_secret,
                                info(b"", AES_CCM_16_64_128, "IV", 13), 13)

assert client_sender_key.hex() == "f0910ed7295e6ad4b54fc793154302ff"
assert client_recip_key.hex()  == "ffb14e093c94c9cac9471648b4f98710"
assert common_iv.hex()         == "4622d4dd6d944168eefb54987c"
print("RFC 8613 C.1.1 vector reproduced")

The nonce for each message is then the Common IV XORed with a padded form of the sender ID and Partial IV. The reason to run this check in CI is simple: if your constrained firmware and your server library disagree on the info array, nothing decrypts, and the error message will tell you nothing useful.

Two layers, one budget

OSCORE EDHOC CoAP security energy and byte budget across handshake, resumption and steady-state traffic

Figure 3: Where bytes and CPU go over a device lifetime. The handshake is paid once per context, per-message overhead is paid on every exchange, and rekeying is paid on a schedule you control.

Figure 3 splits the lifetime cost into three buckets. For a node that sends a reading every fifteen minutes for years, per-message overhead and rekey frequency dominate the handshake. For a node that connects rarely and talks briefly, the handshake dominates. DTLS 1.3 with a Connection ID amortises its handshake well when the association stays alive, but a device that sleeps for hours often loses NAT state and server-side session state, and then it pays again.

OSCORE has no per-association server state beyond the context, and a context is a few dozen bytes of keys and counters. That is why a server can hold millions of them in a database and a device can resume after any gap without a new exchange, provided it kept its sequence number. That provision is where most of the real engineering sits, and it comes up again in the failure-modes section.

Proxies, Caching and Freshness: Where Object Security Changes the Network

The biggest behavioural difference between the two families is not cryptographic, it is what the network can still do. CoAP was designed with forward proxies, reverse proxies and caches in mind, and each of them interacts differently with DTLS and OSCORE.

What a proxy can and cannot do

With DTLS, a CoAP proxy that terminates the association reads the request in full, can cache the response by URI and ETag, and can rewrite anything. That is convenient and also precisely the exposure: the proxy is a fully trusted party, and so is every system with access to its memory or logs.

With OSCORE, a forward proxy needs Proxy-Scheme, Uri-Host and Uri-Port in the clear, which is why RFC 8613 makes them Class U. It can route on those and nothing else. The inner Uri-Path and Uri-Query are encrypted, so it cannot see which resource is being requested. Two consequences follow. First, a proxy cannot cache an OSCORE response in the usual way, because two requests to different resources look identical from the outside apart from the OSCORE option, and the same encrypted response is not reusable for another client with a different context. Second, an intermediary that wants to cache must be given explicit support, for example by participating as an endpoint in the security context, which makes it a trusted party again.

RFC 8613 also lists specific defensive rules. Error responses may carry an outer Max-Age of 0 so intermediaries do not cache them pointlessly. OSCORE-aware intermediaries must not initiate Observe registrations themselves. Outer Block options are unprotected, so a malicious proxy could inject block fragments; the specification therefore expects the application to define a maximum unfragmented message size so that blockwise transfer is rarely needed for protected data.

CoAP security constrained devices proxy and cache path showing which fields DTLS and OSCORE expose to intermediaries

Figure 4: Visibility by hop. Under DTLS each terminating hop sees plaintext, under OSCORE intermediaries see only routing options and an opaque payload.

Figure 4 maps the visibility. Reading it left to right, the key insight is that the cache and the proxy become routing and transport elements instead of content-aware elements. If your design depends on shared caching of sensor readings across many consumers, you either keep DTLS to a trusted caching gateway and accept that boundary, or you push the fan-out to publish and subscribe, as covered in our MQTT Sparkplug B reference architecture.

Freshness is weaker than people assume

OSCORE guarantees that a request is not older than the security context, because the replay window rejects any sequence number it has already seen or that is too far behind. It does not, by itself, prove that a request was sent recently. An attacker who delays a valid, protected “open valve” request and delivers it hours later will pass replay checks if the sequence number was not yet consumed.

RFC 8613 points to the CoAP Echo option for requests where freshness matters, such as actuation. The server responds to the first request with a challenge value in an Echo option, the client repeats the request including that value, and the server knows the repeat was created after it issued the challenge. The same mechanism resynchronises the replay window after a server reboot. If you control actuators, build this into the application profile explicitly rather than assuming “encrypted” means “timely”. DTLS gives a different guarantee: records are bound to a live association, but RFC 9147 itself states that DTLS 1.3 matches TLS 1.3 security except for order protection and non-replayability, so applications still need idempotent or freshness-checked handling of critical commands.

Observe and long-lived subscriptions

CoAP Observe lets a client register once and receive notifications. Under OSCORE the registration and each notification are protected messages, and notifications reuse the request’s token binding with fresh Partial IVs from the server’s sender sequence. That means a subscription that runs for years burns server sequence numbers, so check your counter budget against the notification rate. At one notification per second, 2^23 values last about 97 days, which is why the Zephyr bound quoted earlier is not an academic number for a high-rate stream; at one per minute the same budget lasts roughly sixteen years. These are my arithmetic on the cited bound, so recompute with your own rates and the limit your library enforces.

Key Management, Rekeying and Group Communication

A secure channel is only as useful as the provisioning behind it. The three protocols hand the key-management problem to different places.

Credentials and identity

DTLS 1.3 in a constrained profile typically uses pre-shared keys or raw public keys. PSK is cheapest but gives you a fleet-wide management problem: a PSK per device, stored securely on the server, rotated by an out-of-band process. Certificates scale identity better but cost bytes and CPU, and they pull in revocation and clock questions that small devices handle poorly.

EDHOC offers a middle path. With static Diffie-Hellman credentials identified by a short kid, the handshake is about 101 bytes in the RFC’s best case, and the credential itself can be a compact CBOR certificate or a plain public key registered at enrolment. External Authorization Data (EAD) fields, carried in EAD_1 through EAD_4, let a deployment attach authorisation tokens or certificate-enrolment hints to the handshake. An EAD item is critical if its label is negative, and an unrecognised critical item aborts the session, so a mixed fleet should decide early which EAD labels it will use.

Rekeying and forward secrecy

Plain OSCORE with a provisioned Master Secret has no forward secrecy: if the Master Secret leaks, every past and future message under that context is exposed. EDHOC fixes this at establishment because the ephemeral-ephemeral shared secret means a later compromise of long-term keys does not reveal past session keys. RFC 9528 also defines a key update function so that OSCORE keys can be refreshed with forward secrecy without a full authentication, and RFC 9529 publishes test traces useful for interoperability testing.

The operational rule is to re-run EDHOC, or apply the key update, on a schedule tied to three triggers: sequence number consumption approaching your limit, a device reboot that cannot guarantee counter persistence, and a time-based policy such as every few weeks. DTLS 1.3 has its own KeyUpdate mechanism and its own limits; the RFC bounds failed-record counts at 2^36 for GCM and ChaCha20-Poly1305 and about 2^23.5 for CCM, and says AES-CCM with an 8-byte tag should not be used in DTLS without extra forgery safeguards. That last point is a trap for anyone copying a constrained DTLS 1.2 cipher suite choice.

Group OSCORE for multicast

Multicast is where OSCORE has no DTLS equivalent at all, because DTLS needs a point-to-point association. Group OSCORE, defined in the CoRE working group’s draft-ietf-core-oscore-groupcomm, extends OSCORE to a group of members. At the time I checked, the version available was draft 28 dated 23 December 2025, still an Internet-Draft; verify its status before citing it as an RFC.

It has two modes. In group mode the sender signs each message with its private key, and the signature is carried inside the protected message, so any member, or an allowed intermediary, can verify who sent it. In pairwise mode two members use symmetric keys derived from a Diffie-Hellman shared secret, giving shorter tags and less overhead. A common pattern is a signed group request with pairwise-protected individual responses, which avoids every responder paying for a signature. A Group Manager, which need not be a member, handles joining, key distribution, rekeying and Sender ID assignment. Signature cost on constrained hardware is the main limiter, so size your groups and request rate with measurements rather than with the nominal algorithm list.

Where this matters in practice is lighting, building automation and firmware announcements: one request to a hundred devices, with authentication. It does not replace a proper application-layer authorisation model, and our LwM2M device management architecture shows how a management server layers authorisation, bootstrapping and object models over CoAP.

Deployment on Zephyr and Other Constrained Stacks

Zephyr is the most visible open-source RTOS with a documented OSCORE path, and its behaviour is a useful reference for what a production integration looks like. The documentation describes OSCORE as part of the CoAP library, enabled with CONFIG_COAP_OSCORE=y, depending on the uoscore-uedhoc module and on PSA Crypto. uoscore selects the PSA algorithms it needs, such as AES-CCM and HKDF-SHA256.

# prj.conf  (sketch based on the Zephyr OSCORE documentation)
CONFIG_NETWORKING=y
CONFIG_COAP=y
CONFIG_COAP_OSCORE=y
CONFIG_UOSCORE=y
CONFIG_PSA_CRYPTO=y
CONFIG_COAP_OSCORE_MAX_CONTEXTS=4
CONFIG_COAP_OSCORE_EXCHANGE_CACHE_SIZE=4
CONFIG_COAP_OSCORE_EXCHANGE_LIFETIME_MS=10000
# Only if you persist the sender sequence number across reboots:
CONFIG_COAP_OSCORE_CONTEXT_REUSE=y

Services are declared with COAP_SERVICE_DEFINE_OSCORE, or COAPS_SERVICE_DEFINE_OSCORE for a service that also runs over DTLS, with a final argument of true to require OSCORE for every request. Contexts are added with coap_oscore_context_add(), one per client Recipient ID, and incoming requests are matched on Recipient ID and ID Context. The values above are placeholders; check the option names and defaults against the Zephyr release you build, because Kconfig symbols move between versions.

Two caveats from the documentation deserve a place in your design review. First, on a service that accepts both OSCORE and plaintext clients, a deferred response sent after its exchange-cache entry has expired can go out unprotected. Use an OSCORE-required service for anything sensitive. Second, the Zephyr docs say EDHOC is only referenced via the uoscore-uedhoc module and give no EDHOC API of their own, so plan on integrating that module, or an alternative such as libedhoc, directly. I did not verify RIOT’s current OSCORE or EDHOC coverage, so confirm it against RIOT’s documentation before relying on it.

On the server side, the aiocoap Python library, liboscore and several Java and C stacks implement OSCORE. Test interoperability early using the test vectors in RFC 8613 Appendix C and the EDHOC traces in RFC 9529, because the failure mode of a mismatch is silent decryption failure.

Decision matrix

Scenario DTLS 1.3 OSCORE plus EDHOC Reason
Sensor directly to owned gateway, always-on Good Good One hop; both work. Pick what your stack supports.
Cellular node behind NAT, long sleep Good with CID Better OSCORE needs no live session; CID helps DTLS only while state survives.
Path crosses third-party proxy or HTTP translator Weak Strong Object security survives translation and hides payload.
Multicast commands to many devices Not applicable Group OSCORE DTLS needs unicast associations.
Shared caching of readings Strong at trusted cache Weak Encrypted responses are not reusable across contexts.
Smallest flash and RAM budget Heavier Lighter Benchmark page reports roughly two to four times less RAM, with caveats.
Existing TLS PKI and tooling Strong Moderate DTLS reuses TLS operational practice.

The matrix has no single winner, which is the point. Treat any claim of a universal winner with suspicion.

Trade-offs, Gotchas, and What Goes Wrong

Nonce reuse after reboot. This is the single most dangerous OSCORE failure. If a device restarts, resets its sender sequence number to 0 and keeps using the same Master Secret, it reuses (key, nonce) pairs, and with AES-CCM that breaks confidentiality and integrity. RFC 8613 Section 7.5 lists the remedies: restore the sequence number from non-volatile memory, derive a new context from the existing Master Secret using a method that guarantees fresh randomness, or run a forward-secret key exchange such as EDHOC. Writing flash on every message wears it out, so persist in large steps, for example reserving a block of 1,000 sequence numbers ahead of use and skipping the unused remainder after a crash.

Replay window loss. The server’s replay window is not persisted in the Zephyr implementation and is resynchronised using Echo. If your server cannot issue Echo challenges for some request classes, a restart briefly leaves a replay hole.

Plaintext fallback. Mixed-mode services that accept both protected and unprotected requests are a standing downgrade risk. Make the policy explicit and fail closed: unprotected requests to protected resources get an error, and responses that cannot be protected are dropped, not sent in clear.

Size surprises. OSCORE adds an option and an AEAD tag to each message. With the default 8-byte tag, plus the OSCORE option bytes, expect on the order of tens of bytes of overhead per message, which can push a payload past a link-layer fragmentation boundary. RFC 8613 contains compression examples, for instance a 24-byte uncompressed COSE structure compressing to 17 bytes on the wire, but measure your own messages.

Static DH key compromise. EDHOC’s static-DH methods give up the non-repudiation of signatures, and a compromised static key allows impersonation of that party going forward, although past session keys stay safe thanks to ephemeral keys. Choose signature methods where audit evidence matters.

Unproven benchmarks. Published memory and latency figures come from specific boards and libraries. A change of crypto backend, such as from tinycrypt to a hardware accelerator, can shift results by a large factor. Do not copy another team’s flash budget.

Practical Recommendations

Start from the trust boundary. Draw the path from device to the system that actually consumes the data, mark every hop that you do not fully control, and ask whether plaintext at that hop is acceptable. If the answer is yes for every hop, DTLS 1.3 with a Connection ID is a sound, conventional choice that reuses TLS operational habits. If the answer is no for any hop, or if devices sleep for long periods, or if you need multicast, use OSCORE with EDHOC.

Decide credentials before protocols. A fleet that cannot enrol, rotate and revoke credentials will fail regardless of the cipher. Prefer EDHOC with static-DH or compact credentials when bytes are tight, signature methods when you need audit evidence, and schedule re-keying instead of waiting for an incident.

Treat counters as a safety-critical resource. The sequence number is the thing that keeps an AEAD safe, and the thing most often lost in a crash.

  • Map each hop and decide plaintext exposure explicitly.
  • Choose DTLS 1.3 with CID for single-hop, always-on links; OSCORE plus EDHOC otherwise.
  • Verify key derivation against RFC 8613 Appendix C and EDHOC against RFC 9529 traces in CI.
  • Persist the sender sequence number in blocks and recover through Echo or a fresh EDHOC run.
  • Require protection on sensitive services and fail closed; no mixed-mode endpoints.
  • Add Echo-based freshness to any actuation endpoint.
  • Budget sequence numbers against Observe notification rate and rekey on a schedule.
  • Re-measure flash, RAM and handshake time on your own board and crypto backend.
  • Watch Group OSCORE status before building on it in a product with a long support life.

Frequently Asked Questions

Is OSCORE a replacement for DTLS?

Not in every case. OSCORE protects CoAP messages end to end at the application layer, which suits proxies, translation, multicast and sleepy devices. DTLS 1.3 protects a single datagram association and is simpler when one hop is your whole trust boundary. Many deployments use both: DTLS between a gateway and a cloud endpoint, OSCORE from the device through the gateway. Choose by who must be able to read the data on the path.

What is the relationship between EDHOC and OSCORE?

EDHOC, specified in RFC 9528, is an authenticated key exchange with ephemeral Diffie-Hellman keys, and its main purpose is to establish an OSCORE security context. OSCORE, specified in RFC 8613, then protects the actual CoAP traffic using keys derived from the EDHOC exporter. EDHOC adds forward secrecy and mutual authentication; OSCORE alone assumes you already provisioned a shared Master Secret by some other means.

How big is an EDHOC handshake compared with DTLS 1.3?

RFC 9528 Table 1 lists 101 bytes for the smallest static-DH run with key identifiers and 242 bytes for a signature-based run referencing certificates by hash. A published uoscore-uedhoc benchmark page reports 808 bytes with two 293-byte X.509 certificates against 1,500 bytes for DTLS 1.3, though the DTLS number was taken from a separate paper. Expect real gaps of roughly a factor of two on certificate-based runs.

Does OSCORE work with CoAP proxies and caches?

Forwarding works, caching mostly does not. Proxy-Scheme, Uri-Host and Uri-Port stay visible so a forward proxy can route, but the inner URI path, query and payload are encrypted. Because the encrypted responses are bound to one security context, a shared cache cannot reuse them for other clients. If you need caching, terminate security at a trusted caching gateway or push distribution to a publish and subscribe layer.

What happens to OSCORE when a device reboots?

The sender sequence number must never repeat under the same keys. After a reboot, either restore the counter from non-volatile memory, derive fresh keys using a method with good randomness, or run EDHOC again. The server’s replay window can be resynchronised using the CoAP Echo option. Failing to do one of these causes nonce reuse, which compromises AES-CCM security. This is the most important operational detail in OSCORE.

Is Group OSCORE a finished standard?

At the time of writing, the document I could confirm was Internet-Draft draft-ietf-core-oscore-groupcomm-28, dated 23 December 2025, from the IETF CoRE working group. It defines a signed group mode and a pairwise mode, with a Group Manager handling keys. Check the IETF datatracker for whether it has been published as an RFC before treating it as final in a product specification.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *