Constrained-Device Security: CoAP, DTLS 1.3, OSCORE and EDHOC Compared (2026)

Constrained-Device Security: CoAP, DTLS 1.3, OSCORE and EDHOC Compared (2026)

OSCORE, EDHOC and DTLS 1.3: CoAP Security for Constrained Devices (2026)

Most teams secure a CoAP deployment by wrapping it in DTLS and moving on. That works until the first battery-powered node sits behind a cache, a border router or a cloud proxy that must read the request to forward it. At that point the security boundary you drew at the radio link stops at the proxy, and everything past it is decrypted, inspected and re-encrypted by a party you may not fully trust. The handshake itself is the other surprise: a certificate-based DTLS 1.3 exchange can cost around a kilobyte and a half on the air, many times the size of a typical sensor reading.

OSCORE, short for Object Security for Constrained RESTful Environments, takes a different route. It protects the CoAP message itself instead of the pipe it travels through, and EDHOC provides a key exchange small enough to bootstrap it. Together they trade the session abstraction of DTLS for end-to-end protection that survives proxies, transport changes and sleepy devices.

This post compares the two stacks on threat model, bytes on the wire, memory footprint, proxy behaviour, group communication and operational failure modes. You will leave with a decision matrix, worked handshake arithmetic with every estimate labelled, and runnable configuration for aiocoap.

What this covers: the constrained-device threat model, DTLS 1.3 (RFC 9147), OSCORE (RFC 8613), EDHOC (RFC 9528) and their combination (RFC 9668), proxies and caching, Group OSCORE, credential types, footprints, LwM2M integration, runnable examples and failure modes.

Context and Background

The Constrained Application Protocol (CoAP, RFC 7252) is the REST-style protocol of low-power IoT. It runs over UDP, uses a compact binary header of four bytes, and adds reliability through confirmable messages with exponential retransmission. Its original security story was Datagram Transport Layer Security (DTLS): RFC 7252 defines a coaps scheme and specifies DTLS 1.2 with pre-shared keys, raw public keys or X.509 certificates. For many fleets that is still what ships today.

DTLS was designed for general-purpose datagram applications, and it carries that heritage. It binds security to a session between two transport endpoints. That binding is exactly what a constrained network struggles with: NAT rebinding changes the peer address, a sleeping device loses its session state, and any intermediary that terminates the session becomes a trusted party. The IETF CoRE and LAKE working groups responded with two complementary building blocks. OSCORE, published as RFC 8613 in July 2019, secures the CoAP message end to end. EDHOC, published as RFC 9528 in March 2024, is a lightweight authenticated Diffie-Hellman key exchange whose main use case is establishing an OSCORE security context.

In parallel the TLS family moved forward. DTLS 1.3 was published as RFC 9147 in April 2022 and obsoletes DTLS 1.2 (RFC 6347). It removes legacy cryptography, shortens the handshake, adds a compact record header and a Connection ID (CID) mechanism so that a session can survive an address change. RFC 9668, published in November 2024, then defined how EDHOC runs over CoAP and how to merge it with the first OSCORE request.

So in 2026 an architect has two modern, standards-track choices for CoAP security, not one. They are not interchangeable. If you have not yet chosen an application protocol, our comparison of MQTT-SN, MQTT and CoAP for constrained sensor networks covers when CoAP is the right base. For the TLS 1.3 mechanics that DTLS 1.3 inherits, see TLS 1.3 handshakes, 0-RTT and mTLS performance on constrained IoT devices. The standards themselves are authoritative and readable; the RFC 8613 text is a good starting point for the object-security model.

A note on how this article treats numbers. Where a figure comes from an RFC or a published benchmark I name the source. Where I derive a figure from the wire formats, I mark it as an estimate. Footprint numbers in particular vary with compiler, options and library version, so treat them as orders of magnitude and measure your own build.

The Threat Model for Constrained Nodes

Before comparing protocols, fix what you are defending against. A constrained node differs from a server in four ways that change the threat model. It is physically accessible, so key extraction by an attacker with a debugger is plausible. It is radio-exposed, so passive eavesdropping and injection are cheap. It is resource-starved, so any cryptographic operation competes with battery life. And it is usually reached through intermediaries such as border routers, cloud gateways and caches.

Threat model for constrained devices showing eavesdropping, rogue proxy, spoofing and key extraction against a CoAP path and where OSCORE and DTLS 1.3 apply

Figure 1: Threat model for a constrained CoAP path. Dotted lines mark attacker positions: radio eavesdropping, a compromised proxy, spoofed sources and physical key extraction.

