Internet-Draft moq-e2ee September 2026
Curley Expires 28 March 2027 [Page]
Workgroup:
moq
Internet-Draft:
draft-lcurley-moq-e2ee-00
Published:
Intended Status:
Informational
Expires:
Author:
L. Curley

MoQ End-to-End Encryption Profile

Abstract

This document specifies moq-e2ee-00, a versioned profile for end-to-end encryption of MoQ application payloads. Authorized publishers and subscribers share a 32-byte broadcast secret out of band. Each publisher instance mints an epoch and publishes under an opaque broadcast path ending in it. HKDF-SHA-256 derives opaque physical track names and per-track AES-128-GCM keys from the secret and the epoch; grouped frames and datagrams use separate key domains. Media frames and datagrams carry only ciphertext plus a 16-byte tag. The profile binds object identity through derivation and the nonce, not an on-wire header.

Note to Readers

This document was generated by an AI model from the implementation at github.com/moq-dev/moq and is maintained alongside it. Submit an issue or PR if this spec sucks and you want to fix anything.

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 28 March 2027.

▲

Table of Contents

1. Conventions and Definitions

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

2. Introduction

MoQ relays forward named tracks of groups and frames ([moql], [moqt]) without parsing application payloads. This profile encrypts those payloads and hides the semantic broadcast and track names that would otherwise describe them, so a relay cannot recover content. It reuses AES-128-GCM and the 96-bit group/frame nonce shape of [secure] where those identities map, and specifies the moq-lite and datagram bindings that draft does not cover.

The profile does not distribute keys, sign senders, pad payloads, or rotate a key inside an epoch. Applications that need those properties terminate this profile and run a different one.

4. Profile Version

This document defines profile moq-e2ee-00. The profile is named out of band alongside the credential; an application MUST refuse a credential that names any other profile. A new profile is a new document; implementations MUST NOT fall back to plaintext or to an older profile because a catalog, announcement, or peer suggested one.

5. Credential

The application supplies an immutable credential:

Credential {
  context (b)
  kid (u64)
  secret (32)
}

context: Opaque bytes chosen by the application as the broadcast's end-to-end identity. Both ends MUST use identical bytes. The MoQ broadcast path is visible to relays and may be remounted under another prefix, so it is not this field unless the application copies it in.

kid: Selects among credentials the application retains. It never changes in place; rotating the secret is a new kid.

secret: Exactly 32 bytes from a cryptographically secure random generator. It MUST NOT be a password, passphrase, or other guessable input.

kid MUST be in 0..=2^53-1 inclusive, the largest integer TypeScript can represent exactly. context, every epoch, and every semantic broadcast or track name MUST be at most 65535 bytes, the bytes encoding width. An implementation MUST refuse a credential outside those ranges (identity) or whose secret is not 32 bytes (invalid_secret).

Applications distribute credentials over their own authenticated channel. MoQ announcements, catalogs, paths, and relay authorization MUST NOT carry the secret or authenticate it. Implementations MUST let the application retain more than one credential and select among them; they MUST NOT infer the kid from the transport.

6. Epoch and Broadcast Path

A generation is one credential under one epoch. Every derivation and every nonce is scoped to a generation.

epoch: Opaque nonempty bytes minted by the publisher instance, containing no /. Each instance of a broadcast MUST mint an epoch that no other instance under the same credential has used or will use. Two instances MUST NOT share an epoch: they would derive the same keys and collide on nonces. The RECOMMENDED epoch is the lowercase text of a UUID version 7 ([RFC9562]): its leading 48-bit timestamp makes epochs sort by creation time and its random bits make collisions negligible.

A protected broadcast is published at <opaque>/<epoch>, where <opaque> is the 22-character base64url segment derived from the credential and the application's semantic broadcast name according to Section 8, and <epoch> is the epoch text. The opaque derivation does not include the epoch, so every instance of the same semantic broadcast shares a discovery prefix. The path carries no format or protection marker; for example, the semantic name meeting.hang appears only as an input to the opaque derivation. A plaintext consumer that opens the protected broadcast fails because it cannot find the plaintext catalog it expects, not because of a path naming rule. Subscribers discover instances by the <opaque>/ prefix and select the greatest epoch when epochs are UUID version 7 text, where greatest is newest. Opaque epochs carry no creation order, so any other epoch form needs an application rule for which instance is current. A subscriber that already knows the full path takes the epoch from its last segment.

