Explain how DTLS (Datagram TLS) provides encryption, authentication (PSK or certificates), and integrity protection for CoAP on port 5684
Distinguish between application-layer payload encryption and DTLS transport encryption, and justify why only DTLS satisfies HIPAA compliance for full metadata protection
Apply CoAP in real-world deployments including smart energy metering, building automation (HVAC/lighting), and industrial sensor networks
Select appropriate CoAP message types (CON vs NON) based on message criticality, battery constraints, and reliability requirements, and justify your choice
Diagnose common CoAP implementation issues including token matching failures, retransmission backoff misconfiguration, MTU size violations, and response code misinterpretation
Key Concepts
CoAP: Constrained Application Protocol — REST-style request/response protocol using UDP instead of TCP
Confirmable Message (CON): Requires ACK from recipient — provides reliable delivery over UDP at the cost of one roundtrip
Observe Option: CoAP extension enabling publish/subscribe: client registers to receive notifications on resource changes
Block-wise Transfer: Fragmentation mechanism for transferring payloads larger than a single CoAP datagram
Token: Client-generated value matching responses to requests — enables concurrent request/response pairing
DTLS: Datagram TLS — CoAP’s security layer providing encryption and authentication over UDP
50.2 For Beginners: CoAP Security
Securing CoAP means protecting the data that tiny IoT devices exchange. Since CoAP runs over UDP, it uses DTLS for encryption rather than the TLS used by web browsers. This chapter also covers real-world CoAP applications in smart buildings, industrial sensors, and wearable devices where security is essential.
Sensor Squad: Locking the Door
“Wait – if CoAP sends data over UDP, can’t anyone listen in?” Sammy the Sensor asked nervously. “My medical sensor readings are private!”
Max the Microcontroller nodded seriously. “That’s why we have DTLS – it’s like TLS (the lock on websites) but designed for UDP. It wraps your CoAP messages in encryption so nobody can read them in transit. Think of it as putting your letter in a locked box before mailing it.”
“But locking costs energy,” Bella the Battery pointed out. “The DTLS handshake – where devices exchange keys – takes several extra messages. That’s why you keep the connection open after the handshake so you don’t have to redo it every time. One handshake, then many encrypted messages.”
Lila the LED shared a real example: “In a smart hospital, CoAP with DTLS protects patient data from sensors to the nurse’s station. Without encryption, a hacker with a radio could intercept heart rate data. With DTLS, all they see is scrambled nonsense. Security isn’t optional when lives are at stake!”
In 60 Seconds
CoAP uses DTLS (Datagram TLS) for security over UDP, providing encryption, authentication (via pre-shared keys or certificates), and integrity protection on port 5684. Unlike application-layer encryption which only protects the payload, DTLS encrypts the entire CoAP message including headers, URIs, and options – critical for compliance scenarios like healthcare (HIPAA) where metadata exposure is itself a privacy violation.
50.3 Security with DTLS
CoAP uses DTLS (Datagram TLS) for security:
Features:
Encryption of messages
Authentication (Pre-Shared Keys or Certificates)
Integrity protection
DTLS vs TLS:
TLS = for TCP (HTTP)
DTLS = for UDP (CoAP)
Port:
CoAP: 5683
CoAPs (secure): 5684
Try It: DTLS Handshake Step Explorer
Explore each step of the DTLS handshake process. Adjust the security mode and network latency to see how the handshake timing and energy cost change for constrained IoT devices.
Setting up secure CoAP (CoAPs) with DTLS-PSK authentication using Python:
# pip install aiocoap[dtls]# Requires: tinydtls library (pip install DTLSSocket)import asyncioimport aiocoapimport aiocoap.credentials# --- Server side: CoAPs on port 5684 ---asyncdef create_secure_server():"""Start a CoAP server with DTLS-PSK on port 5684.""" root = aiocoap.resource.Site() root.add_resource(['temperature'], TemperatureResource())# Define server credentials: map client identities to PSKs server_credentials = aiocoap.credentials.CredentialsMap() server_credentials.load_from_dict({# Identity "sensor-042" uses this 16-byte pre-shared key':dtls-psk': {'sensor-042': b'MySecret16ByteK!','sensor-043': b'AnotherKey16Byt!', } }) context =await aiocoap.Context.create_server_context( root, bind=('::', 5684), # Secure CoAP port ) context.client_credentials = server_credentialsreturn context# --- Client side: connect with PSK identity ---asyncdef secure_get_temperature():"""GET coaps://server/temperature with DTLS-PSK.""" context =await aiocoap.Context.create_client_context()# Set client identity and PSK context.client_credentials.load_from_dict({'coaps://server/*': {'dtls': {'psk': b'MySecret16ByteK!','client-identity': b'sensor-042' } } }) request = aiocoap.Message( code=aiocoap.GET, uri='coaps://server/temperature'# Note: coaps:// not coap:// ) response =await context.request(request).responseprint(f"Secure response: {response.payload.decode()}")# What to observe:# - URI scheme is coaps:// (port 5684) vs coap:// (port 5683)# - DTLS handshake adds ~200ms on first request (key exchange)# - Subsequent requests reuse session (~5ms overhead)# - If PSK doesn't match: handshake fails, no data exchanged
Testing DTLS with CLI tools:
# Generate a PSK identity file for coap-client# coap-client supports DTLS-PSK natively# Secure GET requestcoap-client-m get coaps://localhost/temperature \-k"MySecret16ByteK!"\-u"sensor-042"# Verify encryption with Wireshark:# Filter: dtls && udp.port == 5684# You should see:# ClientHello -> ServerHello -> ChangeCipherSpec# Then encrypted ApplicationData (CoAP payload not readable)
DTLS overhead comparison:
Plain CoAP (UDP port 5683):
Request: 4-byte header + payload
Response: 4-byte header + payload
Latency: 1 RTT (~20ms LAN)
Secure CoAP (DTLS port 5684):
First request: DTLS handshake (4-6 messages) + encrypted payload
Latency: 3 RTTs for handshake + 1 RTT for data = ~80ms first request
Overhead: 13 bytes DTLS record header per message
Subsequent requests (session reuse):
Latency: 1 RTT + ~5ms DTLS overhead = ~25ms
Overhead: 13 bytes header + 8-16 bytes MAC (integrity)
Energy cost of DTLS handshake (one-time per session):
6 messages x 100 bytes avg x 10 mA TX = 0.6 mAs
Amortized over 1000 messages: 0.0006 mAs each (negligible)
Keep DTLS sessions alive to avoid repeated handshakes.
Putting Numbers to It
DTLS security has real cost, but it’s manageable with session reuse. Let’s quantify:
DTLS handshake energy (PSK mode): - ClientHello: 120 bytes @ 20 mA TX for 4.8 ms = 0.027 mAh - ServerHello + ChangeCipherSpec: 140 bytes RX = 0.024 mAh - Client Finished: 60 bytes TX = 0.013 mAh - Total handshake: 0.064 mAh (one-time cost)
Select a real-world IoT application domain and configure CoAP parameters. The tool recommends optimal message types, security modes, and observe patterns based on your deployment requirements.
coap_app_scenario_card = {const freq_map = {"Every 1 second":1,"Every 10 seconds":10,"Every 30 seconds":30,"Every 1 minute":60,"Every 5 minutes":300,"Every 15 minutes":900,"Every 1 hour":3600 };const interval_s = freq_map[report_freq];const msgs_per_day =Math.floor(86400/ interval_s);const total_msgs_day = msgs_per_day * num_devices;const domains = {"Smart Energy Metering": {resources: ["coap://meter/consumption","coap://meter/tariff","coap://meter/switch","coap://meter/demand-response"],msg_type:"CON",reason:"Billing data must be reliably delivered -- lost readings affect revenue",security:"DTLS-PSK",observe:true,observe_reason:"Demand-response events push tariff changes to meters instantly",payload_bytes:48,icon:"zap" },"Building Automation (HVAC)": {resources: ["coap://hvac/temperature","coap://hvac/setpoint","coap://hvac/mode","coap://lighting/brightness"],msg_type:"NON for readings, CON for commands",reason:"Temperature readings can tolerate loss; setpoint changes must be confirmed",security:"DTLS-PSK",observe:true,observe_reason:"Thermostat subscribes to schedule changes from building controller",payload_bytes:32,icon:"thermometer" },"Industrial Sensor Network": {resources: ["coap://sensor/vibration","coap://sensor/temperature","coap://sensor/config","coap://sensor/firmware"],msg_type:"NON for telemetry, CON for config/firmware",reason:"High-frequency vibration data uses NON; firmware blocks require CON with Block2",security:"DTLS-Certificate",observe:true,observe_reason:"Predictive maintenance system observes vibration thresholds for alerts",payload_bytes:64,icon:"gear" },"Healthcare Wearables": {resources: ["coaps://device/heartrate","coaps://device/spo2","coaps://device/temperature","coaps://device/alert"],msg_type:"CON",reason:"All patient vital signs must be reliably delivered -- HIPAA compliance requires audit trail",security:"DTLS-Certificate",observe:true,observe_reason:"Nurse station observes patient vitals for real-time dashboard",payload_bytes:56,icon:"heart" },"Smart Agriculture": {resources: ["coap://sensor/soil-moisture","coap://sensor/temperature","coap://actuator/valve","coap://sensor/light"],msg_type:"NON",reason:"Battery conservation is critical; losing occasional soil readings is acceptable",security:"OSCORE",observe:false,observe_reason:"Devices deep-sleep between readings -- observe not practical with sleep cycles",payload_bytes:24,icon:"seedling" },"Asset Tracking": {resources: ["coap://tracker/location","coap://tracker/status","coap://tracker/geofence","coap://tracker/battery"],msg_type:"NON for location, CON for geofence alerts",reason:"Periodic location updates use NON; geofence breach alerts must be confirmed",security:"DTLS-PSK",observe:false,observe_reason:"Trackers are mobile -- persistent observe connections impractical over cellular",payload_bytes:40,icon:"pin" } };const d = domains[app_domain];const energy_per_msg = d.security==="OSCORE"?1.8: d.security.includes("Certificate") ?2.1:1.7;const daily_energy_mj = msgs_per_day * energy_per_msg;const battery_days = (220*3*3600) / (daily_energy_mj);const enc_level = needs_encryption.startsWith("Yes - Regulatory") ?"mandatory": needs_encryption.startsWith("Yes - Best") ?"recommended":"optional";const rec_security = enc_level ==="mandatory"?"DTLS-Certificate": enc_level ==="recommended"? d.security:"No Security";let resources_html = d.resources.map(r => {const method = r.includes("switch") || r.includes("setpoint") || r.includes("valve") || r.includes("brightness") || r.includes("mode") || r.includes("config")?"GET / PUT": r.includes("firmware") ?"GET (Block2)": r.includes("alert") || r.includes("geofence") ?"POST":"GET";return`<div style="padding: 10px 0; border-bottom: 1px solid #ecf0f1;"> <div style="font-family: monospace; color: #16A085; font-size: 0.85em; overflow-wrap: anywhere;">${r}</div> <div style="margin-top: 4px; color: #3498DB; font-weight: 600;">Methods: ${method}</div> </div>`; }).join("");returnhtml`<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #16A085; margin: 20px 0;"> <h4 style="color: #2C3E50; margin-top: 0;">${app_domain} -- CoAP Deployment Plan</h4> <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 15px;"> <div style="background: #fff; padding: 12px; border-radius: 6px; border: 1px solid #ecf0f1;"> <strong style="color: #2C3E50;">Devices:</strong><br> <span style="font-size: 1.4em; color: #3498DB;">${num_devices.toLocaleString()}</span> </div> <div style="background: #fff; padding: 12px; border-radius: 6px; border: 1px solid #ecf0f1;"> <strong style="color: #2C3E50;">Msgs/Device/Day:</strong><br> <span style="font-size: 1.4em; color: #E67E22;">${msgs_per_day.toLocaleString()}</span> </div> <div style="background: #fff; padding: 12px; border-radius: 6px; border: 1px solid #ecf0f1;"> <strong style="color: #2C3E50;">Total Msgs/Day:</strong><br> <span style="font-size: 1.4em; color: #9B59B6;">${total_msgs_day.toLocaleString()}</span> </div> </div> <div style="background: #fff; padding: 15px; border-radius: 6px; border: 1px solid #ecf0f1; margin-bottom: 15px;"> <strong style="color: #2C3E50;">CoAP Resource Endpoints:</strong> <div style="margin-top: 8px;">${resources_html}</div> </div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 15px;"> <div style="background: #fff; padding: 12px; border-radius: 6px; border: 1px solid #ecf0f1;"> <strong style="color: #2C3E50;">Message Type:</strong><br> <span style="color: #16A085; font-weight: bold;">${d.msg_type}</span><br> <small style="color: #7F8C8D;">${d.reason}</small> </div> <div style="background: #fff; padding: 12px; border-radius: 6px; border: 1px solid #ecf0f1;"> <strong style="color: #2C3E50;">Security:</strong><br> <span style="color: #E74C3C; font-weight: bold;">${rec_security}</span><br> <small style="color: #7F8C8D;">${enc_level ==="mandatory"?"Regulatory compliance requires certificates": enc_level ==="recommended"?"Best practice for production":"Lab/testing only -- not for production"}</small> </div> </div> <div style="background: ${d.observe?'#d4edda':'#fff3cd'}; padding: 12px; border-radius: 6px; margin-bottom: 15px;"> <strong style="color: #2C3E50;">Observe Pattern: ${d.observe?"Recommended":"Not Recommended"}</strong><br> <small style="color: #7F8C8D;">${d.observe_reason}</small> </div> <div style="background: #fff; padding: 12px; border-radius: 6px; border: 1px solid #ecf0f1;"> <strong style="color: #2C3E50;">Per-Device Battery Estimate (CR2032, 220 mAh):</strong><br> <span style="font-size: 1.3em; color: ${battery_days >365?'#16A085': battery_days >90?'#E67E22':'#E74C3C'}; font-weight: bold;">${battery_days.toFixed(0)} days (${(battery_days /365).toFixed(1)} years) </span> <small style="color: #7F8C8D;"> at ${d.payload_bytes} bytes/msg, ${energy_per_msg} mJ/msg with ${rec_security}</small> </div> </div>`;}
50.7 Lab Exercise
Goal: Build CoAP temperature monitoring system
Hardware (or simulate):
ESP32 board
DHT22 sensor
Tasks:
Setup CoAP Server (on Raspberry Pi or PC):
Install aiocoap
Create temperature resource
Add humidity resource
Implement GET and PUT methods
Create ESP32 CoAP Client:
Connect to Wi-Fi
Send GET requests every 30s
Display responses on Serial Monitor
Add Observe Pattern:
Server notifies on temperature change
Client receives push updates
No polling needed
Implement Multicast Discovery:
Client broadcasts to find servers
Servers respond with capabilities
Challenge: Add CoAP-to-MQTT bridge for cloud integration
50.8 When to Use CoAP
50.8.1 Choose CoAP when:
Constrained devices (8-bit MCU, limited RAM)
Low power critical (battery-operated)
Request/Response pattern fits your needs
RESTful API desired
Multicast needed for device discovery
IPv6 networks (6LoWPAN)
Direct device-to-device communication
50.8.2 Choose HTTP when:
Web browser access needed
Infrastructure already HTTP-based
Devices not constrained
Complex authentication required
50.8.3 Choose MQTT when:
Publish/Subscribe pattern needed
Central broker acceptable
Many-to-many communication
QoS levels important
Topic-based routing preferred
50.9 Common Implementation Pitfalls
Pitfall: Forgetting to Handle CoAP Token Matching in Observe Responses
The Mistake: Implementing CoAP Observe without properly tracking tokens, causing clients to misattribute notifications to the wrong resources when observing multiple endpoints simultaneously.
Why It Happens: Developers familiar with HTTP assume response correlation is automatic. In CoAP, the token (0-8 bytes) links requests to responses. When observing /temperature and /humidity concurrently, both notification streams arrive on the same UDP socket and must be matched by token.
The Fix: Maintain a token-to-resource mapping for all active observations:
Use unique tokens per observation (e.g., 4-byte random values) and clean up mappings when observations are cancelled or reset.
Pitfall: CoAP Retransmission Backoff Flooding the Network
The Mistake: Using aggressive fixed-interval retransmissions for CON messages instead of exponential backoff, causing network congestion when packet loss occurs.
Why It Happens: Developers implement simple “retry every 2 seconds” logic without realizing that CoAP specifies exponential backoff (RFC 7252). On a lossy wireless network with 20% packet loss, aggressive retries amplify traffic by 5x and worsen congestion.
The Fix: Implement RFC 7252 compliant retransmission with exponential backoff:
# BAD: Fixed interval retry (network flooding)RETRY_INTERVAL =2.0# secondsfor attempt inrange(MAX_RETRIES): send_message(msg)if wait_for_ack(RETRY_INTERVAL):break# GOOD: Exponential backoff per RFC 7252ACK_TIMEOUT =2.0# Initial timeout (2 seconds)ACK_RANDOM_FACTOR =1.5# Randomization factorMAX_RETRANSMIT =4# Maximum retriesdef coap_transmit(msg): timeout = ACK_TIMEOUT * (1+ random.random() * (ACK_RANDOM_FACTOR -1))for attempt inrange(MAX_RETRANSMIT +1): send_message(msg)if wait_for_ack(timeout):returnTrue timeout *=2# Double timeout each retry: 2s, 4s, 8s, 16sreturnFalse# Give up after ~45 seconds total
With proper backoff: first retry at 2-3s, second at 4-6s, third at 8-12s, fourth at 16-24s. Total worst-case wait: ~45 seconds before declaring failure.
50.10 CoAP Implementation Patterns
Understanding CoAP implementation through message exchanges, resource patterns, and performance characteristics:
C/C++: libcoap via system package with docs at libcoap.net
Testing and Debugging Tools:
coap-client (libcoap): Command-line CoAP client for testing
# GET requestcoap-client-m get coap://localhost/temperature# POST with payloadcoap-client-m post coap://localhost/sensor -e"22.5"# Observe resourcecoap-client-m get -s 60 coap://localhost/temperature
Copper (Cu): Firefox/Chrome plugin for CoAP browsing (deprecated but useful for learning)
Wireshark: CoAP dissector included (filter: coap)
nRF Connect CoAP: Mobile app for testing CoAP servers
Example Implementation Patterns:
Pattern 1: Sensor Reading (NON message)
Sensor → Gateway: CON GET /temperature
Gateway → Sensor: ACK 2.05 Content
Payload: 22.5°C
Overhead: 2 messages, ~50 bytes total
Latency: 1 RTT (~20-50ms on local network)
Reliability: Guaranteed (CON requires ACK)
Pattern 2: Frequent Updates (Observe)
Client → Server: CON GET /temperature, Observe: 0
Server → Client: ACK 2.05 Content, Observe: 12
Initial value: 22.5°C
[30 seconds later]
Server → Client: CON 2.05 Content, Observe: 13
Updated value: 23.1°C
[30 seconds later]
Server → Client: CON 2.05 Content, Observe: 14
Updated value: 22.9°C
Overhead: 1 subscribe + N notifications
Battery savings: Avoid polling every 30s
Pattern 3: Multicast Discovery
Client → FF02::FD: NON GET /.well-known/core
Device1 → Client: NON 2.05 Content
</temperature>,</humidity>
Device2 → Client: NON 2.05 Content
</pressure>,</light>
Use case: Discover all CoAP devices on local network
Result: List of available resources from all devices
50.10.2 CoAP Response Code Categories
Success (2.xx): 2.01-2.05, meaning the request succeeded. Examples: 2.05 Content for a successful GET and 2.04 Changed for a successful PUT.
Client Error (4.xx): 4.00-4.15, meaning the client made an error. Examples: 4.04 Not Found for a bad URI and 4.01 Unauthorized when authentication is required.
Server Error (5.xx): 5.00-5.05, meaning the server failed. Examples: 5.00 Internal Server Error and 5.03 Service Unavailable.
Try It: CoAP Response Code Reference
Look up CoAP response codes interactively. Select a category or type a code to see its meaning, HTTP equivalent, and when you would encounter it in practice.
Show code
viewof resp_category = Inputs.select( ["All Codes","2.xx Success","4.xx Client Error","5.xx Server Error"], {value:"All Codes",label:"Filter by Category" })viewof resp_search = Inputs.text({label:"Search by code or keyword",placeholder:"e.g. 4.04 or 'not found'",value:""})
Show code
coap_resp_code_display_card = {const codes = [ { code:"2.01",name:"Created",http:"201 Created",cat:"2.xx Success",color:"#16A085",desc:"Resource successfully created via POST. Response includes location of new resource.",example:"POST /sensors creates a new sensor, server responds 2.01 with Location-Path: /sensors/42" }, { code:"2.02",name:"Deleted",http:"204 No Content",cat:"2.xx Success",color:"#16A085",desc:"Resource successfully deleted via DELETE request.",example:"DELETE /sensors/42 removes the sensor resource" }, { code:"2.03",name:"Valid",http:"304 Not Modified",cat:"2.xx Success",color:"#16A085",desc:"ETag validation: resource has not changed since last retrieval. Saves bandwidth.",example:"GET /config with If-Match ETag returns 2.03 if config unchanged" }, { code:"2.04",name:"Changed",http:"204 No Content",cat:"2.xx Success",color:"#16A085",desc:"Resource successfully updated via PUT or POST.",example:"PUT /thermostat/setpoint with payload '22.5' updates the temperature target" }, { code:"2.05",name:"Content",http:"200 OK",cat:"2.xx Success",color:"#16A085",desc:"GET request succeeded. Payload contains the requested resource representation.",example:"GET /temperature returns 2.05 with payload '23.4' in application/json" }, { code:"4.00",name:"Bad Request",http:"400 Bad Request",cat:"4.xx Client Error",color:"#E67E22",desc:"Malformed request, invalid options, or unparseable payload.",example:"Sending a PUT with invalid JSON payload to /config" }, { code:"4.01",name:"Unauthorized",http:"401 Unauthorized",cat:"4.xx Client Error",color:"#E67E22",desc:"Client not authenticated. DTLS required but not established.",example:"Accessing coaps:// resource without DTLS session" }, { code:"4.03",name:"Forbidden",http:"403 Forbidden",cat:"4.xx Client Error",color:"#E67E22",desc:"Client authenticated but lacks permission for this resource.",example:"Sensor device trying to PUT /admin/config (read-only access)" }, { code:"4.04",name:"Not Found",http:"404 Not Found",cat:"4.xx Client Error",color:"#E67E22",desc:"Resource does not exist at the given URI path.",example:"GET /sensors/999 when sensor 999 has not been registered" }, { code:"4.05",name:"Method Not Allowed",http:"405 Method Not Allowed",cat:"4.xx Client Error",color:"#E67E22",desc:"The HTTP method is not supported on this resource.",example:"DELETE /temperature -- read-only sensor resource does not support DELETE" }, { code:"4.08",name:"Request Entity Incomplete",http:"408 (approx)",cat:"4.xx Client Error",color:"#E67E22",desc:"Block-wise transfer incomplete. Server missing blocks.",example:"Block1 upload failed: blocks 0-3 received but block 4 missing" }, { code:"4.13",name:"Request Entity Too Large",http:"413 Payload Too Large",cat:"4.xx Client Error",color:"#E67E22",desc:"Payload exceeds server capacity. Use block-wise transfer.",example:"Firmware upload of 50 KB without Block1 option on constrained device" }, { code:"4.15",name:"Unsupported Content-Format",http:"415 Unsupported Media Type",cat:"4.xx Client Error",color:"#E67E22",desc:"Server does not understand the payload content format.",example:"Sending XML (content-format 41) to server that only accepts CBOR (60)" }, { code:"5.00",name:"Internal Server Error",http:"500 Internal Server Error",cat:"5.xx Server Error",color:"#E74C3C",desc:"Server encountered an unexpected condition.",example:"Sensor hardware failure while reading ADC for temperature request" }, { code:"5.01",name:"Not Implemented",http:"501 Not Implemented",cat:"5.xx Server Error",color:"#E74C3C",desc:"Server does not support the functionality required.",example:"Server receives PATCH method it has not implemented" }, { code:"5.03",name:"Service Unavailable",http:"503 Service Unavailable",cat:"5.xx Server Error",color:"#E74C3C",desc:"Server temporarily unable to handle request. Includes Max-Age for retry hint.",example:"Gateway overloaded during peak telemetry period, returns retry-after 30s" }, { code:"5.04",name:"Gateway Timeout",http:"504 Gateway Timeout",cat:"5.xx Server Error",color:"#E74C3C",desc:"Proxy/gateway did not receive response from upstream server in time.",example:"CoAP-HTTP proxy waiting for cloud server that is unreachable" } ];const search_lower = resp_search.toLowerCase();const filtered = codes.filter(c => {const cat_match = resp_category ==="All Codes"|| c.cat=== resp_category;const search_match = search_lower ===""|| c.code.includes(search_lower) || c.name.toLowerCase().includes(search_lower) || c.desc.toLowerCase().includes(search_lower);return cat_match && search_match; });let rows = filtered.map(c =>`<div style="padding: 12px; border: 1px solid #ecf0f1; border-radius: 8px; background: #fff; margin-bottom: 10px;"> <div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;"> <span style="font-weight: bold; font-size: 1.05em; color: ${c.color};">${c.code}</span> <strong style="color: #2C3E50;">${c.name}</strong> <span style="color: #7F8C8D; font-size: 0.85em;">${c.http}</span> </div> <div style="margin-top: 8px; color: #2C3E50; font-size: 0.9em;">${c.desc}</div> <div style="margin-top: 6px; color: #9B59B6; font-size: 0.82em;"><em>Example: ${c.example}</em></div> </div>`).join("");returnhtml`<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #E67E22; margin: 20px 0;"> <h4 style="color: #2C3E50; margin-top: 0;">CoAP Response Codes (${filtered.length} of ${codes.length} shown)</h4> <div>${rows}</div>${filtered.length===0?`<div style="padding: 20px; text-align: center; color: #7F8C8D;"><em>No matching response codes found. Try a different search term.</em></div>`:''} <div style="margin-top: 15px; padding: 10px; background: #fff; border-radius: 6px; border: 1px solid #ecf0f1;"> <small style="color: #7F8C8D;"><strong>Tip:</strong> CoAP response codes use a compact format (class.detail) where class 2=Success, 4=Client Error, 5=Server Error. This maps directly to HTTP status codes but uses only 1 byte instead of 3 ASCII characters.</small> </div> </div>`;}
Constrained Devices and CoAP - IETF Educational Series
50.11 Troubleshooting Common Issues
Common Problems and Solutions
Request times out: UDP packet loss or no ACK. Increase retransmission timeout, use CON messages instead of NON, and check network quality.
Confirmable message never acknowledged: Server down or wrong endpoint. Verify the server is running and confirm the CoAP URI format coap://host:port/path.
Block transfer fails mid-stream: MTU too large or packet fragmentation. Reduce block size from 1024 bytes to 512 or 256 and check network MTU settings.
Observe notifications stop: Server crashed or network partition. Re-establish the observe relationship and add client-side timeout detection.
Multicast discovery finds no devices: Wrong multicast address or routing. Use FF02::FD for link-local or FF05::FD for site-local and check IPv6 multicast routing.
DTLS handshake fails: PSK mismatch or certificate error. Verify pre-shared keys match exactly and check certificate validity for certificate mode.
Response code 4.04 Not Found: Wrong resource path. Check URI case sensitivity and verify the resource exists on the server.
Response code 4.05 Method Not Allowed: Unsupported method on the resource. Confirm that the resource supports GET, POST, PUT, or DELETE as expected.
Debug Checklist:
Connection and Discovery Issues:
Request/Response Problems:
Observe and Block Transfer Issues:
Performance and Reliability:
Security (DTLS) Issues:
Common Error Codes:
2.01 Created: Resource successfully created (POST response)
2.02 Deleted: Resource successfully deleted
2.03 Valid: Resource still valid (cache validation)
2.05 Content: Resource content returned (GET response)
4.00 Bad Request: Malformed request or invalid options
4.01 Unauthorized: Authentication required or failed
4.04 Not Found: Resource does not exist
4.05 Method Not Allowed: HTTP method not supported on resource
5.00 Internal Server Error: Server encountered error processing request
5.03 Service Unavailable: Server temporarily unable to handle request
Tools for Debugging:
libcoap tools: coap-client and coap-server for testing
Copper (Cu): Firefox/Chrome plugin for CoAP browsing (deprecated but useful)
Wireshark: CoAP dissector for packet analysis (filter: coap)
tcpdump: Capture UDP packets (tcpdump -i any port 5683 -vv)
Eclipse Californium: Java-based CoAP library with extensive logging
aiocoap: Python library with good debugging output
50.12 Common Pitfalls
Common Pitfall: CoAP Message Size Exceeds MTU
The mistake: Sending CoAP payloads larger than the network MTU (typically 1280 bytes for IPv6, often much smaller for constrained networks like 6LoWPAN with 127-byte frames), causing silent packet drops or fragmentation failures.
Symptoms:
Large GET responses never arrive at the client
PUT/POST requests with substantial payloads fail intermittently
Works on local Wi-Fi but fails over 6LoWPAN or constrained networks
Wireshark shows fragmented packets but no reassembled response
Why it happens: CoAP runs over UDP, which doesn’t handle fragmentation gracefully: - IPv6 minimum MTU: 1280 bytes, but CoAP payload should be much smaller - 6LoWPAN frame: 127 bytes maximum, ~80 bytes after headers - UDP fragmentation: If any fragment is lost, entire message is lost - No automatic retransmission of individual fragments
The fix:
# Use CoAP Block-wise Transfer (RFC 7959) for large payloadsfrom aiocoap import Message, Contextfrom aiocoap.numbers.codes import GETasyncdef get_large_resource(uri): context =await Context.create_client_context()# Request with Block2 option - library handles chunking request = Message(code=GET, uri=uri)# Block size: 64 bytes (szx=2), 128 bytes (szx=3), 256 bytes (szx=4)# Smaller blocks = more round trips but works on constrained networks response =await context.request(request).responsereturn response.payload # Library reassembles blocks automatically
// Embedded C: Check payload size before sending#define COAP_MAX_PAYLOAD_6LOWPAN 64// Safe for 802.15.4#define COAP_MAX_PAYLOAD_UDP 1024// Safe for most networkssize_t max_payload = is_constrained_network()? COAP_MAX_PAYLOAD_6LOWPAN : COAP_MAX_PAYLOAD_UDP;if(payload_len > max_payload){// Use Block1 (request) or Block2 (response) transferreturn coap_send_blockwise(payload, payload_len, max_payload);}
Prevention: Design payloads to fit in 64-128 bytes for 6LoWPAN networks. Use Block-wise Transfer for firmware updates, large configurations, or file downloads. Test on the actual constrained network, not just Wi-Fi.
Common Pitfall: Misusing CON vs NON Message Types
The mistake: Using Confirmable (CON) messages for all communications because “reliability is important,” leading to excessive battery drain and network congestion, or using Non-Confirmable (NON) for critical commands where delivery must be guaranteed.
Symptoms:
Battery-powered sensors lasting weeks instead of years
Unnecessary retransmission storms when network is lossy
Commands occasionally not reaching actuators (lights, locks)
High latency due to waiting for ACKs on every message
Why it happens: Developers often misunderstand the trade-offs: - CON overuse: HTTP background makes developers expect reliability for everything - NON overuse: Trying to maximize battery life without considering message importance - No hybrid strategy: Treating all messages the same regardless of criticality
The fix:
from aiocoap import Message, Contextfrom aiocoap.numbers.codes import GET, PUTfrom aiocoap.numbers.types import CON, NON# NON for periodic telemetry (loss acceptable, battery critical)asyncdef send_temperature_reading(temp): request = Message( code=PUT, mtype=NON, # Fire-and-forget, ~5x better battery life uri='coap://server/sensors/temp', payload=f'{temp}'.encode() )await context.request(request).response# CON for critical commands (must know if it worked)asyncdef unlock_door(): request = Message( code=PUT, mtype=CON, # Need acknowledgment for security uri='coap://door/lock', payload=b'unlock' )try: response =await context.request(request).responsereturn response.code.is_successful()exceptException:returnFalse# Command failed, alert user# CON for configuration changes (must be applied)asyncdef update_reporting_interval(seconds): request = Message( code=PUT, mtype=CON, # Config must be confirmed uri='coap://sensor/config/interval', payload=str(seconds).encode() )returnawait context.request(request).response
Decision matrix: - Periodic sensor readings: Use NON for routine telemetry; do not use CON. - Alert or alarm notifications: Use CON so the receiver must acknowledge delivery. - Device commands (on/off): Use CON because actuation commands need confirmation. - Configuration updates: Use CON so applied settings are confirmed. - Status queries: Depends on how critical the response is. - Firmware chunks: Use CON with Block2 acknowledgements.
Prevention: Default to NON for periodic telemetry. Use CON only for commands, configurations, and alerts. Implement a message priority system that selects type based on criticality.
Try It: CON vs NON Message Type Advisor
Describe your IoT scenario and this tool recommends whether to use Confirmable (CON) or Non-confirmable (NON) messages, with energy and reliability analysis.
Show code
viewof msg_criticality = Inputs.select( ["Periodic telemetry (temperature, humidity)","Safety alert (smoke, gas leak)","Actuator command (lock, valve, switch)","Configuration update (sampling rate, threshold)","Firmware update chunk","Status query (battery level, uptime)"], {value:"Periodic telemetry (temperature, humidity)",label:"Message Purpose" })viewof msg_loss_tolerance = Inputs.range([0,50], {value:5,step:1,label:"Acceptable Loss Rate (%)"})viewof msg_network_loss = Inputs.range([0,40], {value:10,step:1,label:"Estimated Network Loss Rate (%)"})viewof msg_battery_priority = Inputs.radio(["Battery life is critical","Balanced","Reliability is critical"], {value:"Balanced",label:"Priority"})
Worked Example: Hospital Wearable Device DTLS Configuration
Scenario: A hospital deploys 200 wireless vital signs monitors (heart rate, SpO2, temperature) on patients. Each device uses CoAP to report readings every 30 seconds to a centralized gateway. HIPAA compliance requires encryption of all patient data in transit.
Comparing security options:
Option A: Application-layer encryption (AES-128 on payload):
Choose the right security configuration based on deployment requirements:
Factor
No Security (CoAP)
DTLS-PSK (Pre-Shared Key)
DTLS-Cert (Certificates)
OSCORE (Object Security)
Encryption strength
None
AES-128
AES-128/256
AES-128
Authentication
None
Symmetric key
Asymmetric (PKI)
Symmetric key
Handshake overhead
0 bytes
360 bytes (one-time)
2-4 KB (one-time)
0 bytes (pre-provisioned)
Per-message overhead
0 bytes
+29 bytes (header+MAC)
+29 bytes
+8-16 bytes
Key distribution
N/A
Manual or secure channel
PKI infrastructure
Pre-provisioned
End-to-end security
No
No (broker can decrypt)
No (broker can decrypt)
Yes (survives proxies)
NAT traversal
Easy (UDP)
Easy (UDP)
Easy (UDP)
Easy (UDP)
Best for
Lab testing only
IoT devices, constrained networks
Enterprise, device identity critical
Proxy/gateway networks
Decision tree:
Is data sensitive or regulated (PII, HIPAA, financial)? → No: Consider plain CoAP (but use encryption anyway as best practice) → Yes: Continue
Do you have PKI infrastructure (CA, certificate management)? → Yes: DTLS with Certificates (strong identity verification) → No: Continue
Can you securely pre-provision keys to devices during manufacturing? → Yes: DTLS-PSK or OSCORE (both use symmetric keys) → No: You need to set up key distribution mechanism first
Do messages pass through untrusted proxies or gateways? → Yes: OSCORE (end-to-end, proxy can’t decrypt) → No: DTLS-PSK (simpler, transport-layer security)
Is per-message overhead critical (<10 bytes headroom)? → Yes: OSCORE (8-16 bytes vs DTLS’s 29 bytes) → No: DTLS-PSK (easier to debug, standard TLS tools work)
Hybrid approach: Many deployments use DTLS from device to gateway (transport security), then OSCORE from gateway to cloud (end-to-end security). This balances ease of debugging (DTLS is standard TLS) with proxy security (OSCORE protects against compromised gateways).
Common Mistake: Reusing DTLS Session Across Deep Sleep Cycles
The Error: Configuring battery-powered IoT devices to establish a DTLS session once, then sleep/wake multiple times expecting the session to remain valid.
Why It Happens: Developers assume DTLS sessions persist like HTTP cookies. The device wakes, sends an encrypted CoAP message, and expects the server to accept it. However, DTLS sessions have state (sequence numbers, cipher state) that’s lost when the device power-cycles RAM.
Real-World Impact: A smart agriculture deployment of 500 soil moisture sensors (ESP32, deep sleep mode):
Attempted implementation (broken):
// WRONG: Trying to reuse DTLS session after deep sleepvoid setup(){ dtls_session = dtls_new_session(); dtls_connect(dtls_session, SERVER_IP,5684);// Handshake: 360 bytes, 45 mJ}void loop(){float moisture = read_sensor(); dtls_send(dtls_session, coap_message);// Sends with stale sequence number esp_deep_sleep(3600*1000000);// Sleep 1 hour (RAM lost!)}// After waking, `dtls_session` pointer is invalid, sequence numbers reset// Server rejects messages with "Bad MAC" or "Decrypt error"
Symptoms:
95% of messages after first wake rejected by server
Logs show: DTLS decrypt error: sequence number mismatch
Devices retry handshake, draining battery
Battery life: 3 months (expected: 18 months)
Root cause: DTLS maintains per-session state in RAM: - Cipher context: Encryption keys derived from handshake - Sequence numbers: Anti-replay protection (both sides increment per message) - Epoch: Changes on renegotiation
When ESP32 deep sleeps, RAM is powered off. On wake, all session state is lost. The device can’t resume the DTLS session.
The Fix (Option 2): OSCORE (session-less security):
// OSCORE: Pre-provisioned keys, no handshake neededvoid loop(){float moisture = read_sensor();// Encrypt with OSCORE (uses pre-shared master secret) coap_message = oscore_encrypt(moisture, MASTER_SECRET, sequence_number++); coap_send(coap_message, SERVER_IP,5683);// Plain CoAP port, encrypted payload esp_deep_sleep(3600*1000000);// Sleep 1 hour}// Battery cost: 1.8 mJ × 24/day = 43 mJ/day (no handshake!)// Battery life: ~18 months (26× improvement vs full DTLS handshake every wake)
The Fix (Option 3): DTLS session resumption with stored state:
// Store DTLS session state in RTC memory (survives deep sleep on ESP32)RTC_DATA_ATTR uint8_t session_state[256];// RTC_DATA_ATTR = retained during deep sleepvoid loop(){dtls_session_t*session;if(is_first_boot()){// Full handshake on first boot only session = dtls_new_session(); dtls_connect(session, SERVER_IP,5684); dtls_save_session(session, session_state);// Persist to RTC memory}else{// Resume from RTC memory on subsequent wakes session = dtls_restore_session(session_state);}float moisture = read_sensor(); dtls_send(session, coap_message); dtls_save_session(session, session_state);// Update sequence numbers esp_deep_sleep(3600*1000000);}// Battery cost: 45 mJ (first boot) + 1.8 mJ × 24/day = ~43 mJ/day amortized// Battery life: ~18 months (resumption avoids handshake overhead)
Decision matrix:
Handshake every wake: Handshake on every message (about 24/day), ~45 mJ per message, about 6 months of battery life, not recommended.
OSCORE: No handshake after provisioning, ~1.8 mJ per message, about 18 months of battery life, best for deep-sleep devices.
Session resumption (RTC memory): Handshake once per reboot, ~1.8 mJ per message, about 18 months of battery life, best for ESP32 with RTC memory.
Plain CoAP (no security): No handshake, ~1.2 mJ per message, about 24 months of battery life, suitable only for lab testing.
Prevention: For deep-sleep devices, default to OSCORE unless you have a specific reason to use DTLS. If DTLS is required, implement session resumption with RTC-persisted state.
50.13 Concept Relationships
How CoAP security and applications connect to broader IoT security concepts:
DTLS Security Builds On:
TLS Fundamentals - Transport Layer Security adapted for UDP datagrams
Test your understanding of CoAP security concepts with the quizzes below.
Label the Diagram
50.15 What’s Next
Now that you understand CoAP security and implementation patterns, continue with these related chapters:
CoAP Practice and Exercises: Visual reference gallery, worked examples, and hands-on exercises. Reinforce and test everything covered in this chapter with real calculations and problem sets.
CoAP Observe Extension: Server-push notifications and subscription lifecycle. Understand how observe patterns interact with DTLS sessions and token management.
CoAP Methods and Features: RESTful operations, block transfer, and multicast. Deepen knowledge of the CoAP features used in the real-world applications in this chapter.
CoAP Fundamentals and Architecture: Message types, reliability, and header structure. Review the foundations of CON vs NON and token matching discussed in the implementation pitfalls.
IoT Security Threats: Attack vectors and threat modelling for IoT. Learn what DTLS protects against: eavesdropping, replay, and man-in-the-middle attacks.
Cryptography for IoT: AES, key management, and cipher suites. Understand the cryptographic primitives that underpin DTLS-PSK and DTLS-Certificate modes.