Figure 1 sketches the path from sensor to server. Four attackers matter. The radio eavesdropper reads and replays packets; any confidentiality, integrity and replay protection handles it. The rogue or curious intermediary sits in a proxy or cache and either reads payloads or rewrites options; only end-to-end protection handles it. The spoofer sends packets with a forged source address, aiming either to exhaust the server or to reflect traffic at a victim; this is an amplification and denial-of-service concern that DTLS addresses with a cookie exchange and OSCORE addresses by being cheap to reject. The physical attacker extracts keys; neither protocol prevents it, but forward secrecy limits what stolen long-term keys reveal about past traffic.

Notice what is absent from the list: a passive attacker with a quantum computer. Both EDHOC and DTLS 1.3 as standardised rely on elliptic-curve Diffie-Hellman, so neither is post-quantum on its own. That is an honest limit worth stating early, and it is revisited in the gotchas section.

Reference Architecture: Transport Security versus Object Security

The core distinction is the layer at which protection happens. DTLS 1.3 protects datagrams between two transport endpoints. OSCORE protects CoAP messages between two application endpoints, and the result is still an ordinary CoAP message that any CoAP-aware intermediary can forward.

In one sentence: DTLS 1.3 secures a hop and OSCORE secures an exchange. DTLS 1.3 encrypts everything above UDP between two endpoints, so proxies must terminate it. OSCORE encrypts the CoAP code, most options and the payload end to end, leaving only routing fields visible, so proxies can forward and even cache without seeing content.

CoAP security layering comparing DTLS 1.3 transport security with OSCORE object security for constrained devices

Figure 2: Where each mechanism sits. DTLS wraps the whole CoAP message in a record over UDP. OSCORE rewrites the CoAP message into an outer message carrying an encrypted inner message, and works over any transport.

Figure 2 shows the two layerings. Under DTLS, the CoAP message is application data inside a DTLS record, and the record is the only thing on the wire above UDP. Under OSCORE, the original CoAP request is split into an inner part and an outer part. The inner part, called the plaintext, holds the real method or response code, the sensitive options and the payload, and it is encrypted and authenticated with an AEAD cipher. The outer part is a new CoAP message that carries the ciphertext as payload, a new code (POST for requests, 2.04 Changed for responses, or FETCH and 2.05 Content when Observe is used), the OSCORE option, and any options a proxy needs.

How DTLS 1.3 protects a CoAP exchange

DTLS 1.3 is the TLS 1.3 handshake adapted for datagrams. The client sends a ClientHello with a key share, the server answers with its own key share and encrypted authentication messages, and the client finishes. A server that wants to avoid state for unverified senders replies with a HelloRetryRequest carrying a stateless cookie, which adds a round trip; RFC 9147 says servers should perform this exchange by default. Handshake flights are acknowledged with explicit ACK records, and the retransmission timer starts at 1000 ms and doubles on each retry.

The record layer is where DTLS 1.3 improved most for constrained devices. The unified header starts with a first byte whose top bits are 001, followed by flags for the Connection ID, a short or long sequence number, an optional length field and the low two bits of the epoch. Sequence numbers are 8 or 16 bits on the wire out of a 48-bit full number, and they are masked with a value derived from the ciphertext so that observers cannot trivially track a device. Ciphertexts must be at least 16 bytes for that masking to work, so short messages are padded. The Connection ID, when negotiated, appears in every datagram, which is what lets a session survive an IP address change.

What you get from this is a full session abstraction: key update with a KeyUpdate message, resumption with tickets, even post-handshake client authentication. What you pay is a stateful association, a handshake that exchanges certificates or key shares in several flights, and a trust boundary at every endpoint that terminates the session.

How OSCORE protects a CoAP exchange

OSCORE has no handshake of its own. It assumes both endpoints already share a Master Secret, a Master Salt and identifiers, from which each derives a Sender Context and a Recipient Context using HKDF (HMAC-based key derivation, SHA-256 by default). The default AEAD (authenticated encryption with associated data) algorithm is AES-CCM-16-64-128: a 128-bit key, a 13-byte nonce and a 64-bit authentication tag.

Each message carries a Partial IV, which is the sender’s sequence number encoded in as few bytes as possible. The nonce is built by padding the Partial IV and the Sender ID and XORing the result with a Common IV, so sender and recipient derive the same nonce without it being transmitted in full. The sender sequence number starts at zero and must stay below 2^40; once an endpoint reaches the maximum it must stop using that Sender Context. The recipient keeps an anti-replay sliding window, with a default size of 32 as in DTLS.

The OSCORE option itself is compact. Its value is a flag byte, the Partial IV, an optional kid context and the kid (the sender identifier). It is critical, safe to forward, part of the cache key and not repeatable. A request from a freshly started device with a small sequence number and a one-byte sender ID needs only a few bytes of option value.

Option classes: what an intermediary can still see

