{"id":225951,"date":"2026-08-10T00:50:00","date_gmt":"2026-08-10T00:50:00","guid":{"rendered":"https:\/\/neodata.com.uy\/labs\/?p=225951"},"modified":"2026-09-10T10:00:12","modified_gmt":"2026-09-10T10:00:12","slug":"the-payload-must-fit-the-channel","status":"publish","type":"post","link":"https:\/\/neodata.com.uy\/labs\/the-payload-must-fit-the-channel\/","title":{"rendered":"The payload must fit the channel."},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>A compact signed object needs one deterministic binary form and three explicit transport profiles. QR, NFC and Bluetooth LE fail in different ways when signature and key material cross their byte budgets.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The useful payload is rarely the byte string that reaches the channel. A claim set becomes a serialized object with an algorithm identifier, a key identifier and a signature. Each carrier then adds its own representation or framing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The design problem is therefore not \u201cHow large is the signature?\u201d It is \u201cWhat exact byte sequence must cross this channel, and how many channel units does it consume?\u201d A compact implementation answers that question before it chooses a QR version, an NFC tag or a GATT transfer profile.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"start-with-one-transport-independent-object\">Start with one transport-independent object<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The cryptographic object should have one binary representation across all three channels. Creating a QR-specific JSON form, an NFC-specific TLV and a BLE-specific packet format produces three canonicalization problems and three test surfaces. A better boundary is a deterministic CBOR payload protected by COSE. QR, NDEF and BLE then carry the same authenticated bytes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CBOR is compact because integers, byte strings, arrays and maps have binary encodings with explicit lengths. Compactness alone is not sufficient for signing. RFC 8949 permits multiple encodings for some equivalent data items, so the application profile must require deterministic encoding. Use definite lengths, shortest permitted integer and length encodings, and a documented ordering rule for map keys. Reject duplicate keys. Do not sign a decoded object and hope another encoder reconstructs the same bytes. Sign the exact deterministic byte string.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">COSE_Sign1 provides a four-element structure containing protected headers, unprotected headers, a payload and one signature. Put the algorithm identifier in the protected header. Put a short key identifier there as well when changing it would alter verifier behavior. A <code>kid<\/code> is a lookup hint, not proof of which key is valid. The verifier still needs a trusted binding between that identifier and a public key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A small CDDL schema should define every integer label, field, unit and range. Version it explicitly rather than inferring a version from field presence.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"public-keys-are-provisioning-decisions\">Public keys are provisioning decisions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ed25519 has a 32-byte public key and a 64-byte signature. Its signature-to-public-key ratio is 2:1. ML-DSA-44 has a 1,312-byte public key and a 2,420-byte signature, a ratio of about 1.84:1. The similar ratios conceal a very different transport result. Omitting an Ed25519 public key saves a useful but modest number of bytes. Omitting an ML-DSA-44 public key can determine whether a message fits in a single QR symbol at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Transmit the public key only when the verifier cannot obtain it through prior provisioning, a cached trust list or an authenticated key directory. Replacing it with an eight-byte identifier moves cost into lifecycle management, which must cover rotation, cache expiry and identifier collisions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Point compression also belongs at the key-distribution layer. A P-256 point in the SEC 1 uncompressed form uses a prefix plus two 32-byte coordinates. Its compressed form uses a prefix plus the 32-byte x-coordinate. COSE can represent the y-coordinate as a sign bit for EC2 keys. Ed25519 already defines a 32-byte compressed point encoding, so applying a second generic compression step provides no corresponding reduction. Compression must be defined by the key format. Removing bytes from an ad hoc point representation can make validation ambiguous or impossible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Signatures and public keys leave little for general compression to exploit. A repetitive application body may be compressed under a fixed, bounded profile before signing. Authenticate the method and set decompression limits.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"three-channels-three-failure-modes\">Three channels, three failure modes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"qr-code-has-an-optical-budget\">QR Code has an optical budget<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A QR symbol is not a stream. The complete object must fit one symbol unless the application defines a multi-symbol assembly protocol. DENSO WAVE specifies versions 1 through 40 and four error-correction levels. Raising error correction reduces data capacity while increasing recovery from damaged codewords.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Selected byte-mode capacities show the discontinuities that matter to design:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr class=\"header\">\n<th>QR version and level<\/th>\n<th style=\"text-align: right;\">Maximum binary bytes<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr class=\"odd\">\n<td>Version 4, M<\/td>\n<td style=\"text-align: right;\">62<\/td>\n<\/tr>\n<tr class=\"even\">\n<td>Version 8, M<\/td>\n<td style=\"text-align: right;\">152<\/td>\n<\/tr>\n<tr class=\"odd\">\n<td>Version 10, M<\/td>\n<td style=\"text-align: right;\">213<\/td>\n<\/tr>\n<tr class=\"even\">\n<td>Version 40, L<\/td>\n<td style=\"text-align: right;\">2,953<\/td>\n<\/tr>\n<tr class=\"odd\">\n<td>Version 40, M<\/td>\n<td style=\"text-align: right;\">2,331<\/td>\n<\/tr>\n<tr class=\"even\">\n<td>Version 40, Q<\/td>\n<td style=\"text-align: right;\">1,663<\/td>\n<\/tr>\n<tr class=\"odd\">\n<td>Version 40, H<\/td>\n<td style=\"text-align: right;\">1,273<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Capacity is not field readability. Select error correction for the operating environment, retain margin below the table limit, and test the resulting module size on the intended display and optical path.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Carry binary bytes directly when the QR library supports byte mode. Base64url expands three input bytes into four output characters, apart from final padding rules. That conversion can force a larger QR version. A text-only integration should count the encoded text, not the original binary object.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"nfc-has-a-record-and-memory-budget\">NFC has a record and memory budget<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">NDEF standardizes a common application data format, but it does not create one universal NFC payload limit. Available space and transfer behavior depend on the tag type, product and protocol. Measure the encoded NDEF message against the actual target, not against a generic statement that NFC carries \u201ca few kilobytes.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A single NDEF external-type record is a compact carrier for the authenticated bytes. For an illustrative type name <code>example.org:sf<\/code>, which occupies 14 bytes, a short NDEF record with no ID adds 17 bytes: one record header, one type-length octet, one payload-length octet and the type name. A 180-byte COSE object therefore becomes a 197-byte NDEF record. Once the payload exceeds 255 bytes, the short-record form no longer applies and the payload-length field uses four bytes. The same type then adds 20 bytes. These figures describe that exact record construction, not all NDEF messages.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"bluetooth-le-has-a-negotiated-fragmentation-budget\">Bluetooth LE has a negotiated fragmentation budget<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For a GATT notification, the characteristic value available to the application is the negotiated ATT_MTU minus the three-byte notification opcode and handle. An ATT_MTU of 23 therefore carries 20 characteristic bytes. Larger MTUs may be negotiated, but the sender must not assume that negotiation succeeds or that every peer exposes the same application limit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bluetooth link-layer segmentation does not eliminate application framing. A complete signed object can span multiple notifications, be interrupted by disconnect, or arrive alongside another transfer. A minimal profile can reserve four bytes per notification: a two-byte transfer identifier, one-byte fragment sequence and one-byte fragment count. That leaves 16 object bytes at ATT_MTU 23 and 240 bytes at ATT_MTU 247. The one-byte count caps a transfer at 255 fragments, so the profile must reject larger objects or define an extended header before deployment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The signature validates the reassembled COSE object, but it does not tell the receiver how long to retain partial transfers. Specify a timeout, a maximum total length, one active transfer limit per peer and duplicate-fragment behavior. Allocate memory only after validating the fragment count against the profile maximum.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"a-deterministic-serialization-and-transport-algorithm\">A deterministic serialization and transport algorithm<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The following procedure keeps cryptographic and transport decisions separate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>encode_for_channel(claims, suite, key_ref, channel):\n    1. Validate claims against a versioned schema.\n       Reject unknown critical fields, duplicate labels and out-of-range values.\n\n    2. Normalize values.\n       Use integer units, raw byte strings and fixed semantic types.\n       Do not include display formatting.\n\n    3. Build the application map with small registered integer labels.\n       Include schema version and freshness fields required by the use case.\n\n    4. Encode the map as deterministic CBOR.\n       Use definite lengths, shortest encodings and the profile&#39;s map order.\n\n    5. Build protected COSE headers containing algorithm and key identifier.\n       Include a public key only when the trust model requires in-band delivery.\n\n    6. Construct the COSE Sig_structure and sign its exact bytes.\n       Emit an untagged COSE_Sign1 if the surrounding profile identifies it.\n\n    7. Measure the final COSE byte string. Do not estimate from claim lengths.\n\n    8. Adapt without changing the COSE bytes:\n       QR: encode directly in byte mode and select version plus error correction.\n       NFC: place in one external-type NDEF record and measure the full record.\n       BLE: fragment by negotiated ATT_MTU using the four-byte frame header.\n\n    9. Reject any result above the channel profile limit.\n       Never truncate a key, signature, protected header or authenticated payload.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The serializer should expose a size trace in tests: application CBOR, protected headers, signature, COSE total, carrier wrapper and fragment count. This makes a later algorithm migration visible before it reaches field devices.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"worked-byte-budgets\">Worked byte budgets<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Consider a deterministic application payload that is exactly 96 bytes. An untagged Ed25519 COSE_Sign1 with protected <code>{alg, kid}<\/code> headers can be 180 bytes under this explicit encoding: one byte for the outer array, 14 bytes for a protected-header byte string containing an eight-byte <code>kid<\/code>, one byte for an empty unprotected map, 98 bytes for the 96-byte payload and its byte-string header, and 66 bytes for the 64-byte signature and its byte-string header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That object fits a Version 10-M QR with 33 bytes of nominal capacity remaining. In the illustrative NDEF record it occupies 197 bytes. Over BLE notifications with the four-byte application header, it needs 12 fragments at ATT_MTU 23 because each carries 16 object bytes. At ATT_MTU 247 it fits one 240-byte application fragment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now add the 32-byte Ed25519 public key as a new CBOR map member. The integer label, byte-string length and value add 35 bytes, bringing the signed object to 215 bytes. It no longer fits Version 10-M. The NDEF record becomes 232 bytes. BLE needs 14 fragments at ATT_MTU 23 but still one at ATT_MTU 247. A small key has crossed a QR boundary and two BLE-fragment boundaries.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For ML-DSA-44, the 2,420-byte signature plus the 96-byte application payload already exceeds 2,516 bytes before COSE headers. It cannot fit Version 40-M, whose binary capacity is 2,331 bytes. It may fit Version 40-L only if the complete measured wrapper remains below 2,953 bytes, at the cost of using the lowest error-correction level. Adding the 1,312-byte public key raises the lower bound above 3,828 bytes, which cannot fit any single standard QR Code. At ATT_MTU 23, even the 2,516-byte lower bound requires at least 158 fragments with the four-byte header. At ATT_MTU 247 it requires at least 11.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These are sizing results, not latency or scan-time measurements. They show where the format crosses a structural boundary. Camera capture time, NFC transaction time and BLE throughput still require tests on the intended devices and conditions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-useful-optimization-order\">The useful optimization order<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The first savings should come from removing duplicated trust material, text encodings and optional claims. The second should come from deterministic binary structures and registered integer labels. Point compression is useful when the selected key format supports it. Fragment-header tuning comes last because saving one header byte rarely compensates for carrying a public key or certificate chain on every transaction.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A transport profile is complete when it states a maximum authenticated-object size, a key-provisioning model, one canonical encoding, a QR error-correction policy, an NDEF record form and a BLE reassembly limit. Without those constraints, \u201ccompact\u201d is only an adjective. With them, every algorithm choice has a byte cost that can be tested before deployment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"context-and-scope\">Context and scope<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This implementation note abstracts payload engineering for a constrained field deployment. The commissioning organisation, device models and measured channel results are omitted. Worked totals are accounting examples from stated encodings and transport constants.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"references\">References<\/h2>\n\n\n\n<ol class=\"wp-block-list\" type=\"1\">\n\n<li><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc8949\">RFC 8949: Concise Binary Object Representation (CBOR)<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9052\">RFC 9052: CBOR Object Signing and Encryption (COSE): Structures and Process<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9053\">RFC 9053: CBOR Object Signing and Encryption (COSE): Initial Algorithms<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc8032\">RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)<\/a><\/li>\n\n\n<li><a href=\"https:\/\/csrc.nist.gov\/pubs\/fips\/204\/final\">NIST FIPS 204: Module-Lattice-Based Digital Signature Standard<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.qrcode.com\/en\/about\/version.html\">DENSO WAVE: Information capacity and versions of QR Code<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.qrcode.com\/en\/about\/error_correction.html\">DENSO WAVE: QR Code error correction feature<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.bluetooth.com\/specifications\/specs\/core-specification\/\">Bluetooth SIG: Bluetooth Core Specification 6.3<\/a><\/li>\n\n\n<li><a href=\"https:\/\/nfc-forum.org\/build\/specifications\/\">NFC Forum: Specifications, including NDEF and Tag Types<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.secg.org\/sec1-v2.pdf\">SEC 1, Version 2.0: Elliptic Curve Cryptography<\/a><\/li>\n\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>A compact signed object needs one deterministic binary form and three explicit transport profiles. QR, NFC and Bluetooth LE fail in different ways when signature and key material cross their byte budgets.<\/p>\n","protected":false},"author":2,"featured_media":225944,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","nlabs_series_part":3},"categories":[11],"tags":[69,82,56,75,81,77],"nlabs_series":[79],"nlabs_article_type":[37],"class_list":["post-225951","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-in-practice","tag-applied-cryptography","tag-bluetooth-low-energy","tag-digital-identity","tag-protocols","tag-qr-codes","tag-transport-systems","nlabs_series-signature-footprint-engineering","nlabs_article_type-analysis"],"_links":{"self":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts\/225951","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/comments?post=225951"}],"version-history":[{"count":1,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts\/225951\/revisions"}],"predecessor-version":[{"id":225957,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts\/225951\/revisions\/225957"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/media\/225944"}],"wp:attachment":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/media?parent=225951"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/categories?post=225951"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/tags?post=225951"},{"taxonomy":"nlabs_series","embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/nlabs_series?post=225951"},{"taxonomy":"nlabs_article_type","embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/nlabs_article_type?post=225951"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}