The epoch and the path are not secret and are not authenticated. A relay that presents a wrong epoch causes authentication failure; a relay that withholds a newer instance denies service. Neither can cause a nonce to repeat, because only the publisher instance chooses the epoch it encrypts under.

A restart or replacement of a publisher is a new instance and mints a new epoch. Transport sequence numbers therefore restart freely without any coordination between instances. Ended instances remain readable at their own path for as long as relays or archives retain them.

7. Canonical Encoding

HKDF info fields use unique encodings, not varints.

Integers used as group, frame, or kid identities are refused before encoding if they fail Section 11.

8. Key Derivation

Keys and physical names are derived with HKDF-SHA-256 [RFC5869]. Let salt be the ASCII bytes of "moq-e2ee-00".

prk = HKDF-Extract(salt, secret)

Opaque broadcast path material is 16 bytes:

path_info = "moq-e2ee-00 path"
            || bytes(context)
            || u64(kid)
            || bytes(semantic_broadcast)
opaque    = HKDF-Expand(prk, path_info, 16)

semantic_broadcast is the UTF-8 bytes of the application's semantic broadcast name. The opaque path segment is the unpadded base64url encoding of opaque ([RFC4648] Section 5): 22 ASCII characters. The epoch is deliberately absent from this derivation, so a subscriber can derive the prefix before discovering an instance.

Physical track name material is 16 bytes:

name_info = "moq-e2ee-00 name"
            || bytes(context)
            || bytes(epoch)
            || u64(kid)
            || bytes(semantic_name)
physical  = HKDF-Expand(prk, name_info, 16)

semantic_name is the UTF-8 bytes of the application's track name (catalog.json, video, and so on). The physical track name is the unpadded base64url encoding of physical ([RFC4648] Section 5): 22 ASCII characters, which is a valid moq-lite track name. This function may hide any track-shaped name the application wants a relay not to read; it is not limited to media tracks.

AEAD keys are 16 bytes, one per physical name and domain:

key_info = "moq-e2ee-00 key"
           || bytes(context)
           || bytes(epoch)
           || u64(kid)
           || bytes(physical_name)
           || domain
key      = HKDF-Expand(prk, key_info, 16)

domain is a single byte: 0x00 for grouped frames, 0x01 for datagrams. physical_name here is the 22-character ASCII string, not the raw 16-byte material.

A given (generation, physical_name, domain) tuple has one key. Implementations MUST derive names from semantic names, then keys from the resulting physical names. A subscriber that learns a physical name from a decrypted catalog derives its key without ever knowing the semantic name.

9. Object Identity

A protected object is the tuple (generation, physical_name, domain, group, frame).

9.1. Grouped Frames

A grouped frame uses domain = 0x00, the group's sequence as group, and the frame index within that group as frame. moq-lite numbers frames from 0 in write order ([moql]). On MoQ Transport, frame is the explicit Object ID, never its arrival ordinal. Publishers supporting both transports MUST assign contiguous Object IDs from zero so that each matches its moq-lite write-order index; relays MUST NOT renumber protected objects. An Object ID above 2^32-1 MUST be refused as identity before encryption or decryption.

9.2. Datagrams

A datagram uses domain = 0x01, its 64-bit sequence as group, and frame = 0. MoQ Transport has no datagram mapping in this profile; shared vectors cover grouped tracks on both transports and datagrams on moq-lite only.

9.3. Nonce

The 96-bit AES-GCM nonce is:

nonce = u64(group) || u32(frame)

AES-GCM's internal block counter is not frame. Implementations MUST call a standard AEAD API [RFC5116] with this nonce and an empty AAD.

The empty AAD is deliberate: profile version, context, epoch, kid, physical name, and domain are bound by HKDF; group and frame are bound by the nonce. Rewritten timestamps and mutable routing properties are not authenticated.

10. Payload Protection

Let Nt = 16. AES-128-GCM encrypts the application bytes with key, nonce, and empty AAD. The bytes placed in the MoQ frame or datagram payload are ciphertext concatenated with the 16-byte tag, in the [RFC5116] convention. There is no inner header.