The security properties hinge on how RFC 8613 classifies CoAP options. Class E options are encrypted and integrity protected inside the ciphertext: If-Match, ETag, If-None-Match, Location-Path, Uri-Path, Content-Format, Uri-Query, Accept and Location-Query. Class U options stay in the clear on the outer message: Uri-Host, Uri-Port, Proxy-Uri, Proxy-Scheme and the OSCORE option itself. A third set, Observe, Max-Age, Block1, Block2, Size1, Size2 and No-Response, are processed by intermediaries and so appear on the outer message, with the Observe, block and No-Response values also carried in a protected copy where they matter. Class I, integrity-only, has no options defined in the RFC.

The practical consequence is blunt. A proxy forwarding an OSCORE request sees the destination, the OSCORE option, the CoAP message type and token, and the size of the ciphertext. It does not see the resource path, the query, the content format or the payload. That is a strong privacy improvement over a DTLS-terminating proxy, and it is also the source of every awkward corner discussed later.

EDHOC and the Combined Handshake: Walk-Through with Worked Numbers

OSCORE is only as good as the keys it starts from. The RFC deliberately leaves key establishment out, and early deployments provisioned Master Secrets at the factory. That scales poorly and gives no forward secrecy: anyone who later extracts the factory secret can decrypt recorded traffic. EDHOC (Ephemeral Diffie-Hellman Over COSE) fills the gap with a three-message authenticated key exchange built on the CBOR and COSE formats that CoAP stacks already contain.

The EDHOC message flow

EDHOC defines an Initiator and a Responder. Message 1 carries the selected method, the cipher suites the Initiator supports, its ephemeral public key and a connection identifier. Message 2 returns the Responder’s ephemeral key and an encrypted block containing the Responder’s credential identifier and a MAC or signature. Message 3 delivers the Initiator’s credential identifier and its own MAC or signature, also encrypted. An optional fourth message exists for profiles that need explicit key confirmation to the Initiator, but it is typically not sent. RFC 9528 states that the Initiator can derive application keys once it has created message 3, so protected application data can follow immediately.

Four authentication methods are defined by which side uses a signature key and which uses a static Diffie-Hellman key. Method 0 uses signatures on both sides, method 1 a signature for the Initiator and static DH for the Responder, method 2 the reverse, and method 3 static DH on both sides. Static DH authentication is cheaper in bytes because a MAC replaces a signature, which matters in the table below.

EDHOC three message flow producing an OSCORE security context between constrained devices

Figure 3: EDHOC key exchange followed by the first OSCORE transaction. The Initiator can send message_3 together with its first protected request, which is the RFC 9668 optimization.

Cipher suites are grouped by intent. Suites 0 to 3 use AES-CCM and target constrained IoT where overhead matters most. Suites 4 and 5 use ChaCha20 with 128-bit tags, suite 6 uses AES-GCM for unconstrained settings, and suites 24 and 25 target high-security use, with suite 24 built on CNSA 1.0 algorithms. Identity protection is built in: the Initiator’s identity is hidden from passive observers and the Responder’s identity from active ones. Forward secrecy comes from the ephemeral keys. The EDHOC MAC length must be at least 8 bytes.

Message sizes from the specification

RFC 9528 Table 1 reports message sizes in bytes for a minimal setup using a one-byte key identifier (“kid”) as the credential reference, and for a setup where the credential is referenced by an X.509 certificate hash (“x5t”).

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

Read this table the way the authors intend. The 101-byte total assumes both parties already hold each other’s raw public keys, so no certificate crosses the air. The 242-byte total with signatures still assumes the peer’s certificate is pre-established and only referenced by hash. When certificates must be transmitted, the bytes grow by the certificate size. The uoscore-uedhoc project reports 808 bytes when two 293-byte X.509 certificates are exchanged, against 242 bytes with pre-established certificates and 101 bytes with pre-established static DH keys. The same page lists 1,371 bytes for a TLS 1.3 handshake and 1,500 bytes for DTLS 1.3 under comparable certificate-based conditions. RFC 9528 itself states that, for raw public key authentication over CoAP, EDHOC messages can be less than one seventh the size of a DTLS 1.3 handshake using ECDHE and a connection ID.

Worked arithmetic: what the bytes cost on a lossy radio

Take IEEE 802.15.4 as the example link. Its maximum physical frame is 127 bytes, and after the MAC header, 6LoWPAN compression and UDP, an application can typically count on roughly 60 to 90 bytes of payload per frame. These are illustrative engineering figures that depend on addressing mode and security at the link layer, not constants. Using 80 bytes as the working figure:

  • EDHOC with static DH keys, 101 bytes in three messages: message 1 (37 bytes) fits in one frame, message 2 (45) fits, message 3 (19) fits. Three frames, no fragmentation.
  • EDHOC with signatures and certificate hashes, 242 bytes: message 2 (115) and message 3 (90) each need two frames. Message 1 fits. Five frames.
  • EDHOC exchanging two 293-byte certificates, 808 bytes: roughly ten frames, with message 2 and 3 fragmented through block-wise transfer (RFC 7959) or lower-layer fragmentation.
  • DTLS 1.3 with certificates, about 1,500 bytes: roughly nineteen frames, and DTLS flights must be retransmitted as flights if any fragment is lost.

Now apply a per-frame loss rate. Suppose, illustratively, 10 percent of frames are lost and a lost fragment triggers retransmission of the affected message. With three single-frame messages, the expected number of transmissions per message is about 1/(1-0.1), or 1.11, so the handshake needs on the order of three and a third frames. With a nineteen-frame handshake in five or six flights, the probability that at least one frame in a flight is lost rises sharply: a four-frame flight survives intact with probability 0.9 to the fourth power, about 66 percent, so about a third of such flights require a retry. Energy follows frame count, and so does latency. This is a simplified model with independent losses; real links have bursty loss, which makes fragmentation costlier still.

EDHOC plus OSCORE in two round trips

RFC 9668 defines two flows. In the sequential flow EDHOC completes first and the first OSCORE request follows, which the RFC says takes three round trips. In the combined flow, the client sends EDHOC message 3 and its first OSCORE-protected request in one CoAP message, and the sequence finishes in two round trips. The mechanism uses a new CoAP option, the EDHOC option (number 21). It is critical, safe to forward, part of the cache key, used in requests only, and must be empty. The message 3 bytes are prepended to the OSCORE ciphertext in the payload. The EDHOC connection identifier C_R is not repeated in the payload; it is carried in the kid field of the OSCORE option, because it doubles as the client’s OSCORE Sender ID. The server uses that kid to find the EDHOC session, processes message 3, derives the OSCORE context from the EDHOC output, and decrypts the request.

Limits apply. The optimization only works when the client is the EDHOC Initiator and the server the Responder, and it does not apply if the application profile requires message 4. A server that does not understand the EDHOC option rejects the request as carrying an unprocessed critical option, so clients need a fallback to the sequential flow.

Overhead of OSCORE per message

After the handshake, per-message overhead matters more than handshake bytes because it is paid forever. Deriving from the formats, a request needs an outer CoAP option header, the flag byte, a one-byte Partial IV and a one-byte kid in the OSCORE option, a payload marker, one byte for the inner code and the 8-byte tag of AES-CCM-16-64-128. My estimate is roughly 13 to 16 bytes of added overhead per request, and about 10 to 12 bytes for a response, because responses usually omit the Partial IV and kid. Treat these as derived estimates; RFC 8613 provides worked compressed examples in Section 6.3 if you want to verify against your own message mix. Option bytes that move from the outer to the inner message are not extra.

DTLS 1.3 overhead per record is comparable. A minimal unified header is one byte plus a one- or two-byte sequence number, an optional two-byte length, an optional connection ID, one inner content-type byte and the AEAD tag, which is 8 bytes only with the CCM_8 suites and 16 bytes with the mainstream ones. A typical figure is 12 to 24 bytes, again my estimate. The difference is not decisive; what decides is the handshake, session state and the proxy model.

Computation cost

Latency measured by the uoscore-uedhoc authors is computation-only, with no radio time, and gives a feel for the CPU budget. Key exchange with exchanged X.509 certificates took 0.402 s on an ESP32 at 160 MHz, about 1.17 s on an nRF52840 (Cortex-M4 at 64 MHz) and about 11.7 s on an nRF51822 (Cortex-M0 at 16 MHz). With pre-established certificates the figures were 0.294 s, 0.86 s and 8.6 s. Encrypting or decrypting a 100-byte OSCORE payload took 977 microseconds on the ESP32, 3.7 ms on the nRF52840 and 20.5 ms on the nRF51822. The page gives no TLS or DTLS latency for comparison, so I cannot claim a speed ratio. The honest takeaway: handshake cost on a Cortex-M0 is dominated by elliptic-curve operations, and it argues for static DH keys or pre-computed credentials on the weakest parts.

Proxies, Caching and Group Communication

The sharpest practical difference between the stacks shows up the moment something sits between client and server. Constrained deployments almost always have such a thing: a border router that translates between 6LoWPAN and IP, a CoAP-to-HTTP gateway, a forward proxy that caches responses for sleepy nodes, or a cloud ingress that fans requests out.

What a proxy can and cannot do under OSCORE