Within a generation a publisher MUST allocate group and datagram sequences monotonically and frame indices in write order, so an identity is encrypted at most once. Encrypting at an identity the same instance already used is reuse and MUST be refused. Relays and caches forward ciphertext unchanged, so replay from a cache never re-encrypts.

10.1. Plaintext Ceiling

Protected payload length is plaintext length plus Nt. A publisher MUST refuse plaintext that would make the protected payload exceed the transport payload limit for that object (oversize), before it touches the network.

The interoperable grouped-frame payload cap matching current moq-net implementations is 32 MiB, so grouped plaintext MUST be at most 32 MiB - 16 bytes. moq-lite datagram bodies MUST remain at most 1200 bytes including Subscribe ID, Group Sequence, and Timestamp ([moql] Section "Datagrams"). Those three varints are at most 24 bytes, so datagram plaintext MUST be at most 1200 - 24 - 16 = 1160 bytes; a publisher cannot observe the Subscribe ID each hop will encode and MUST NOT budget for a smaller header.

10.2. Catalogs

A catalog is a track like any other: published under the physical name derived from its semantic name, with each snapshot or delta protected at its own group and frame identity. Hang [hang] catalog.json and catalog.json.z and MSF's catalog are semantic names; authorized clients derive those physical names from the generation, then learn the remaining opaque names from the decrypted catalog. A Hang rendition-map key in that catalog is the physical name of the track.

If a representation is compressed, compression is applied to the catalog bytes before AEAD and reversed after decryption. Encrypting then compressing is forbidden: ciphertext does not compress, and the .z sibling would leak the uncompressed size ratio.

11. Bounds

Implementations MUST refuse non-integer identities (including NaN and infinities) and identities outside these bounds before encoding or AEAD:

2^53-1 is Number.MAX_SAFE_INTEGER. It is the strictest exact integer bound across current TypeScript and Rust implementations. The 32-bit frame width is the nonce field. The 2^24 invocation cap is the interoperable AES-GCM record limit from [aeadlimits]. GCM authenticity also depends on total processed blocks, so 2^24 frames at the 32 MiB transport ceiling would be about 2^45 blocks; the 2^36-byte cap is the matching total-block bound. Small records hit the invocation cap first; large records hit the byte cap first. A receiver counts failed opens too, since each is an AEAD invocation under that key.

12. Failure Behavior

E2EE is an explicit per-broadcast mode. There is no plaintext fallback.

Typed failures:

Authentication failure on a grouped track MUST end that track with authentication. Authentication failure on a datagram MUST drop that datagram and emit authentication; the track continues.

Grouped frames need no duplicate window: a transport delivers each frame of a group once, in order, at its index. Receivers SHOULD suppress datagram sequences they still retain; the window MUST be bounded, and 1024 sequences below the greatest opened is RECOMMENDED. The AEAD identity and epoch rules are the security boundary; a relay may still delay, reorder, suppress, or replay ciphertext outside a receiver's window.

A late subscriber MAY start at any group the publisher still holds. Gaps are not errors. A receiver MUST NOT require group 0 or any prior identity before opening a later authentic object.

13. Test Vectors

Known-answer and negative vectors live in moq-e2ee-00.json beside this draft. Hex strings are octet sequences. The JSON is authoritative for primitive interop; an implementation of this profile MUST pass every vector. Each negative row specifies an operation, its inputs, and its expected typed error. Non-finite frame inputs use the strings NaN, Infinity, and -Infinity; group inputs in identity tests are decimal strings. Implementations whose types cannot represent an invalid input MUST reject it at their input boundary.

The file covers opaque path derivation, key derivation, physical naming, grouped frames, a datagram at the fixed budget, the same identity under two epochs, relocation across every identity dimension, tag failure, malformed physical names, identity bounds, and oversize plaintext.

The shared verifier is stateless. It does not verify reuse, exhausted, duplicate, or failure propagation. Each language core MUST test those lifecycle requirements, including monotonic allocation, per-key invocation and plaintext-byte accounting, bounded datagram suppression, and that a new instance under a new epoch authenticates while the old epoch does not. Passing the primitive vectors alone is not profile conformance.