With DTLS, a proxy must terminate the session on one side and open another on the other side. It sees plaintext, it can cache by URI, and it can synthesise responses. It is, in effect, a trusted party for every byte. If that proxy runs in a shared cloud environment, the confidentiality of your telemetry rests on that environment.

With OSCORE the proxy forwards an opaque message. It reads the destination from Uri-Host, Uri-Port or Proxy-Uri, and it handles outer Observe, Max-Age and Block options. RFC 8613 is explicit about what it cannot do. A proxy cannot create requests or responses that verify as coming from the other endpoint, cannot start or cancel a verifiable Observe registration, and cannot turn an ordinary response into a verifiable notification. Because Uri-Path and Uri-Query are encrypted, a proxy may mix up blocks belonging to concurrent requests to the same server. Because outer Block options are unprotected, a proxy could inject fragments indefinitely, so applications are told to set a maximum unfragmented message size. Clients must also be ready to receive and discard 5.04 Gateway Timeout errors that intermediaries generate, since those cannot be protected.

Caching trade-offs

Caching is where the abstraction leaks. The OSCORE option is part of the cache key, and it contains the Partial IV, which changes with every request. A conventional proxy cache therefore never gets a hit for two separate OSCORE requests from the same client, let alone from different clients. Max-Age remains visible on the outer message, so a proxy can still hold stale-or-fresh judgments on forwarded responses, but it cannot reuse a response encrypted for a different recipient.

This matters for the dominant IoT pattern of many readers observing a few sensors. With DTLS you can place a caching proxy in front of the sensor and offload it, at the cost of trusting the proxy. With OSCORE end to end you cannot, unless you either accept per-requester traffic to the device or move to Group OSCORE and an architecture where a trusted intermediary re-publishes data. The cleaner design is often to treat the constrained network as a hop that terminates at a gateway with its own OSCORE context towards the device, then publish northbound through a normal broker. For that pattern, see our guide to MQTT store-and-forward at the edge gateway, which discusses what the gateway should and should not hold.

Group OSCORE

Multicast is a natural fit for building automation and lighting: one request to every luminaire in a corridor. Pairwise OSCORE and DTLS both fail here, because one sender would need a separate security association per receiver. Group OSCORE extends the model so a group of endpoints shares group keying material and each sender signs its messages, so a recipient can verify which group member sent the message, not merely that some member did. There is also a pairwise mode for unicast within the group that uses key material derived from static-static Diffie-Hellman.

The status deserves care. Group OSCORE is specified in the Internet-Draft draft-ietf-core-oscore-groupcomm, and the latest revision I could confirm is version 28, dated December 2025. I could not confirm whether it has since been published as an RFC, so check the datatracker before citing a number. The aiocoap project implements Group OSCORE but notes that group use requires further manual steps. Mark it as promising but less settled than pairwise OSCORE, and note that group signatures cost far more CPU than a MAC on a Cortex-M0.

DTLS 1.3 and group traffic

DTLS has no multicast mode. If a design depends on one-to-many secure CoAP, the realistic options are Group OSCORE, or fan-out to unicast sessions, or application-layer publish-subscribe through a broker. That alone settles the decision for some lighting and HVAC deployments.

Credentials: Pre-Shared Keys, Raw Public Keys and Certificates

Both stacks accept three families of credentials, and the choice influences footprint and operations more than the protocol does.

Pre-shared keys (PSK) are the smallest and the cheapest. There is no signature verification, no certificate parsing and the handshake shrinks. The operational cost is provisioning and rotation: every device needs a unique secret, and compromise of the server’s key store exposes every device. With DTLS 1.3, PSK modes can run with or without an ephemeral Diffie-Hellman exchange; use the variant with it if you want forward secrecy.

Raw public keys (RPK) put a bare public key on the device and in a registry. They give the strongest byte savings in EDHOC, the 101-byte static DH case above, and avoid the X.509 parser, which is a large and historically bug-prone component. The cost is that you lose certificate semantics: no validity period, no chain, no standard revocation. You must run your own registry and decide how a key is revoked.

X.509 certificates give you the PKI toolchain: issuance, expiry, revocation and a recognised audit story. EDHOC can reference a certificate by hash (x5t) if the peer already has it, or transmit it in full. In full-transmission mode certificates dominate handshake bytes, as the 808-byte figure shows. A common compromise, hinted at by the numbers above, is to authenticate with certificates but cache the peer certificate after first contact.

A fourth option has appeared in EDHOC practice: CWT or CCS claim sets (CBOR Web Token and CWT Claims Set) as compact credential containers. I mention them without a size figure because I did not verify one.

Cost summary by credential type

Credential Handshake bytes Needs parser Rotation Fit
PSK Smallest None Manual, per device Closed fleets, tiny MCUs
Raw public key Small (101 B EDHOC static DH) CBOR/COSE only Registry managed Mid-size fleets with own registry
Certificate by reference Moderate (242 B EDHOC) X.509 on the peer side at least PKI Regulated or long-lived fleets
Certificate in full Large (808 B EDHOC, about 1.4 to 1.5 KB TLS or DTLS 1.3) Full X.509 PKI Open ecosystems, first contact

Footprint: Flash, RAM and Code Complexity

Memory decides what runs on a Cortex-M0 class part with 32 KB of RAM. The only published head-to-head I could verify is the uoscore-uedhoc benchmark page, measured on an nRF52840 with P-256 and AES-128-CCM and X.509 certificates. The (D)TLS figures there are drawn from mbed TLS as cited in a research paper, not freshly measured on the same toolchain, so the comparison is indicative, not a controlled benchmark.

Stack Flash (bytes) RAM (bytes)
uoscore-uedhoc 3.0.x (OSCORE plus EDHOC) 41,156 5,639 initiator, 5,351 to 6,656 responder depending on array mode
TLS 1.3 (mbed TLS) 52,023 21,692
DTLS 1.3 (mbed TLS) 58,097 22,026

The RAM numbers for uoscore-uedhoc count stack memory only, so they are not directly comparable to heap-using stacks. Even with that caveat, the shape is clear: the combined OSCORE and EDHOC implementation uses roughly 20 to 30 percent less flash and about a quarter of the RAM. The RAM advantage comes from the absence of a session layer, record buffers, a certificate chain and handshake state for many message types.

The independent academic study “The Cost of OSCORE and EDHOC for Constrained Devices” (arXiv 2103.13832) also compares these stacks, and is worth reading for its methodology; I did not extract its numbers, so I do not quote them. Two other claims you will see online I could not verify and do not make: that a specific DTLS 1.3 build fits in a given RAM budget on tinyDTLS, and a precise libcoap footprint. The libcoap project documents OSCORE support in its coap_oscore man page; check the library version and TLS backend you actually use, because DTLS 1.3 availability in embedded libraries has lagged the RFC and depends on the backend.

Integrating with LwM2M and Device Management

Lightweight M2M (LwM2M) layers device management on CoAP, and it fixes its security options at the transport. Release 1.0 and 1.1 specify DTLS with PSK, RPK or certificates, and 1.1 added TLS for TCP transports. LwM2M 1.2 added support for OSCORE with EDHOC as an alternative, which I recall from the OMA specification roadmap but did not re-verify this run, so check the current OMA SpecWorks release before depending on it. The architectural implication is worth thinking through regardless. LwM2M uses a bootstrap server to provision credentials, a management server for operations and often a proxy or gateway in between. Object security would let the management server be reached across an untrusted gateway without trusting it with payloads, which suits operators that pay a third party for connectivity.

For a full tour of the object model and registration flows, read our LwM2M device management architecture guide. In the context of this article, the question to ask of any LwM2M client you buy is which security modes it implements and whether its DTLS stack supports Connection ID, since NAT rebinding on cellular IoT links kills sessions without it.

Running It: aiocoap with OSCORE and EDHOC

aiocoap is a Python CoAP library that implements OSCORE, Group OSCORE and a partial EDHOC implementation. Its documentation states that EDHOC support is still being extended and that the credential format is experimental and subject to change, so pin versions and expect to revisit these snippets. The following steps follow the documented tool flow; I have not executed them in this run, so treat them as a template to verify against your installed version.

First, generate a key for each side with the bundled tool, giving each a key identifier:

pip install "aiocoap[all]"
aiocoap-keygen generate server.key --kid 01
aiocoap-keygen generate client.key --kid 02

Next, write a credentials file for the server. The entry type is edhoc-oscore, with the suite, method, how the own credential is presented, the private key file and the peer credential. The structure below mirrors the documentation’s example, suite 2 and method 3 (static DH on both sides). The exact field values for own_cred and peer_cred are produced by the keygen tool, so replace the placeholders:

{
  "coap://localhost/*": {
    "edhoc-oscore": {
      "suite": 2,
      "method": 3,
      "own_cred_style": "by-key-id",
      "own_cred": "<printed by aiocoap-keygen for server.key>",
      "private_key_file": "server.key",
      "peer_cred": "<printed by aiocoap-keygen for client.key>"
    }
  }
}

Then start the example file server and a client with their credentials files:

aiocoap-fileserver --credentials server.json ./served &
aiocoap-client --credentials client.json coap://localhost/hello.txt