14. Security Considerations

Relays, caches, recorders, and control planes are untrusted for content. Authorized endpoints that hold the broadcast secret are trusted. Sender authenticity against another endpoint that also holds the secret is not a goal of moq-e2ee-00.

A relay can still observe the outer broadcast path including the opaque segment and epoch, opaque physical names, group and frame structure, timestamps, sizes, and traffic patterns. Padding and metadata-flow confidentiality are out of scope.

The opaque segment hides the application's semantic broadcast name from a relay only while the application does not publish or otherwise expose the same name in plaintext. The epoch remains visible to every relay on the path.

Nonce reuse under one key is catastrophic for AES-GCM. The profile prevents it by deriving every key from an epoch that only one publisher instance ever uses, allocating identities monotonically within that instance, separating datagram and grouped domains, and capping invocations and plaintext bytes per key. No state survives an instance: nothing needs to be persisted across restarts to stay safe.

Empty AAD does not weaken the binding: every immutable end-to-end field is in the HKDF info or the nonce. Timestamps are excluded because relays rewrite them; a relay can therefore shift or reorder authentic objects in time within a receiver's tolerance.

The opaque path segment is a deterministic function of the secret and semantic broadcast name; physical track names are deterministic functions of the secret and epoch. An attacker without the secret cannot predict them; an attacker with the secret can derive every name, which is intended.

15. IANA Considerations

This document requests no registrations.

16. References

16.1. Normative References

[moql]
Curley, L., "Media over QUIC - Lite", Work in Progress, Internet-Draft, draft-lcurley-moq-lite-05, , <https://datatracker.ietf.org/doc/html/draft-lcurley-moq-lite-05>.
[moqt]
Nandakumar, S., Vasiliev, V., Swett, I., and A. Frindell, "Media over QUIC Transport", Work in Progress, Internet-Draft, draft-ietf-moq-transport-21, , <https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport-21>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC4648]
Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, , <https://www.rfc-editor.org/rfc/rfc4648>.
[RFC5116]
McGrew, D., "An Interface and Algorithms for Authenticated Encryption", RFC 5116, DOI 10.17487/RFC5116, , <https://www.rfc-editor.org/rfc/rfc5116>.
[RFC5869]
Krawczyk, H. and P. Eronen, "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)", RFC 5869, DOI 10.17487/RFC5869, , <https://www.rfc-editor.org/rfc/rfc5869>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.
[RFC9562]
Davis, K., Peabody, B., and P. Leach, "Universally Unique IDentifiers (UUIDs)", RFC 9562, DOI 10.17487/RFC9562, , <https://www.rfc-editor.org/rfc/rfc9562>.

16.2. Informative References

[aeadlimits]
Günther, F., Thomson, M., and C. A. Wood, "Usage Limits on AEAD Algorithms", Work in Progress, Internet-Draft, draft-irtf-cfrg-aead-limits-13, , <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-aead-limits-13>.
[hang]
Curley, L., "Media over QUIC - Hang", Work in Progress, Internet-Draft, draft-lcurley-moq-hang-02, , <https://datatracker.ietf.org/doc/html/draft-lcurley-moq-hang-02>.
[secure]
Jennings, C. F., Nandakumar, S., and R. Barnes, "End-to-End Secure Objects for Media over QUIC Transport", Work in Progress, Internet-Draft, draft-ietf-moq-secure-objects-01, , <https://datatracker.ietf.org/doc/html/draft-ietf-moq-secure-objects-01>.
[sframe]
Omara, E., Uberti, J., Murillo, S. G., Barnes, R., Ed., and Y. Fablet, "Secure Frame (SFrame): Lightweight Authenticated Encryption for Real-Time Media", RFC 9605, DOI 10.17487/RFC9605, , <https://www.rfc-editor.org/rfc/rfc9605>.

Changelog

draft-lcurley-moq-e2ee-00

  • Initial moq-e2ee-00 profile: out-of-band credential, opaque broadcast path with a publisher-minted epoch as its last segment, HKDF physical names and keys, AES-128-GCM payloads, identity bounds, typed failures, and shared primitive vectors.

Acknowledgments

This document was drafted with the assistance of Grok, an AI assistant by xAI.

Author's Address

Luke Curley