Two documented caveats deserve emphasis. Providing credentials does not by itself require encryption: unprotected requests still work, so a real deployment needs a policy that rejects them. And the example file server performs no authorization, so any authenticated client can do whatever any other can. For manual OSCORE without EDHOC, aiocoap stores contexts in a directory with a settings file holding sender and recipient IDs and the secret, with the stern warning that each secret must be used only once and contexts must be moved, not copied. That warning leads directly into the failure modes.

Decision Matrix: Choosing Between the Stacks

Choosing is easier once you separate three questions: who must not see the data, how constrained is the weakest device, and does the traffic leave a single link. Figure 4 turns that into a decision path.

Decision tree for choosing OSCORE, Group OSCORE, EDHOC or DTLS 1.3 for CoAP security on constrained devices

Figure 4: Decision path for CoAP security. An untrusted proxy in the path points to OSCORE; one-to-many traffic points to Group OSCORE; tight flash, RAM or a lossy radio points to EDHOC plus OSCORE; otherwise DTLS 1.3 is a reasonable general choice.

The tree is a starting point, not a verdict. The matrix below scores both approaches against common deployment situations. The ratings are my judgement from the properties described above, not benchmark results.

Situation DTLS 1.3 OSCORE with EDHOC Comment
Battery sensor behind a cloud proxy you do not control Weak: proxy terminates the session Strong: end-to-end OSCORE keeps payloads opaque to the proxy
Cortex-M0 class node, 32 KB RAM Tight: roughly 22 KB RAM in the cited build Better: about 5.6 KB stack in the cited build Verify on your own toolchain
Lossy 802.15.4 mesh Costly: larger flights, fragmentation Better: 101 to 242 bytes of handshake Use static DH where possible
Existing UDP application beyond CoAP Good: protects any datagram protocol Not applicable OSCORE is CoAP-specific
Building-wide multicast control Not supported Group OSCORE, draft status Check the draft status
Mature library support on gateways Wide in general-purpose TLS libraries Narrower, growing Confirm DTLS 1.3 in your backend
Regulated audit with PKI tooling Strong: standard TLS tooling Workable with x5t or full certificates Both support X.509
Sleeping node with changing IP Good with Connection ID Good: no transport binding OSCORE state is the sequence number, not the address

A few patterns follow from the matrix. If your fleet already terminates DTLS at a trusted gateway you operate, and the gateway is the only intermediary, DTLS 1.3 is simple and well understood, and the main gain from migrating is the shorter handshake and the Connection ID. If you outsource connectivity or deploy through shared infrastructure, the case for OSCORE is the threat model, not the byte count. And if your weakest device is a Cortex-M0, the footprint alone may decide.

Hybrid designs are valid. Run DTLS between the constrained network and a gateway for link protection, and OSCORE end to end for the application payload that must remain private across the gateway. The cost is double overhead on the constrained leg, so use it only when both properties are required.

Trade-offs, Gotchas, and What Goes Wrong

Both stacks fail in operational ways that the specifications warn about but implementations often forget.

Sequence-number reuse in OSCORE is catastrophic. With AES-CCM, reusing a nonce with the same key breaks both confidentiality and integrity. The nonce comes from the sender sequence number, so any rollback of persistent state, such as restoring a flash image after a firmware update, a backup restore or a cloned device, can reuse numbers. That is why the aiocoap documentation warns against copying contexts. The mitigation is to write the sequence number to non-volatile memory ahead of use, in blocks, so a crash can only skip numbers, never repeat them. The related limit is the 2^40 ceiling on the sequence number; at one message per second that lasts far longer than hardware does, but at high rates or with short-lived keys it needs a re-key plan, which is where EDHOC key update comes in.

Replay-window mismatch. The default window of 32 tolerates mild reordering. On a mesh with multipath delivery and aggressive retransmission, messages can arrive more than 32 positions late and be dropped as replays. Tune the window to your path, and remember that a larger window costs state.

Proxy surprises. The unprotected Block options let a proxy inject or reorder fragments, so cap message sizes and use end-to-end verification. Gateway-generated 5.04 errors arrive unauthenticated. Caches see no repeated cache keys, so a design that assumed proxy caching will send every request to the device.

DTLS handshake loss. Flights are retransmitted with exponential backoff starting at one second. On congested low-power networks the initial retransmission timer is slow, and a certificate flight spans many fragments. Servers should use the cookie exchange to resist spoofed floods, which costs a round trip; skipping it saves latency but exposes you to amplification.

Connection ID privacy. The CID is carried in the clear in every record, so it is a stable identifier an observer can use to follow a device across address changes. RFC 9147 addresses this partly through NewConnectionId messages that let endpoints rotate identifiers, but you must actually use them.

Credential and clock hygiene. Expired certificates on devices without a trusted clock are a classic outage. Raw public keys avoid it, at the price of building your own revocation story. Authentication failures per key must also be counted: DTLS 1.3 requires implementations to track AEAD limits and start a key update before reaching them.

No post-quantum protection. Both EDHOC and DTLS 1.3 as published depend on elliptic-curve Diffie-Hellman. A recorded session today could be decrypted by a future quantum adversary. For long-lived industrial data, watch IETF work on hybrid key exchange, and do not claim quantum resistance until your stack supports it. I did not verify the status of such work for EDHOC in this run.

Open implementation maturity. aiocoap states that its EDHOC support is partial and its credential format is changing. Do not treat a reference implementation as production code without testing against a second implementation for interoperability.

Practical Recommendations

Start from the threat model, not from the library you already have. If a party you do not control touches the traffic, or if you cannot afford a stateful session on the weakest device, plan for OSCORE with EDHOC. If both ends are yours, the path is short, and your gateway stack already supports DTLS 1.3 with Connection ID, then DTLS 1.3 is the lower-risk choice because of tooling maturity.

Choose credentials to fit the fleet: PSK only for closed, small fleets with a rotation procedure; raw public keys plus a registry when bytes matter and you control both ends; certificates referenced by hash when you need PKI semantics without paying the full certificate cost on every connect. Prefer the combined EDHOC and OSCORE request from RFC 9668 to save a round trip, with a sequential fallback.

Pilot on the weakest board you intend to ship, and measure flash, stack depth, handshake energy and time-to-first-reading under realistic loss. Published numbers, including those in this article, are from specific toolchains.

  • Write a one-page threat model naming every intermediary and who operates it.
  • Pick credential type per device class and document revocation for each.
  • Persist the OSCORE sender sequence number ahead of use and test power-loss and image-restore scenarios.
  • Enforce a policy that rejects unprotected requests on secured resources.
  • Cap unfragmented message size and test with a malicious proxy.
  • Use the DTLS cookie exchange and Connection ID rotation if you run DTLS.
  • Test interoperability against at least two implementations.
  • Plan for post-quantum migration and key rotation at the fleet level.

Frequently Asked Questions

What is OSCORE and how is it different from DTLS?

OSCORE (RFC 8613) is an object-security protocol that encrypts and authenticates the CoAP message itself, using an AEAD cipher with keys derived from a shared master secret. DTLS secures the whole datagram session between two transport endpoints. Because OSCORE protects the message, it works through proxies, over any transport and without a handshake of its own, while DTLS needs a stateful session that every intermediary must terminate. OSCORE is specific to CoAP, whereas DTLS protects any UDP protocol.

What does EDHOC do and does it replace DTLS?

EDHOC (RFC 9528) is a three-message authenticated Diffie-Hellman key exchange with forward secrecy and identity protection, designed so constrained devices can establish an OSCORE security context cheaply. It does not replace DTLS as a general protocol; it replaces the DTLS handshake in setups where you secure CoAP with OSCORE. With static Diffie-Hellman keys the messages total 101 bytes according to the RFC, far below a certificate-based DTLS 1.3 handshake.

Is DTLS 1.3 better than DTLS 1.2 for IoT?

Generally yes. DTLS 1.3 (RFC 9147) removes legacy cryptography, uses a compact unified record header, encrypts record sequence numbers, supports Connection ID natively and protects more of the handshake. It obsoletes DTLS 1.2. The practical constraint is library support: confirm that your embedded TLS backend implements DTLS 1.3 and Connection ID before planning a migration, because availability varies between libraries and versions.

Can OSCORE work with a CoAP proxy or cache?

Yes for forwarding, with limits. Proxies can route using the unprotected Uri-Host, Uri-Port or Proxy-Uri fields and handle outer Observe, Max-Age and Block options. They cannot read the path, query or payload, cannot forge verifiable responses and cannot start verifiable Observe registrations. Because the OSCORE option carries a changing Partial IV and is part of the cache key, shared caching by a standard proxy does not work for separate requests.

What is Group OSCORE and is it standardized?

Group OSCORE extends OSCORE to groups so a sender can protect a multicast request and recipients can verify which member sent it, using signatures plus an optional pairwise mode. It is specified in the Internet-Draft draft-ietf-core-oscore-groupcomm, whose latest revision I could confirm was version 28 from December 2025. I could not confirm RFC publication, so check the IETF datatracker. Implementations such as aiocoap exist, but require extra manual setup.

How many round trips does EDHOC with OSCORE need?

EDHOC itself has three messages, and the sequential flow in RFC 9668 takes three round trips to complete the key exchange and the first protected transaction. The combined flow places EDHOC message 3 and the first OSCORE request in one CoAP message using the EDHOC option (number 21), reaching the minimum of two round trips. It requires the client to be the Initiator and does not apply when message 4 is needed.

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 *