| Internet-Draft | raAE | July 2026 |
| Sullivan | Expires 24 January 2027 | [Page] |
This document defines random-access authenticated encryption (raAE), a primitive that partitions a message into an indexed sequence of segments that can be encrypted and decrypted independently and in any order. It also specifies SEAL (Segmented Encryption and Authentication Layer), a parameterized construction that defines a family of concrete raAE instantiations, one for each valid choice of an Authenticated Encryption with Associated Data (AEAD) algorithm, a key derivation function (KDF), and associated parameters.¶
SEAL supports immutable (write-once), append-only, and mutable (in-place ciphertext rewrite) operations, each with per-segment authentication. A separately configured snapshot authenticator can additionally authenticate the complete, indexed segment set.¶
The document also defines the security notions of raAE, specifies the requirements for conforming constructions, and analyzes SEAL against those requirements. Concrete cipher suites, serialization layouts, and test vectors for SEAL are specified in a companion document.¶
This note is to be removed before publishing as an RFC.¶
Discussion of this document takes place on the Crypto Forum Research Group mailing list (cfrg@ietf.org), which is archived at https://mailarchive.ietf.org/arch/browse/cfrg.¶
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 24 January 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
Large encrypted content is often stored as a sequence of fixed-size segments, so that an application can read or modify any part of it without processing the whole. Encrypted backups, encrypted file formats, object stores with partial updates, and full-disk encryption over fixed-size blocks all work this way. Such a system needs to encrypt, decrypt, or re-encrypt any individual segment on its own and in arbitrary order, and to verify that the stored object as a whole is authentic and complete, without re-encrypting the segments that did not change.¶
This document specifies the raAE primitive (Section 3), whose base interface and security notions come from Fábrega et al. ([FLRR25]), and defines SEAL (Segmented Encryption and Authentication Layer), a parameterized construction of raAE. The base algorithms encrypt and decrypt segments in arbitrary order, so a caller can read or replace any segment on its own. On top of that base, the extended raAE interface adds in-place rewrite and a snapshot, a stored value that authenticates the segment set as the writer last recorded it. A rewrite re-encrypts only the changed segment and updates the snapshot, leaving every other segment untouched. Appending to the end, or truncating from the end, composes the same per-segment operations, so the snapshot stays consistent without a separate algorithm.¶
SEAL builds the primitive from a chosen AEAD algorithm, a key derivation function (KDF), and configuration parameters. Under a snapshot authenticator, it binds every segment and the count into a public snapshot value that an adversary cannot forge without the content-derived key.¶
raAE separates the two authentication scopes that applications often conflate:¶
One AEAD tag verifies under the segment index, a finality bit marking the last segment, and any caller-supplied associated data. It does not establish that the segment belongs to the current snapshot.¶
When a snapshot authenticator is configured, snapshot verification checks that the present segment set and the count are exactly what the writer last recorded, under a content-derived key. Verification targets the whole segment set, and, where the authenticator supports it, can instead confirm that a designated subset of segments is included at their recorded positions, without reading the others (Section 4.7.1). It does not establish freshness against whole-object rollback, which a consuming protocol must supply.¶
raAE and SEAL are deliberately distinct layers. A consuming protocol supplies serialization, storage transactions, key management, and rollback protection, which are out of scope here. The full boundary of what raAE does not provide is Section 6.10.¶
The remainder of this document presents raAE first and SEAL second. Section 1.1 situates raAE against prior segmented authenticated-encryption (AE) constructions. Section 2 fixes terminology and notation. Section 3 specifies the extended raAE primitive and its snapshot operations. Section 4 defines SEAL. Section 5 states the target security properties an raAE construction must meet, the assumptions SEAL's components must satisfy, and the operational limits on its use, and defines the security notions. Cipher suites, serialization layouts, named instantiations, and test vectors are specified in the companion document [I-D.sullivan-seal-concrete].¶
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.¶
A message segment is a unit of plaintext that is individually encrypted and authenticated. Segments need not be uniform in length. A construction MAY set a maximum segment size, and any segment may be shorter than that maximum.¶
A message is the content that one CEK and salt pair protects, the unit the primitive encrypts. An object is that message in stored form: the ciphertext segments, their metadata, and any snapshot that a storage layer holds. Each object carries one message, whose content may change through rewrite and length-changing operations.¶
A ciphertext core is the AEAD encryption output corresponding to one message segment, excluding file-level headers and external segment metadata. An AEAD-specific split divides this output into ct_i, the encrypted segment body, and tag_i, the authentication tag.¶
A ciphertext segment is the stored representation of one encrypted message segment. Depending on nonce_mode, it contains the ciphertext core and may also contain a stored nonce or other per-segment metadata.¶
Segment metadata is per-segment information needed to decrypt or verify a ciphertext segment, such as a stored nonce, tag, finality status, or profile-defined values.¶
This document writes AEAD.Encrypt and AEAD.Decrypt for the authenticated encryption and decryption operations defined in [RFC5116].¶
Symbol form follows the layer a name belongs to. The abstract interface and the analysis use mathematical notation: a capital letter is an operand of that interface, either an octet string (K, N, G, T_g, A_i, M_i, C_i) or the per-message state (S). A lowercase letter is an integer index or bound (i, e, p, r).¶
N with a lowercase letter is a size that a chosen component fixes rather than one a deployment picks: the AEAD (Nk, Nn, Nt, Nb), the KDF (Nh), the snapshot authenticator (Na), or the nonce mode (Np). Everything the construction names concretely uses lowercase snake_case, whatever its type: octet strings (salt, commitment, segment_key, ct_i), counts (n_seg, n_ep), lengths a deployment configures (segment_max, commitment_length), and identifiers, which take an _id suffix (aead_id, kdf_id, snap_id, protocol_id).¶
Names that take arguments follow the same split. Initialisms are capitalized (LH, KDF, AEAD, XOF). The raAE tuple is CamelCase (KeyGen, StartEnc, EncSeg, StartDec, DecSeg, RewriteSeg, SnapVerify), while the methods of a component it parameterizes over are lowercase (snapshot, verify, add, remove, set_length), so a name shows which interface it belongs to. Derivations this document defines are lowercase (encode, frame, leaf, nonce).¶
A subscript selects one instance of a value: X_i is X for the segment at index i, as in A_i, P_i, ct_i, and tag_i, and d_e is the epoch head for epoch e. A value written f(i), as in segment_key(i) or nonce(i), is the value that the named derivation defines for index i.¶
Two suffixes and one prefix recur. A label suffix is an ASCII constant that separates one derivation from another (aad_label, epoch_key_label). A _calc suffix marks a value recomputed for comparison against a stored one (d_calc(e), leaf_calc(i)). An Adv prefix names an adversary's advantage in the analysis (Adv_snap, Adv_muPRF).¶
The remaining terms (ikm, label, info, L, salt, CEK, protocol_id, nonce_mode, epoch, epoch_key, payload_key, segment_key, snap_key, and the commitment) are defined in Section 4 where they first appear.¶
This document uses the following terms to keep the raAE primitive, the SEAL construction, profiles, and consuming protocols distinct.¶
The abstract random-access AE interface and security target.¶
The base interface plus snapshot maintenance and verification.¶
The AEAD and KDF algorithms and their sizes.¶
A named set of fixed parameter choices, identified by a protocol_id, needed for interoperability.¶
The per-object values that affect derivation and verification.¶
The protocol or format that stores objects and manages keys.¶
The consuming protocol's wire or storage encoding.¶
A zero-based independently encrypted plaintext unit.¶
A group of 2^epoch_length segment indices sharing one epoch key.¶
The public value that, when a snapshot authenticator is configured, authenticates one segment set.¶
A pluggable SEAL component that produces and verifies the snapshot value. Its abstract interface is Section 4.7.1, and its concrete instantiations are in [I-D.sullivan-seal-concrete].¶
AEAD verification for one segment only.¶
Verification of the complete segment set and snapshot.¶
A hash of one segment's ciphertext, stored in that segment's metadata. It binds the segment's content, not its position (Section 4.7.1).¶
Rollback protection supplied outside raAE.¶
The w-octet big-endian encoding of the non-negative integer n, as defined in [RFC8017].¶
Shorthand for I2OSP(n, 1), I2OSP(n, 2), I2OSP(n, 4), and I2OSP(n, 8) respectively.¶
Octet string concatenation.¶
Bitwise exclusive-or of two octet strings of equal length.¶
Octets a through b-1 inclusive of X (zero-based, half-open interval).¶
An ordered list of octet strings. A single-element list is written [x]. The brackets denote a list, not an optional parameter.¶
The length-prefixed injective encoding used by SEAL, defined in Section 4.3. frame is total over all field lengths. The construction requires an injective framing (Section 5.4) but does not mandate this encoding. Another profile MAY use any injective encoding that meets Section 5.4.¶
Key size in octets for the chosen AEAD algorithm.¶
Nonce size in octets for the chosen AEAD algorithm.¶
Presented nonce size in octets: the per-segment nonce stored in metadata. Np = Nn in random nonce mode and Np = 0 in derived nonce mode, where the nonce is recomputed from the key schedule.¶
Hash or pseudorandom function (PRF) output size in octets for the chosen KDF.¶
Authentication tag size in octets for the chosen AEAD algorithm.¶
Cipher block size in octets for the chosen AEAD algorithm, defined only for an AEAD built on a block cipher. The block-size birthday limit of Section 5.8 is expressed in Nb and does not apply to an AEAD with no block-cipher structure.¶
The snapshot value length in octets, the output size of the configured snapshot authenticator. It MAY depend on the segment count n_seg, and is 0 when none (Section 4.7).¶
The number of ciphertext segments in a stored message. Written n_seg (lowercase) to distinguish it from the per-message nonce N in the raAE interface (Section 3.2).¶
Random-access authenticated encryption (raAE) partitions a message into segments, each encrypted and decrypted on its own and in any order. A reader can open one segment without the rest of the message, and a writer can encrypt one without holding the others. In the base interface [FLRR25], each segment is written once and thereafter only read.¶
The extended raAE interface keeps that base and adds one capability: a segment already written may be replaced in place, without disturbing the rest. Rewriting changes the threat model. Per-segment authentication binds each segment in isolation, so it cannot tie the segments a reader assembles to a single recorded version of the object: a storage layer holding the ciphertext can roll a segment back to an older valid copy, substitute one valid same-index segment for another, or drop one, and each segment it presents still authenticates. An optional snapshot authenticates the segment set as the writer last recorded it, so a reader can detect these changes. The extension (Section 3.3) defines the snapshot and the operations that maintain it.¶
The security notions follow the interface. raAE and its rewrites target ra-ROR (random-access real-or-random) and ra-CMT (random-access context commitment) (Section 5.1). Rewriting leaves both untouched, since replacing a segment is an encryption the read-only adversary could already ask for. Snapshot authentication adds the one notion of its own, snapshot integrity (Section 5.1.4).¶
The base raAE interface, from [FLRR25], is a tuple of five algorithms: KeyGen, StartEnc, EncSeg, StartDec, and DecSeg.¶
Randomized key generation. Takes no input. Outputs a secret key K.¶
Deterministic. Takes a secret key K, nonce N, and global associated data G. Outputs a ciphertext header T_g and an initial per-message encryption state S.¶
Randomized. Takes the encryption state S, a position identifier p, per-segment associated data A_i, and message segment M_i. Outputs a ciphertext segment C_i. S is not modified. EncSeg may be called with the same S at any position, in any order, or concurrently.¶
Deterministic. Takes key K, nonce N, global associated data G, and ciphertext header T_g. Outputs a per-message decryption state S or an error.¶
Deterministic. Takes the decryption state S, position identifier p, per-segment associated data A_i, and ciphertext segment C_i. Outputs message segment M_i or an error.¶
The position identifier p = (i, b) consists of a segment number i and a terminal bit b in {0, 1}. They impose a total ordering on segments and carry a truncation defense: a ciphertext lacking a segment with b = 1 is incomplete. The encryption state S is immutable. That immutability is what makes arbitrary-order and fully parallel operation possible. The ciphertext header T_g commits to K, the nonce N, and global associated data G. A headerless scheme emits T_g = empty.¶
Replacing a segment already written, without re-encrypting the others, requires no new operation: re-encrypting a position is an ordinary EncSeg call over the immutable state, and it leaves ra-ROR and ra-CMT untouched.¶
A scheme that supports rewriting MAY carry a snapshot, a value over the current set of segments that a reader checks to detect tampering that per-segment authentication does not catch. A write-once scheme MAY carry one too: a single value that a consuming protocol authenticates once for the whole segment set (Section 4.7.1).¶
Two operations, added by this document, maintain and check the snapshot. RewriteSeg replaces a segment and updates the snapshot in the same step. SnapVerify decides whether a given set of segments is the one the writer last recorded.¶
Randomized. Takes the encryption state S and position identifier p, per-segment associated data A_i, the new message segment M'_i, the existing ciphertext C_i at position p, and the current snapshot. Outputs replacement ciphertext C'_i and an updated snapshot value, snapshot'.¶
Deterministic. Takes the encryption state S, the segments to verify as (position, ciphertext segment) pairs, and the snapshot. These are either the whole current object state or, where the authenticator supports it, a designated subset of it. Outputs accept or reject.¶
The extension also changes two base signatures. In the extended interface:¶
Deterministic. As in Section 3.2, and additionally outputs the initial snapshot value, the snapshot over the empty segment set.¶
Randomized. As in Section 3.2, and additionally outputs the snapshot value over the segment set that results from adding C_i at position p.¶
The snapshot is an authenticator over the set of segments belonging to the current object state. The base primitive has no snapshot value.¶
These additions define the extended raAE interface of this document: the tuple (KeyGen, StartEnc, EncSeg, StartDec, DecSeg, RewriteSeg, SnapVerify) over the snapshot value. The first five algorithms are those of Section 3.2. The snapshot value, RewriteSeg, and SnapVerify are the extension. A scheme can realize the base interface alone, as FLOE ([FLRR25]) does. SEAL (Section 4) realizes the full extended interface.¶
Changing a message's length is a composition of these operations, not a separate algorithm in either direction. Position-addressed encryption over immutable state already permits re-encrypting a position or encrypting at a higher index (see Section 5.1.1).¶
A holder appends by encrypting the new segments and re-marking the old final segment with RewriteSeg to clear its terminal bit. It truncates by re-marking the new final segment with RewriteSeg, dropping the trailing segments, and updating the snapshot over the surviving set. Appending to an empty object skips the re-mark, since there is no old final segment, and truncating away every segment yields the empty object, with no new final segment to re-mark.¶
A length change appends segments or drops a suffix. Each step updates the snapshot, and Section 4.9.3 gives the construction for both directions.¶
SEAL is a construction for the raAE primitive of Section 3, realizing its extended interface from a KDF, an AEAD, the labels, a nonce-generation method, and an optional snapshot authenticator. Its KDF combiner (Section 4.3) frames every derived key and input over any KDF with a two-step or one-step form. SEAL provides commitment around the AEAD, which need not itself commit.¶
A SEAL construction MAY select any AEAD, KDF, nonce mode, commitment configuration, or snapshot authenticator that satisfies the requirements of this section. Section 5 derives SEAL's bounds from the abstract properties of these components. Concrete parameter choices and interoperable suites are in [I-D.sullivan-seal-concrete].¶
In the key hierarchy below, a CEK and per-content salt feed the payload schedule, which derives four per-message values (Section 4.5).¶
The figure shows each segment's tag feeding the authenticator. Its actual per-segment input is the segment's metadata leaf (Section 4.7.1).¶
The commitment lets a reader reject a wrong key or parameter set before decrypting. The snapshot, when present, authenticates the current segment set. SEAL realizes the raAE operations of Section 3.2 as follows:¶
| raAE operation | SEAL realization |
|---|---|
| StartEnc | Derive the payload schedule from the CEK and salt, committing G (Section 4.5) |
| StartDec | Re-derive the schedule and verify the stored commitment (Section 4.6) |
| EncSeg, DecSeg | EncryptSegment and DecryptSegment (Section 4.8) |
| RewriteSeg | RewriteSegment, then update the snapshot authenticator for the changed segment (Section 4.9.2.1) |
| SnapVerify | Check the present indices and finality, then verify the snapshot via the authenticator (Section 4.7) |
SEAL's salt, payload_info, and G carry the raAE per-message inputs, and the commitment stands in for the header (Section 4.2).¶
Table 2 lists the parameters, inputs, and derived values, with what sets each and where it is defined.¶
| Parameter | Type or range | Set by | Defined in |
|---|---|---|---|
| AEAD | RFC 5116 algorithm | suite | Section 4.2.1 |
| KDF | KDF algorithm | suite | Section 4.3 |
| segment_max | power of two, at least 4096 | suite | Section 4.2.1 |
| epoch_length | integer r in 0 to 63 | suite | Section 4.5.2 |
| protocol_id | octet string | application | Section 6.11 |
| nonce_mode | "random" or "derived" | message, within the profile | Section 4.5.3 |
| aad_label | ASCII string | profile | Section 4.4.2 |
| commitment_length | integer, at least 16, default Nh | profile | Appendix C.4 |
| Nk, Nn, Nt, Nb | sizes in octets | AEAD | Section 2.2 |
| Nh | size in octets | KDF | Section 2.2 |
| CEK | 32 uniform random octets | message input | Section 4.5 |
| salt | 32 uniform random octets | message input | Section 4.5 |
| payload_info | ordered element list | message input | Section 4.4 |
| G | octet string, empty by default | message input | Section 4.6 |
| commitment | commitment_length octets | derived per message | Section 4.5 |
| payload_key | Nk octets | derived per message | Section 4.5 |
| snap_key | Nh octets | derived per message | Section 4.5 |
| nonce_base | Nn octets, derived mode only | derived per message | Section 4.5 |
| epoch_key(i) | Nk octets | derived per segment | Section 4.5.2 |
| segment_key(i) | Nk octets | derived per segment | Section 4.5.2 |
| nonce(i) | Nn octets | derived per segment | Section 4.5.3 |
| segment_aad(i, is_final, A_i) | octet string | derived per segment | Section 4.4.2 |
| tag_i | Nt octets | derived per segment | Section 4.8 |
SEAL fixes the segment additional authenticated data (AAD) encoding (Section 4.4.2), the commitment derivation (Section 4.6), and the snapshot value (Section 4.7) identically across all suites. Their octet lengths still vary by suite, because Nh and Nt are suite-dependent.¶
A suite is assembled from the following choices.¶
An authenticated encryption with associated data algorithm satisfying the interface of [RFC5116], with Nk-octet keys, Nn-octet nonces, and Nt-octet authentication tags.¶
A key derivation function supporting the abstract interface defined in Section 4.3.¶
A positive integer, a power of two and at least 4096 octets. Each segment carries at most this many plaintext octets. The length-dependent limits of Section 5.8 are computed at this size.¶
A non-negative integer r in 0 to 63. Each epoch covers 2^r consecutive segments aligned to a multiple of 2^r, so r sets the key-rotation granularity: r = 0 gives a fresh epoch key per segment and r = 63 one key for all segments (Section 4.5.2). Profiles MAY restrict r further per AEAD.¶
An application-chosen octet string that binds all derived values to a specific protocol version. Selection guidance is in Section 6.11.¶
A payload_info element selecting one of two nonce constructions, "random" or "derived". The constructions are defined in Section 4.5.3, and the valid (nonce_mode, snap_id) tuples depend on the profile.¶
One constant is fixed by the profile rather than carried per message.¶
A profile-level ASCII string bound as the first element of the segment AAD encoding (Section 4.4.2). Consuming protocols MAY define their own profiles with a distinct aad_label.¶
The sizes Nk, Nn, Nt, and Nh are fixed by the chosen AEAD and KDF rather than by the parameter set directly, as is Nb where it is defined. They are defined in Section 2.2. One configurable length defaults from Nh.¶
A positive integer of at least 16, defaulting to Nh, the length in octets of the key commitment prefix. The collision bounds for each length are given in Appendix C.4.¶
An encrypted message is built from four input values.¶
A 32-octet uniform random Content Encryption Key generated fresh for each message. The CEK is input keying material for the key schedule, not an AEAD key.¶
A per-content value of 32 uniformly random octets. The salt separates payload schedule outputs across messages even when the CEK is reused. Section 4.9.1.1 and Section 4.9.3 give the rules for generating and reusing it, and Section 6.7 analyzes the consequences of reuse.¶
The committed record of the object's configuration: an ordered list of parameter set context elements covering key derivation, AEAD operations, AAD construction, and nonce construction, which the commitment (Section 4.6) binds. The profile fixes the concrete element set and their encodings. SEAL's payload_info is specified in Section 4.4.¶
An octet string of global associated data, the G input of StartEnc in the raAE primitive (Section 3.2), binding whole-message application context such as a name, version, or policy. G defaults to the empty octet string and is never stored: a decryptor supplies it from application context, and the commitment binds it (Section 4.6).¶
The remaining symbols in Table 2 are derived values. The payload schedule computes the per-message values once from the CEK and payload_info, fixing them for the message's lifetime (Section 4.5). The commitment additionally binds G (Section 4.6). Per-segment values are defined where they are computed, in the sections Table 2 cites.¶
The KDF combiner uses a length-prefixed encoding for all of its inputs.¶
frame(x):
if len(x) <= 0xFFFE: return uint16(len(x)) || x ;; literal
else: return uint16(0xFFFF) || LH(x) ;; digest
encode(x1, ..., xn) = frame(x1) || ... || frame(xn)
¶
frame is total over every field length. A field too large to emit literally is instead prefixed by the reserved length 0xFFFF and an Nh-octet digest LH(x).¶
LH(x), the over-large-field digest, runs the cipher suite's native KDF primitive directly on x, invoking neither encode nor frame, and returns Nh octets. The two-step and one-step classes are HPKE's: a two-step KDF ([RFC9180]) uses its extract step, and a one-step KDF ([I-D.ietf-hpke-pq]) its XOF, with x carried under the label "raAE-LP-v1":¶
two-step: LH(x) = Extract("raAE-LP-v1", x)
one-step: LH(x) = XOF("raAE-LP-v1" || x, Nh)
¶
Running LH through the full KDF would re-enter frame on the over-large x and recurse. The direct native call avoids that.¶
Section 5.4 requires the construction to justify the collision resistance of the over-large-field digest. LH's is that of the cipher suite's KDF primitive at the suite's Nh. In the two-step class LH(x) is HMAC under the fixed public key "raAE-LP-v1", so a collision in LH yields a collision in the underlying hash, by the reduction Section 5.3 gives for the commit derivation. In the one-step class LH(x) is the XOF itself over a domain-separated input, so a collision in LH is a collision in the XOF at that output length.¶
The injectivity of encode, and LH's domain separation from the literal encoding, are established in Section 5.4.¶
Key material is derived through a KDF with the interface¶
KDF(protocol_id, label, ikm, info, L)¶
protocol_id: binds the output to a specific protocol version and application context.¶
label: a unique ASCII string identifying the derivation role.¶
ikm: input keying material, provided as a single octet string or an ordered list of octet strings.¶
info: context information, provided as a single octet string or an ordered list of octet strings.¶
L: the requested output length in octets.¶
This KDF binds protocol_id, a label, and inputs of any size into a derived value. frame reduces any over-large input to a fixed-size digest before the KDF sees it. Any protocol instantiates the combiner by supplying its own protocol_id, which domain-separates its derivations from those of every other protocol.¶
The KDF is built from the cipher suite's hash in one of two forms. The cipher suite's kdf_id selects which one.¶
KDF(protocol_id, label, ikm, info, L):
extract_input = encode(protocol_id, label,
...ikm)
prk = Extract(salt=protocol_id,
ikm=extract_input)
expand_info = encode(protocol_id, label,
...info, uint16(L))
return Expand(prk, expand_info, L)
¶
The notation ...x means each element of the sequence x, whether ikm or
info, is a separate argument to encode. A single octet string is the
one-element sequence containing it. An absent value is the empty
sequence and contributes no arguments. The empty sequence and the
one-element sequence whose element is the empty octet string are
distinct: the first adds nothing to the encode, the second adds one
zero-length field. A derivation with no info MUST use the empty
sequence, not the empty octet string, so that every implementation
encodes a given (protocol_id, label, ikm, info, L) tuple to the same
octets. The protocol_id appears in both the Extract salt and the
extract_input as a defensive measure, and binding in either position
suffices for domain separation.¶
The label appears in both Extract and Expand so that label binding is preserved even if one phase is weak in isolation. Implementations MAY amortize the Extract computation internally when deriving multiple outputs from the same protocol_id, label, and ikm.¶
KDF(protocol_id, label, ikm, info, L):
M = encode(protocol_id, label,
encode(...ikm), encode(...info),
uint16(L))
return XOF(M, L)
¶
The XOF-based form frames ikm and info each as a single element of one length-prefixed encode and squeezes L octets of output. protocol_id binding is via the message-side encode prefix only. In both forms the encoded input ends with uint16(L): the two-step form places it last in expand_info and the one-step form places it last in M. Within a fixed kdf_id, distinct (protocol_id, label, ikm, info, L) tuples yield distinct outputs, by the injectivity of encode (established in Section 5.4). The two forms are not mutually injective: a cipher suite's kdf_id selects one form for that suite, and payload_info commits to the kdf_id (Section 4.4), so no derivation ever compares outputs across forms. The two-step form binds ikm in Extract and info in Expand. A multi-value ikm or info is the encode of its elements, so any number of elements is admitted.¶
payload_info = [aead_id, segment_max_be, kdf_id, snap_id,
segment_commitment, nonce_mode, epoch_length_u8, salt]
¶
Element encodings for SEAL:¶
aead_id: 2 octets, uint16(id), the unsigned 16-bit AEAD identifier.¶
segment_max_be: 4 octets, uint32(segment_max).¶
kdf_id: 2 octets, uint16(id), the unsigned 16-bit KDF identifier.¶
snap_id: 2 octets, uint16(id), the snapshot authenticator identifier.¶
segment_commitment: 1 octet, 0x00 when the leaf does not bind the segment's content and 0x01 when it does (Section 4.7.1). It is a choice independent of snap_id, but not every (snap_id, segment_commitment) pair is sound, so a profile selects a valid one.¶
nonce_mode: 1 octet, the nonce construction: 0x00 for random, 0x01 for derived. A profile MAY select any valid (nonce_mode, snap_id, segment_commitment) tuple.¶
epoch_length_u8: 1 octet, uint8(epoch_length). Implementations MUST reject epoch_length >= 64 on both encode and decode.¶
salt: 32 raw octets, the per-message salt.¶
The aad_label is a profile-level constant for SEAL (see Section 4.4.2) and is not carried in payload_info. The KDF (Section 4.3) applies frame to each element of the list.¶
The segment AAD binds each segment to its context. What it must carry depends on the nonce mode: a random nonce binds neither the segment index nor the finality bit, so the AAD must, whereas a derived nonce binds both (Section 4.5.3.2).¶
segment_aad(i, is_final, A_i):
if nonce_mode is "random":
if A_i is empty:
return encode(aad_label, uint64(i), uint8(is_final))
return encode(aad_label, uint64(i), uint8(is_final), A_i)
;; derived nonce mode
if A_i is empty:
return ""
return encode(aad_label, A_i)
¶
| Random nonce mode | Derived nonce mode | |
|---|---|---|
| Index i and is_final | bound in the AAD | bound in the nonce |
| A_i, when present | appended as a fourth encode element | the sole variable element |
| A_i, when empty | omitted, not a zero-length element | segment_aad is empty, with no associated-data pass |
aad_label is the profile-chosen label from the parameter set, the index i is a big-endian 8-octet integer, and is_final is 1 for the last segment and 0 for all others. Because encode length-prefixes every element, omitting an empty A_i stays injective, so the index and finality bit remain authenticated. Any profile that defines a random-mode segment_aad MUST preserve injectivity and MUST authenticate the segment index and finality bit. aad_label is bound transitively through protocol_id (Section 6.3), so segment_aad need not repeat it for cross-profile separation.¶
More generally, the binding of the triple (i, is_final, A_i) MUST be unambiguous under one segment key, so that no two distinct triples present the same inputs to the AEAD. In random nonce mode segment_aad alone MUST injectively bind the triple, because the nonce is an independent random draw. In derived nonce mode i and is_final are bound in the nonce and A_i, when present, in segment_aad, and the (nonce, segment_aad) pair MUST jointly determine the triple (Section 4.5.3.2).¶
A_i is the caller-supplied per-segment associated data of EncSeg and DecSeg (Section 3.2), passed to the segment algorithms (Section 4.8). It MAY be empty and MAY differ per segment. Unlike a global associated data value G, which the commitment fixes at creation (Section 4.6), A_i rides the per-segment AEAD associated data and is rewritable: a rewrite can change a segment's A_i.¶
The salt is not included in the AAD because it is already bound into segment_key via the payload schedule. Different messages produce different segment keys even at the same index. Transposing two segments within a message fails because the AAD at each position encodes the expected index.¶
Because each segment binds its index and finality bit (through the AAD or the nonce, per Table 3), these truncation and extension attacks are caught by per-segment authentication alone, not by the snapshot, and a reader checks them locally before decrypting any one segment. Unauthorized truncation is detectable and Section 4.9.1.2 requires a decryptor to reject it: a highest-indexed present segment carrying is_final = 0 signals missing data. Authorized truncation by the CEK holder instead re-marks the new final segment and updates the snapshot (Section 4.9.3). Appending after a segment marked is_final = 1 either invalidates that segment on re-read or produces a ciphertext that cannot verify.¶
The per-segment AEAD binds a segment's index and finality, not its length, by the same means. A segment's ciphertext length is authenticated implicitly by the AEAD tag over the ciphertext: altering, truncating, or splicing a segment ciphertext is an AEAD forgery, not a parsing ambiguity. Because segments may be shorter than segment_max, the consuming format MUST convey each segment's ciphertext length to the decoder. A wrong length yields an AEAD rejection, never a silent accept.¶
| Derivation role | Label variable | Value |
|---|---|---|
| Commitment | commit_label | "commit" |
| Payload key | payload_key_label | "payload_key" |
| Snapshot authenticator key | snap_key_label | "snap_key" |
| Nonce base | nonce_base_label | "nonce_base" |
| Epoch key | epoch_key_label | "epoch_key" |
SEAL fixes each label to the value above. The construction formulas reference labels by variable name, so the labels are SEAL parameters rather than constants of the raAE construction. Each value is an ASCII string and is distinct from all others. These are the always-present key-schedule labels. The configured snapshot authenticator (Section 4.7.1), the plaintext-bound nonce construction (Appendix A), and hedged randomness (Appendix B) each define their own labels.¶
A message's entire key hierarchy grows from two random values. All other keys are derived deterministically through the KDF of Section 4.3.¶
Four values are derived from the CEK and per-content salt.¶
payload_info = [aead_id,
uint32(segment_max),
kdf_id,
snap_id,
segment_commitment,
nonce_mode,
uint8(epoch_length),
salt]
commitment = KDF(protocol_id, commit_label,
[CEK], [...payload_info, G], commitment_length)
payload_key = KDF(protocol_id, payload_key_label,
[CEK], payload_info, Nk)
snap_key = KDF(protocol_id, snap_key_label,
[CEK], payload_info, Nh)
;; For derived nonce mode only:
nonce_base = KDF(protocol_id, nonce_base_label,
[CEK], payload_info, Nn)
¶
G is the global associated data of Section 4.2.4, always the last element of the commitment info. It defaults to the empty octet string, one zero-length element, so every commitment derivation includes it. The other three derivations never take G.¶
An AEAD with a 96-bit nonce (Nn = 12) has, at a 2^(-32) collision-probability target, a random-nonce budget of about 2^32 encryptions per key. For rewritable content, this limit can be reached through repeated modifications to the same segments. Epoch keys partition the nonce space: each epoch key covers at most 2^r segment positions (initial writes plus rewrites), so the per-key invocation count is bounded regardless of the content's total size.¶
When epoch_length = r is specified, the segment key for segment i is an epoch key derived from the payload key:¶
segment_key(i):
epoch_index = i >> epoch_length
return KDF(protocol_id, epoch_key_label,
[payload_key],
[uint64(epoch_index)], Nk)
¶
At epoch_length = 0 the shift is the identity, so each segment has its own epoch key. At a large epoch_length every segment shares one epoch key. Implementations MUST apply this derivation at every epoch_length, including 0, and MUST NOT use payload_key directly as a segment key. Epoch keys implement the parallel external rekeying pattern of [RFC8645], adapted for random-access patterns.¶
Two nonce modes, random and derived, trade off AEAD flexibility, storage cost, and trust in a cryptographically secure pseudorandom number generator (CSPRNG). Derived mode needs misuse-resistant authenticated encryption (MRAE) unless the profile's writer discipline keeps every derived nonce unique. The chosen mode is part of the parameter set and MUST be consistent across all segments of a message.¶
The two modes differ as follows:¶
| Property | Random nonce mode | Derived nonce mode |
|---|---|---|
| Per-segment nonce stored | yes, in segment metadata | no |
| CSPRNG required | yes | no |
| AEAD requirement | any AEAD | MRAE, or any under a nonce-uniqueness discipline |
| Rewrite | fresh nonce each write | nonce fixed by index, reused on rewrite |
| Per-segment AAD | binds index and finality | empty when no A_i (index/finality in nonce) |
| Minimum nonce size Nn | no added constraint | at least 8 octets |
In random nonce mode (nonce_mode = "random"), each segment gets an independent random nonce:¶
nonce(i) = Random(Nn)¶
Each segment's nonce is stored in per-segment metadata and MUST be accessible to any reader that decrypts individual segments. This mode requires a functioning CSPRNG. The nonce collision probability across independently generated per-segment nonces follows the standard birthday bound. See Section 5.8.¶
Two optional encryptor-side hedges harden this mode against different threats. Hedged randomness (Appendix B) mixes in a long-term key and defends against a weak or predictable CSPRNG. It does not defend against CSPRNG state duplication, since identical CSPRNG output still yields identical hedged output. Only the plaintext-bound construction (Appendix A), which mixes in plaintext content, defends against state duplication. Both are implementation-local, leave the wire format unchanged, and are not separate nonce modes.¶
Derived nonce mode is intended for profiles that do not store a per-segment nonce. Nonces are derived deterministically from the payload schedule. Derived nonce mode is defined only for AEADs whose nonce size Nn is at least 8 octets. The segment index occupies the high bits of an 8-octet counter and the finality bit occupies the low bit, so the per-segment AAD need not carry either one. The XOR is applied to the last 8 octets of nonce_base, with the remaining octets unchanged:¶
nonce(i) = nonce_base[0:Nn-8]
|| (nonce_base[Nn-8:Nn]
XOR uint64((i << 1) | is_final))
¶
The encoded value (i << 1) | is_final MUST fit in the low 8 octets of nonce_base, so the segment index i MUST be less than 2^63. This caps an object at 2^63 segments, far beyond any reachable object. See Section 5.8.3 for the resulting size limit.¶
A derived nonce is fixed by the segment's index and finality bit, so re-encrypting a segment at the same index and finality repeats its nonce. The repetition is harmless when the plaintext and the associated data are unchanged, because AEAD.Encrypt is a function of its inputs: the second invocation reproduces the first one's ciphertext and tells an adversary nothing it did not already hold. With changed inputs it is not harmless. An MRAE AEAD tolerates it, degrading to equality leakage rather than plaintext recovery, within the per-segment rewrite limit given in Section 5.8. A non-MRAE AEAD does not: it leaks plaintext. So with a non-MRAE AEAD, derived nonce mode MUST be confined to a profile whose discipline never re-encrypts a segment with a different plaintext or associated data.¶
Two such disciplines matter here. A write-once profile rewrites nothing and performs no length change, so no segment is re-encrypted at all. An append-only profile re-encrypts one segment per append, the re-mark that supersedes the old terminal segment, and that re-mark carries the segment's existing plaintext and changes only the finality bit, which changes the nonce (Section 4.9.3.1).¶
Under either discipline the encryptor MUST draw a fresh salt for every object, so the nonce base differs, and MUST NOT reuse any (i, is_final) pair under that salt, including after a crash (Section 6.2).¶
Because the nonce binds index and finality, an immutable, write-once profile in derived nonce mode coincides with STREAM (Section 1.1) at the nonce-and-AEAD layer: a key-derived counter-plus-final-bit nonce over a per-segment AEAD with no associated data.¶
The commitment is a commitment_length-octet value the payload schedule derives from the CEK, payload_info, and the global associated data G (Section 4.5).¶
A reader MUST verify the commitment before decrypting any segment, deriving the expected value from the CEK, payload_info, and G and comparing it octet-for-octet with the stored value, and MUST treat a mismatch as an authentication failure for the object. A mismatch indicates a wrong key, parameter set, or G, or a corrupted header. This is the ra-CMT context commitment (Section 5.1.2). The reduction outline and bound are in Section 5.6.¶
Because the commitment is fixed at creation, G is immutable: changing it invalidates the stored commitment, while segment contents stay rewritable through the snapshot authenticator (Section 4.7).¶
G defaults to the empty octet string, so an application that supplies no context commits over an empty final element. A wrong G fails the commitment check (Section 4.9.1.2) the same way a wrong CEK does (Section 6.3).¶
The snapshot authenticator binds the current segment set into the public snapshot value. A SEAL profile selects one with snap_id.¶
A snapshot authenticator defines a per-segment input, one value per segment. It produces and checks a snapshot value over the (index, input) set and the count with two operations:¶
Produce the snapshot value over the current (index, input) set and the count n_seg.¶
Recompute the snapshot value over the current (index, input) set and the count, and accept only if it equals the stored snapshot under a constant-time comparison.¶
The per-segment input is the segment's metadata leaf, binding the segment's index and presented nonce. A CEK holder can reproduce that binding for altered content. Setting the segment commitment binds the segment's ciphertext into the leaf, so changing the ciphertext changes the snapshot value. The commitment is therefore needed where the object is shared among CEK holders, and MAY be omitted where the CEK has a single holder.¶
The segment commitment is necessary for that binding but not sufficient. An ordering is linear when a holder of the snapshot key can solve for a different segment set giving the same snapshot value, and strict when finding one is a collision problem. Under a linear ordering a snap_key holder can find such a set even when every leaf commits to its content, so content binding against a CEK holder requires the segment commitment, a strict ordering, and a snapshot authenticated outside the construction (Section 5.3).¶
Every authenticator supports the whole-object target: verify the full (index, input) set and the count. An authenticator MAY additionally support a subset target: verify that a designated subset of (index, input) pairs is included at their positions in the recorded set, without the rest. The snapshot value stays a single target-independent value of length Na, with no per-segment proof stored.¶
To change a stored object, an authenticator updates its snapshot value. It MAY provide add(i, input), remove(i, input), and set_length(n) to update the value in place from the changed segment alone, or it MAY recompute the value over the changed (index, input) set.¶
A snapshot update is not atomic against a second concurrent update, so a consuming protocol MUST serialize concurrent updates to one object. This document requires that a serialization exist and does not say how to obtain it, which remains a storage-transaction concern of the consuming protocol. Serialization is needed whether the authenticator recomputes the value or updates it in place, and only the failure differs. An authenticator that recomputes over the full set binds whatever set it read, which is neither the state before the interleaved update nor the state after. One that updates in place loses an update, when two read-modify-write cycles on the stored value interleave and the later write never observed the earlier delta. This is the concurrent case, distinct from the crash case of Section 6.8, where one update is interrupted rather than two interleaved.¶
The scheme's RewriteSeg and SnapVerify (Section 3.3) are realized from these operations. SnapVerify(S, segments, snapshot) checks a stored snapshot:¶
Let n_seg be the number of present segments.¶
Verify that the present segments occupy exactly the indices 0..n_seg-1, each once.¶
Call verify(snapshot) over the present (index, input) set and the count n_seg.¶
Finality is not checked in SnapVerify: is_final is authenticated by the segment AEAD at decryption, and Section 4.9.1.2 rejects a present set whose highest-indexed segment does not carry is_final = 1.¶
A duplicate or missing index is caught only by step 2, because an authenticator's verify MAY accept a malformed index multiset. An implementation MUST NOT skip the index-set check.¶
A conforming authenticator's snapshot() MUST bind both the (index, input) set and the count n_seg, so a verifier detects any added, dropped, reordered, re-marked, or otherwise altered segment, and the snapshot value MUST be unforgeable by a party without the authenticator's key, under that authenticator's security assumption.¶
The snapshot value is a fixed-length value of Na octets, with Na = 0 meaning no value.¶
A segment's finality is a function of its index and the count:¶
is_final(i) = 1 if i = n_seg - 1, else 0¶
It is carried by that segment's input. Because finality follows the count, changing n_seg re-marks the boundary segment on its own, so rewrite, extend, and truncate are one update over the segments whose input changes:¶
Under the current count, remove each changed segment's current input (remove(i, input)). The current n_seg fixes which segment is terminal, so each removal carries that segment's current finality.¶
Set the length to the new count (set_length(n)).¶
Under the new count, re-encrypt each surviving changed segment under its new finality and add its new input (add(i, input)). New segments are added here. Discarded segments are not added back.¶
Call snapshot(), which recomputes the snapshot value over the new count and segment set.¶
Each operation is an instance of that shape over different segments:¶
| Operation | Count | Inputs removed | Inputs added | Terminal re-mark |
|---|---|---|---|---|
| Rewrite | unchanged | the changed segment | the changed segment | none |
| Extend by k | n_seg to n_seg+k | old terminal | old terminal and k new | old final, 1 to 0 (none if empty) |
| Truncate to m | n_seg to m | the tail and old terminal | new terminal | new final, 0 to 1 (none if m=0) |
This sequence uses remove, the mutable-authenticator path. One that omits remove instead resets and re-adds the surviving segments to rebuild the value.¶
This selection configures no snapshot authenticator: no snapshot value is produced (Na = 0), snapshot() and verify() are no-ops, and a mutable object rests on per-segment authentication alone. Such a profile MUST NOT produce the empty object (n_seg = 0). With no snapshot value to bind the zero count, a truncation that removed every segment would be indistinguishable from a legitimate empty object, so a valid object always has at least one segment (Section 6.6).¶
The construction defines the per-segment subroutines EncryptSegment, DecryptSegment, and RewriteSegment. In this random-access construction, is_final is an explicit input to each subroutine. Full-message encryption derives it from the total segment count n_seg, while random-access decryption and rewrite callers supply it directly. The full-message procedures (Section 4.9.1.1, Section 4.9.1.2, Section 4.9.3) and the rewrite procedure (Section 4.9.2.1) compose them.¶
Each subroutine is the cryptographic core of one operation of the base or extended raAE interface (Section 3.2, Section 3.3), refining Table 1 to the signature level. The primitive's position becomes the explicit arguments i and is_final, and its state is the payload schedule, which the segment_key(i), nonce(i), and segment_aad calls below read from context rather than take as an argument. A segment's plaintext is P_i, and its stored record is stored_i = (nonce_metadata_i, ct_i, tag_i). leaf(i) is the segment's leaf (Section 4.7.1), and new_leaf(i) the leaf of a rewritten segment.¶
| Subroutine | Realizes | Composed by |
|---|---|---|
| EncryptSegment | the encryption core of EncSeg | Section 4.9.1.1 and Section 4.9.3, which add the snapshot bookkeeping |
| DecryptSegment | DecSeg | Section 4.9.1.2 |
| RewriteSegment | the encryption core of RewriteSeg | Section 4.9.2.1 and Section 4.9.3, which add the snapshot bookkeeping |
These subroutines emit ct_i and tag_i as separate fields, with ct_i of length len(P_i), at most segment_max. DecryptSegment splits stored_i using the ciphertext length the consuming format conveys (Section 4.4.2).¶
In every subroutine, i is the segment index (a non-negative integer), is_final is 1 for the last segment and 0 for all others, A_i is the caller-supplied associated data for segment i, and P_i is the segment plaintext. The subroutines call segment_key(i) and nonce(i) as defined in Section 4.5, with nonce(i) following the chosen nonce_mode, and segment_aad(i, is_final, A_i) as defined in Section 4.4.2.¶
nonce_metadata_i denotes the stored nonce metadata for segment i: nonce(i) when nonce_mode is "random", and empty when it is "derived". In random mode DecryptSegment reads its nonce from nonce_metadata_i, and in derived mode it recomputes nonce(i).¶
EncryptSegment(i, is_final, A_i, P_i): key = segment_key(i) nonce = nonce(i) aad = segment_aad(i, is_final, A_i) C_i = AEAD.Encrypt(key, nonce, aad, P_i) split C_i into ct_i and tag_i return (nonce_metadata_i, ct_i, tag_i)¶
AEAD.Encrypt and AEAD.Decrypt are the interfaces of [RFC5116] Sections 2.1 and 2.2. For an AEAD whose ciphertext places the tag in its final Nt octets, ct_i is the first len(P_i) octets and C_i = ct_i || tag_i.¶
The caller supplies the segment's leaf to the snapshot authenticator (Section 4.7.1). EncryptSegment itself does no snapshot work.¶
DecryptSegment returns the segment plaintext, or a decryption error:¶
DecryptSegment(i, is_final, A_i, stored_i):
nonce_metadata_i, ct_i, tag_i = stored_i
key = segment_key(i)
nonce = nonce_metadata_i or nonce(i)
aad = segment_aad(i, is_final, A_i)
C_i = ct_i || tag_i
P_i = AEAD.Decrypt(key, nonce, aad, C_i)
(on verification failure, return a decryption error)
return P_i
¶
RewriteSegment assumes the caller has already verified the commitment or otherwise confirmed it holds the correct CEK. A rewrite under the wrong key produces unreadable ciphertext and corrupts the snapshot authenticator's state. The error is detectable only on a subsequent read.¶
RewriteSegment(i, is_final, A_i, new_P_i) is defined as EncryptSegment(i, is_final, A_i, new_P_i): a rewrite is a fresh encryption of the new plaintext new_P_i. The output record new_stored_i replaces the stored record for segment i.¶
RewriteSegment produces the replacement record new_stored_i only. The caller updates the snapshot authenticator with the old and new leaves, calling remove(i, leaf(i)) then add(i, new_leaf(i)) (Section 4.9.2.1).¶
The top-level algorithms compose the segment subroutines (Section 4.8) over complete messages. They call the snapshot-authenticator operations of Section 4.7 (snapshot() and verify() in every profile, and add(i, input), remove(i, input), and set_length(n) in a mutable or append-only profile) without referring to how any is computed. The profile's selected authenticator realizes those operations (Section 4.7.1).¶
These procedures do not expose per-segment associated data, so they pass an empty A_i to each subroutine. An application that needs per-segment A_i calls the subroutines directly.¶
Encryption and decryption are available in every SEAL profile. They write a message once and read individual segments. Neither rewrites a stored segment. When the profile configures no snapshot authenticator, the snapshot steps below are omitted, and a reader relies on per-segment authentication alone.¶
Encryption runs in two phases. The first derives the per-message schedule and writes the commitment. The second encrypts each segment independently, feeds its input to the snapshot authenticator, and then calls snapshot() to bind the whole (index, input) set and the count. Because the segments are independent, the second phase may encrypt them in any order or in parallel. The snapshot calls mutate one value under a single writer, so encryption is never the concurrent case that Section 4.7.1 requires a protocol to order.¶
An application that writes a new message MUST generate a fresh, uniformly random salt. Given a CEK, that salt, a parameter set, G, and a plaintext P split into segments P_0 through P_{n_seg-1}:¶
derive commitment, payload_key, snap_key from the payload schedule
(and nonce_base, in derived mode)
store commitment
for i in 0 .. n_seg-1:
is_final = 1 if i = n_seg-1, else 0
stored_i = EncryptSegment(i, is_final, empty, P_i)
store stored_i
if snapshot authentication is enabled:
snapshot = snapshot()
store snapshot
¶
A zero-length plaintext produces a single segment of length 0 with is_final = 1.¶
Opening a segment is a local operation: check the commitment to reject a wrong key early, then AEAD-decrypt the one segment. This yields per-segment authenticity over the ciphertext core, the segment index, and the finality bit. Under a snapshot authenticator, whole-set authenticity is a separate and stronger check that requires the full present segment set. It detects any altered segment set, including same-index rollback to an older sibling (Section 4.7.1). A reader holding only one segment gets per-segment authenticity but MUST NOT claim snapshot integrity until it runs SnapVerify (Section 4.7). A profile with no snapshot authenticator defines no snapshot, so per-segment authenticity is the whole integrity guarantee.¶
Given the CEK, salt, parameter set, G, segment index i, finality status, and the stored ciphertext segment for index i:¶
derive commitment from CEK, salt, and G
derive payload_key, snap_key from CEK and salt
if stored commitment != derived commitment: return error
P_i = DecryptSegment(i, is_final, empty, stored_i)
(return its error on failure)
if snapshot authentication is enabled and the full set is available:
if SnapVerify(snapshot) fails: return error (do not deliver P_i)
return P_i
¶
Here SnapVerify is the three-argument SnapVerify(S, segments, snapshot) of Section 3.3, with S the derived schedule and segments the present set, each carrying the authenticator's per-segment input (Section 4.7.1).¶
Whole-object SnapVerify needs the leaf of every segment (Section 4.7.1), so a reader that streams segments in order cannot gate their release on whole-object snapshot integrity without first buffering those leaves or using a layout that carries them in a header. Where the authenticator supports a subset target, a reader can instead verify a subset it holds against the snapshot without the leaves of the other segments. Delivering on per-segment authenticity alone forgoes snapshot freshness for that segment (Section 6.6). A streaming reader likewise cannot rule out truncation before the end of the stream: plaintext delivered before the highest-indexed segment verifies under is_final = 1, and before SnapVerify passes where a snapshot authenticator is configured, MUST be treated as unverified with respect to completeness.¶
A decryptor MUST reject a truncated object: one whose highest-indexed present segment carries is_final = 0 is incomplete, and the decryptor MUST fail rather than return the partial plaintext.¶
The check works because the application supplies the segment count n_seg, from which the decryptor derives the finality each segment must carry (Section 4.8). It then verifies the final segment under is_final = 1. A truncated object, whose last surviving segment was written as non-final, fails that verification. The finality bit is bound per segment and unforgeable without the CEK, through the nonce or the segment AAD per the nonce mode (Section 4.4.2).¶
Under a write-once profile no segment ever carried an earlier terminal form, so an adversary cannot present a valid is_final = 1 segment at a truncated boundary: it cannot re-mark a non-final segment, and this check detects every dropped trailing segment. A profile that changes length re-marks a terminal segment (Section 4.9.3), so one segment may have carried is_final = 1 under an earlier nonce. An adversary can replay that retained terminal ciphertext at the shorter count, which this check alone accepts. SnapVerify over the complete segment set then rejects the replay. The mutable profile requires a snapshot authenticator, so SnapVerify always runs there (Section 6.6).¶
Authorized truncation by the CEK holder re-encrypts the new final segment under is_final = 1 with a fresh nonce (Section 4.9.3), so a legitimately truncated object still ends with is_final = 1 and verifies. This check needs no snapshot authenticator.¶
Truncation to an empty object is covered too. Without a snapshot authenticator a valid object has at least one segment, so a decryptor MUST reject one with none. With a snapshot authenticator the snapshot binds the count, making the empty object (n_seg = 0) a distinct authenticated state (Section 6.6).¶
On a commitment mismatch the implementation MUST NOT proceed with decryption, SHOULD zeroize derived key material, and MUST return an error. The SnapVerify gate in the procedure above runs whenever a snapshot authenticator is configured and the full present set is available, for example a full read or an integrity audit.¶
Decryption can fail in up to three distinguishable ways:¶
| Failure | Cause |
|---|---|
| Commitment mismatch | wrong key or parameters |
| AEAD verification failure | corrupted or tampered segment |
| Snapshot mismatch | segment set or count modified (snapshot authenticator only) |
Implementations SHOULD report these as distinct error conditions for local diagnosis, but SHOULD return a single opaque error to untrusted callers to avoid leaking an oracle (for example, distinguishing "wrong key" from "tampered segment" over a network).¶
Rewriting is available only in a mutable profile, which requires a snapshot authenticator. Length change is a separate operation (Section 4.9.3), available in both directions in a mutable profile and in the append direction alone in an append-only profile (Section 4.9.3.1). The procedure below is written against the in-place update path (add, remove, set_length; Section 4.7.1). Under an authenticator that recomputes instead, the remove, add, and set_length calls in this procedure collapse into a single snapshot() over the updated (index, input) set, and the operations are otherwise identical. A caller that cannot recover a re-encrypted segment's plaintext cannot perform it.¶
A rewrite touches exactly one segment's ciphertext, and no other segment is read or decrypted (Section 4.7). In-place update trusts, rather than re-verifies, the stored snapshot state, whereas an authenticator that recomputes rebuilds the value from the stored per-segment leaves and trusts none.¶
The procedure is:¶
read leaf(i) new_stored_i = RewriteSegment(i, is_final, empty, new_P_i) remove(i, leaf(i)) add(i, new_leaf(i)) snapshot = snapshot() store new_stored_i, snapshot¶
In derived nonce mode the nonce for segment i is deterministic, so a rewrite reuses the same nonce under a key whose derivation context is identical. This is safe because a mutable profile that selects derived nonce mode requires an MRAE AEAD, which tolerates the reuse (Section 4.5.3.2) within the per-segment rewrite limit (Section 5.8.4).¶
The stored snapshot cannot be validated during an in-place rewrite: SnapVerify recomputes the authenticator from every segment's tag and so requires reading all tags (Section 4.7). Because the rewrite trusts the stored snapshot state, the writer MUST take it from locally trusted storage or run SnapVerify first. Otherwise a storage adversary can roll the stored snapshot back and have the writer rebuild on it, laundering the rollback into a snapshot that verifies going forward (Section 6.6). Full validation is a separate operation performed with access to all tags, for example on a full read or an integrity audit.¶
An application that holds the CEK MAY change a stored message's length in either direction: appending new segments, or truncating to a prefix. The two directions share one shape. Exactly one retained segment, the old or the new terminal one, is re-encrypted to move the finality bit, with none re-encrypted when appending from the empty object or truncating to it. Every other changed segment is removed or added by tag without re-encryption, and the snapshot is updated over the new count (Table 6). Both reuse the existing salt: an application extending an existing message MUST reuse the salt from the object it extends. A fresh salt would change every payload schedule output and force re-encryption of the retained segments.¶
def Append(P_new):
k = len(P_new)
t = n_seg - 1
if n_seg > 0:
P_t = DecryptSegment(t, 1, empty, stored_t)
new_stored_t = RewriteSegment(t, 0, empty, P_t)
remove(t, leaf(t))
set_length(n_seg + k)
if n_seg > 0:
store new_stored_t
add(t, new_leaf(t))
for j in n_seg .. n_seg+k-1:
is_final = 1 if j == n_seg+k-1 else 0
stored_j = EncryptSegment(j, is_final, empty, P_new[j-n_seg])
store stored_j
add(j, leaf(j))
store snapshot()
def Truncate(m):
t = m - 1
if m > 0:
P_t = DecryptSegment(t, 0, empty, stored_t)
new_stored_t = RewriteSegment(t, 1, empty, P_t)
remove(t, leaf(t))
for j in m .. n_seg-1:
remove(j, leaf(j))
set_length(m)
if m > 0:
store new_stored_t
add(t, new_leaf(t))
discard segments m .. n_seg-1
store snapshot()
With in-place update each add and remove takes the segment's stored tag, which already encodes that segment's is_final bit, so an implementation never tracks finality separately.¶
A botched in-place update corrupts the snapshot authenticator's state silently, surfacing only at a later SnapVerify. An implementation MAY run SnapVerify over the new segment set immediately after a length change to catch this. A botched update yields a value that the from-scratch recomputation will not match, turning a silent corruption into a caught error. An authenticator that recomputes rebuilds the snapshot from the stored per-segment leaves on every update, so it has no analogous silent-corruption failure mode.¶
Both operations count against the same per-CEK AEAD usage budget as initial encryption (Section 5.8). Each re-encrypts one terminal segment, except a truncate to the empty object (m = 0), which re-encrypts none. In derived nonce mode the nonce for the re-marked segment is fixed by its index and finality bit. Re-marking flips the finality bit, so this nonce differs from the segment's previous one, but repeated length changes in both directions can make the same (index, finality bit) pair recur, and only an MRAE AEAD tolerates the resulting nonce reuse (Section 4.5). An append-only profile admits no truncation, so no pair recurs and any AEAD is safe (Section 4.9.3.1).¶
Applications that do not need length changes MAY forbid them. The salt-reuse requirement applies only when a length change is performed. An application that re-encrypts the entire content can generate a fresh salt and is not bound by the constraints in this section.¶
An append-only profile admits the append direction of this section (Figure 5) and forbids truncation.¶
An encryptor under an append-only profile MUST NOT rewrite a segment with a different plaintext or associated data, and MUST NOT reduce n_seg.¶
The first rule is what lets the profile pair a derived nonce with any AEAD, including a non-MRAE one. At a fixed index and finality the key and the nonce are already fixed by the payload schedule, so the plaintext and the associated data are the only inputs that can differ, and the rule forbids exactly those. No nonce ever carries two different plaintexts, which is the condition Section 4.5.3.2 sets for a non-MRAE AEAD.¶
The re-mark satisfies the first rule without an exception: it changes only the finality bit, not the plaintext or associated data (Section 4.5.3.2).¶
The second rule makes the profile append-only rather than non-rewriting. It forbids the truncate direction, so a stored object only grows, and an index the object has spent is never freed for a later append to fill with different content.¶
An append re-encrypts one retained segment, so each segment is encrypted at most twice, and an epoch key covers at most 2^(epoch_length + 1) segment encryptions (Section 5.8.3).¶
Because the re-mark preserves the segment's length, an append to an object whose terminal segment is short leaves a short interior segment, which a layout with uniformly sized interior segments cannot represent. A consuming protocol using this profile supplies a serialization that conveys each segment's ciphertext length (Section 4.4.2).¶
The formal proofs are not reproduced here. The ra-ROR and ra-CMT notions are taken unchanged from [FLRR25], which proved them for a construction that instantiates the same component contracts other than the snapshot authenticator (Section 5.4). SEAL's analysis of those notions follows the same structure. Snapshot integrity is the one notion this document defines. The configured authenticator's definition argues its snapshot-integrity bound (Section 5.7), and integrating that argument into a combined proof is deferred to the companion proof [SEALPROOFS] (in preparation). The limits of Section 5.8 are operational deployment bounds, not proof obligations.¶
raAE has two security tiers. The base tier comprises the notions inherited from [FLRR25]: ra-ROR, ra-CMT, and its position-choosing variant ra-CMT-p, summarized here. The extension tier adds one notion, snapshot integrity, defined by this document. A construction MAY provide the base tier only. Advantage definitions and proofs for ra-ROR, ra-CMT, and ra-CMT-p are in [FLRR25].¶
ra-ROR (random-access real-or-random) is the joint confidentiality and authenticity notion for raAE in a multi-user, nonce-respecting setting. An adversary with adaptive encryption and decryption access across many key instances, sessions, and arbitrary positions cannot distinguish real ciphertexts and headers from random, and cannot make an out-of-context ciphertext segment decrypt. The game is defined in [FLRR25].¶
The encryption core of the extension operations is ordinary EncSeg. A RewriteSeg produces its replacement ciphertext as a repeated EncSeg at the same position, and appending is EncSeg at fresh positions plus one RewriteSeg of the old final segment.¶
In random nonce mode these draw fresh nonces and stay within the nonce-respecting ra-ROR game. In derived nonce mode the analysis turns on whether the profile permits a rewrite in place.¶
Where it does, a rewrite repeats the position's derived nonce with new content. That is a deliberate nonce-repeating query, and the underlying MRAE AEAD handles it under the equality-leakage relaxation documented for derived nonce mode (Appendix C.5), not the unmodified nonce-respecting game.¶
Where it does not, no query repeats a position with new content. The write-once profile re-encrypts nothing beyond an identical replay of an interrupted write. The append-only profile re-encrypts only at the append's RewriteSeg, which flips the finality bit and so writes a position the initial write did not use, and at an identical replay. Both profiles bind every position to one set of inputs (Section 4.9.3.1), so their repeated queries carry the inputs of the query they repeat. A reduction answers such a query from the first one's output, and both profiles stay in the nonce-respecting game and need no MRAE AEAD (Section 4.5.3.2).¶
These operations also maintain a snapshot value. That value is outside the [FLRR25] syntax. It is governed not by ra-ROR but by the snapshot integrity notion of Section 5.1.4.¶
ra-CMT (random-access context commitment) is the segment-level commitment notion: no single ciphertext segment decrypts successfully under two different decryption contexts (key, nonce, global associated data, and per-segment associated data). In this position-respecting notion the two contexts share a position. Because the scheme is random access, committing one segment's context commits the full ciphertext's context. ra-CMT is defined in [FLRR25].¶
ra-CMT is distinct from the per-AEAD notions CMT-1 and CMT-4 of [RFC9771]: a scheme can achieve ra-CMT through an external commitment mechanism even when its underlying AEAD commits to nothing. The Invisible Salamanders attacks ([DGRW18]) show what a non-committing AEAD permits, and [ADG22] frames it as a key-commitment failure with fixes.¶
ra-CMT-p is the position-choosing variant of ra-CMT: the two decryption contexts need not share a position, so the adversary binds a segment's content to a position of its choice.¶
Under honest keys a construction inherits ra-CMT-p from the underlying AEAD's commitment. Against a holder of the CEK it does not follow from per-segment AEAD alone: a snapshot authenticator restores it by committing each segment's content and ordering segments strictly, with the snapshot value authenticated outside the construction (Section 5.3, Appendix C.4).¶
Per-segment AEAD authenticates a segment at its position, but it does not authenticate the current object snapshot: which set of segments belongs to the current stored object state. An adversary with write access to stored segments can silently substitute an old valid same-index segment or delete a segment, and per-segment AEAD verification at each presented segment still passes. (A segment moved to a different index fails its AEAD check through the index binding of Section 4.4.2.)¶
Snapshot authentication covers that set. The resulting notion is snapshot integrity. Its interface and requirements are in Section 4.7.1, and its general bound is in Section 5.7. A combined formal treatment with the ra-ROR framework is in preparation ([SEALPROOFS]).¶
A construction MAY omit snapshot integrity, providing only the per-segment guarantees and ra-CMT. SEAL provides it under a snapshot authenticator and omits it otherwise.¶
raAE generalizes AEAD to multi-segment content with arbitrary-order encryption. A single-segment raAE is an AEAD.¶
nOAE2 is the nonce-based online authenticated encryption notion for schemes that encrypt in order and support random-access decryption. Hoang and Shen introduced nOAE2 in their analysis of Tink Streaming AEAD ([Tink]), where they also show STREAM satisfies it. ra-ROR is strictly stronger: every ra-ROR-secure scheme is nOAE2-secure when used in order, but nOAE2-secure schemes can fail when encryption order is arbitrary ([FLRR25]).¶
CMT-1 and CMT-4 ([RFC9771]) are per-AEAD commitment notions. ra-CMT is the corresponding notion for segmented random-access schemes. See Section 5.1.2.¶
Rewrite safety and extension safety are corollaries of the notions above, not additional notions. Replacing a segment in place is an EncSeg at an already-used position together with a snapshot update. In random nonce mode it adds no adversary capability beyond ra-ROR (Section 5.1.1) and snapshot integrity (Section 5.1.4). In derived nonce mode the rewrite repeats the segment nonce, so it MUST use an MRAE AEAD (Section 4.5.3.2), and it adds the equality-leakage relaxation that derived nonce mode already documents (Appendix C.5). Each rewrite also counts against the per-key AEAD usage budget (Section 5.8).¶
Appending and truncating likewise add no algorithm (Section 4.9.3) and no notion. An adversary's ability to add, drop, reorder, or re-mark a terminal segment is exactly the snapshot integrity adversary of Section 5.1.4, and detection follows from the index and finality binding of Section 4.4.2 together with snapshot verification. Rollback of the whole object to an earlier honest (state, snapshot) after truncation is the freshness case that snapshot integrity excludes (Section 6.6). The application supplies it.¶
The adversary model is that of the ra-ROR game (Section 5.1.1, [FLRR25]). In the construction's terms: the adversary can observe all ciphertexts and content metadata, tamper with individual segments or their ordering, replace the snapshot value, and attempt decryption with chosen keys. The adversary does not know the CEK, any derived key, or the per-content salt. The confidentiality and integrity bounds assume the CEK is secret and uniform, the single-owner regime: an application MUST supply a CEK that is either generated uniformly at random or derived so that it is computationally indistinguishable from uniform, such as the output of a KDF keyed by a secret. When multiple messages share a CEK, the per-content salt separates payload schedule outputs across messages under the PRF assumption. The multi-message advantage grows linearly in the number of messages.¶
The commitment notions ra-CMT and ra-CMT-p (Section 5.6) also cover a shared-CEK regime, where the adversary holds the CEK. A CEK holder defeats the per-segment AEAD: it holds the key, so it can decrypt and forge tags at will, and confidentiality and per-segment integrity do not bind it.¶
One binding can still hold against a CEK holder, under three conditions that each close a distinct gap. The snapshot value MUST be authenticated outside the construction, because a CEK holder derives snap_key and can otherwise forge any snapshot value directly. The segment commitment MUST be set: without it a segment's leaf binds its ciphertext only through the AEAD tag, which a CEK holder forges for altered content. Setting it adds a collision-resistant commitment to the ciphertext that holding the CEK does not defeat. The authenticator's ordering MUST be strict: under a linear ordering a snap_key holder can present a different segment set with the same snapshot value even when every leaf commits to its content, and only a strict ordering makes finding one a collision problem. A snapshot meeting all three binds each segment's content to its position against a CEK holder.¶
The persistent state of an raAE object, and the party that owns each item, is:¶
Secret key-management state. It is the root secret for the object.¶
Stored object metadata. It separates objects under one CEK.¶
Profile or object metadata. It fixes suite, maximum segment size, and epoch policy.¶
Stored object metadata. It rejects wrong key or parameter context before decryption.¶
Storage-format state. It holds AEAD tags, the per-segment nonces in random nonce mode, and, when the segment commitment is set, each segment's content commitment (Section 4.7.1).¶
Stored object metadata. It authenticates the segment set and segment count.¶
Consuming-protocol state. It detects whole-object rollback.¶
The security argument relies on three distinct assumptions about the KDF. For derivations whose input keying material is secret and uniformly random, including payload_key, snap_key, nonce_base, epoch_key, and the snapshot authenticator's keyed derivations, the KDF is assumed to be a multi-user PRF with domain separation by protocol_id and label.¶
For commitment, the function¶
Commit(CEK, payload_info, G, L) =
KDF(protocol_id, "commit", [CEK], [...payload_info, G], L)
¶
is additionally assumed to be collision resistant over adversarially chosen tuples (CEK, payload_info, G), with G empty by default. PRF security alone is not sufficient for this property, because the commitment adversary may choose the CEK and context values.¶
For the plaintext-bound nonce construction, the plaintext digest step targets collision resistance, not PRF security.¶
The bounds below rely on properties of the construction's components, namely the KDF framing and the snapshot authenticator. The requirements those components must satisfy are obligations on the raAE construction, not part of the raAE primitive of Section 3.¶
A profile selects the concrete framing for every derivation surface: the KDF input assembly (Section 4.5), the payload_info construction (Section 4.2), the segment AAD (Section 4.4.2), and the snapshot authenticator's keyed inputs (Section 4.7). The requirements below are the contract that any such framing MUST meet. SEAL is one such framing (Section 4.3, Section 4.4).¶
KDF outputs MUST be computationally indistinguishable from random to an adversary that does not know the ikm, assuming the underlying hash or PRF is a secure pseudorandom function.¶
A construction encodes the tuple (protocol_id, label, ikm, info, L) into the underlying primitive input, and that encoding MUST be injective: distinct tuples map to distinct primitive inputs. This is a requirement on the encoding, not on the KDF, which need not itself be injective. ikm and info MUST be unambiguously separable in the encoded input, whatever the number of elements each contains. The two-step form binds ikm and info in its separate Extract and Expand inputs. The one-step form frames ikm and info each as a single encode element. Where the encoding admits inputs too large to encode literally, it MAY substitute a fixed-length digest of the over-large field. The encoding is then injective only up to the collision resistance of that digest, which the construction MUST justify and domain-separate from the literal encoding (Section 4.3).¶
Each derivation role, including commitment, payload key, snapshot authenticator key, nonce base, epoch key, segment nonce, hedged key, and the authenticator's internal roles, MUST use a distinct label string within each protocol version.¶
The protocol_id MUST appear as a distinct component of the KDF primitive input. Different application protocols using the same AEAD and KDF MUST use different protocol_id values to ensure that derived values from one protocol cannot be confused with those from another.¶
The requested output length L MUST be part of the KDF primitive input. This prevents attacks in which an adversary attempts to use a truncated version of a longer derived output as a valid shorter one.¶
Cross-role isolation follows from label uniqueness and PRF security of the underlying primitive: a KDF output for one derivation role, such as payload_key, is computationally independent of the output for any other role, such as snap_key, even when derived from the same CEK and payload_info.¶
The construction MUST bind the current segment set and the segment count into a public snapshot value that an adversary without the authenticator's secret key cannot forge for a modified segment set or count, even after observing snapshot values and their deltas across rewrites. How the value is computed, and whether a single-segment change updates it in place or rebuilds it, is the authenticator's choice. SEAL's snapshot authenticators are in Section 4.7.¶
SEAL's construction (Section 4) satisfies these requirements. Its KDF (Section 4.3), keyed by the CEK, is modeled as a pseudorandom function. Its encode framing (Section 4.3) is injective and places protocol_id and the output length L in the primitive input, so injectivity, protocol binding, and output-length commitment hold. SEAL's labels (Table 4, each configured snapshot authenticator's own labels, the plaintext-bound construction's labels in Appendix A, and the hedged-randomness label in Appendix B) are distinct, giving label uniqueness and, with PRF security, cross-role isolation. Each configured snapshot authenticator satisfies the snapshot authenticator requirement, with its forgery argument accounted in Section 5.7.¶
SEAL's encode framing meets the injectivity requirement. Its frame/encode construction, the LH digest for over-large fields, and LH's domain separation from the literal encoding are defined and justified in Section 4.3. SEAL's own derived and fixed fields (the payload_info elements, the commitment, the leaf, and the uint-encoded scalars) are all at most 0xFFFE octets, so for them frame is byte-identical to a plain 2-octet length prefix. The two caller-supplied octet strings, G (Section 4.2.4) and A_i, are unbounded, and a field longer than 0xFFFE octets is bound as 0xFFFF || LH of it. Cross-protocol separation comes from protocol_id in the KDF inputs and from aad_label in the segment AAD, not from LH (Section 4.3).¶
SEAL's additional components, the epoch keys and the snapshot maintained across rewrite and length change, are analyzed in Section 5.5 and Section 5.7.¶
The construction targets ra-ROR security (Section 5.1.1). The ra-ROR advantage bound, its adversary parameters, and the per-term analysis are in Appendix C, with the full proof in preparation ([SEALPROOFS]).¶
Ciphertext cores are indistinguishable from random under the ra-ROR definition, up to the public length leakage, the equality leakage allowed by derived nonce mode, and public metadata exposed by the consuming format (file-level headers, stored nonces in random mode, the salt, and the commitment). Each segment AEAD ciphertext binds the segment to its own index and finality bit (through segment_aad in random nonce mode and through the nonce in derived nonce mode) so an adversary cannot modify or substitute a segment under a given index, or flip its finality bit, without causing an AEAD verification failure on that segment. Same-index rollback to an older valid segment within the same message is not detected by per-segment AEAD alone. This requires the snapshot verification of Section 4.7.¶
Three quadratic floors bound the multi-message advantage:¶
salt collision q_m^2 / 2^256 all suites
epoch-key collision n_ep^2 / 2^128 128-bit-key suite
nonce collision q^2 / 2^(8*Nn + 1) random nonce mode
q^2 / 2^97 at Nn = 12
¶
In derived nonce mode the salt-collision floor is the only one for the 256-bit-key suites. The 128-bit-key suite also has the epoch-key collision floor, which dominates (Appendix C.2). In random nonce mode, with or without the plaintext-bound hedge, the nonce-collision floor applies. Here q is the number of segment encryptions under one key, and Nn is the nonce length in octets. The block-size birthday term is derived in Section 5.8.¶
Segment size enters the analysis only through the forgery and block-size terms, both bounded in Section 5.8. It does not affect the confidentiality or commitment terms. A segment's length is authenticated implicitly by its AEAD tag, not through segment_aad or the nonce, so the analysis assumes the consuming format conveys each segment's true ciphertext length. Segments shorter than segment_max consume no more than their share of the per-key budget and need no separate accounting.¶
raAE's commitment target is ra-CMT (Section 5.1.2), not the per-AEAD CMT-1 ([RFC9771]): the commitment binds the CEK and payload_info, which together carry the full parameter context affecting key derivation, AEAD operation, and AAD and nonce construction.¶
ra-CMT relies on the collision resistance of the commitment derivation map over the tuple (protocol_id, "commit", CEK, payload_info, G, commitment_length), with G empty by default. PRF security alone does not suffice, because the commitment adversary may choose the CEK and the context values (Section 4.5). SEAL sets commitment_length = Nh, so the commitment offers about 2^128 collision-search work at Nh = 32 and about 2^256 at Nh = 64. The reduction outline for this assumption and the collision quantities for a general commitment_length are in Appendix C.4.¶
The commitment bounds the ra-CMT variant in which the positions match (Section 5.1.2). It does not bind the segment position. Position binding for the ra-CMT-p variant is inherited from the underlying AEAD's commitment level. Because the SEAL AEADs are not relied upon to provide it, position confusion under an adversarially chosen key is out of scope, consistent with [FLRR25]. A malicious encryptor that controls the CEK can craft a single ciphertext segment that opens at two positions, and the per-segment AEAD tag does not prevent it.¶
A snapshot authenticator that sets the segment commitment over a strict ordering, with the snapshot authenticated outside the construction, restores ra-CMT-p against adversarially chosen keys: the collision-resistant leaf commitment and the strict ordering together pin each segment's ciphertext to its index even against a CEK holder (Section 4.7.1, Section 5.3). It applies only where a profile selects that authenticator and a consuming protocol authenticates the one snapshot value. The base construction pins position no more strongly than the AEAD commits under an adversarial key.¶
The per-segment associated data A_i is likewise outside the commitment: SEAL binds A_i through segment_aad and the AEAD tag for that segment. Contexts that differ only in A_i are therefore not commitment collisions. Their separation relies on AEAD commitment for the segment AAD. A_i is also rewritable across rewrites and is not bound at the snapshot level. An application that needs it bound there MUST bind it externally. The exact accounting of this term in the ra-CMT reduction is deferred to Appendix C.¶
A deployment that must defend against a malicious encryptor who controls the CEK can close the adversarial-key position gap by one of two changes. These are non-normative. SEAL adds none of them to the base construction.¶
A context-committing AEAD, so that each segment ciphertext commits to its full decryption context rather than relying on the external commitment over (CEK, payload_info, G).¶
A per-segment collision-resistant commitment over the segment's position and ciphertext, on the order of 16 to 64 octets per segment, bound alongside the segment so that no segment opens at two positions.¶
Both changes act in the base construction. A snapshot authenticator with the segment commitment set over a strict ordering (Section 4.7.1) is a separate, conditional route: it leaves the base construction unchanged and closes the gap only where a profile selects it and a consuming protocol authenticates the one snapshot value.¶
None of the SEAL AEADs is relied upon to provide CMT-1 or CMT-4 ([RFC9771]). In particular AES-256-GCM-SIV is not key-committing, with known attacks such as partitioning oracles. raAE obtains ra-CMT context commitment from the external commitment over (CEK, payload_info, G) regardless of the AEAD, provided the commitment check is not bypassed and payload_info carries the full parameter context (Section 5.1.2).¶
With commitment_length = Nh and Nt = 16, ra-CMT is bounded by the commitment collision resistance, and ra-CMT-p adds the per-segment AEAD commitment term. The ra-CMT collision bound is derived in Appendix C.4, and the ra-CMT-p bound is stated there as a sum. The two key regimes differ. Under honestly generated keys the idealized per-pair tag-forgery floor is 2^(-128) because Nt = 16, and concrete per-AEAD authenticity bounds degrade with segment length (Section 5.8). Under an adversarially chosen CEK the position term is the AEAD's commitment level, which the SEAL AEADs are not relied upon to provide.¶
The global associated data G (Section 4.6) enters the committed map as an additional input, empty by default, and the collision-resistance argument above applies unchanged with G in the committed context. This matches the ra-CMT treatment of global associated data in [FLRR25].¶
The snapshot authenticator adds two operations (RewriteSeg, SnapVerify) over a snapshot value and one new security property, snapshot integrity (Section 5.1.4). Integrating its reduction into a single combined proof with the ra-ROR framework is in preparation and will be published separately ([SEALPROOFS]).¶
The keyed snapshot is not read as sender attribution.¶
| Guarantee | Established by | Scope | Provided by |
|---|---|---|---|
| Segment confidentiality and integrity, index and finality bound | AEAD decrypt, per segment | segment | this construction |
| Segment-set integrity: content, position, count | SnapVerify over the snapshot value | whole object | this construction |
| Sender attribution, third-party verifiability | signature over the snapshot context | whole object | consuming protocol, not provided |
| Freshness, whole-object rollback protection | external version or freshness state | whole object | consuming protocol, not provided |
Snapshot integrity is keyed by snap_key and protects the writer's current segment set against a storage or write adversary that does not hold snap_key. A verifier who holds snap_key, hence the CEK from which it is derived, detects any alteration of the segment set (Section 4.7.1). Forgery of a fresh snapshot value gives no protection among parties that share the CEK, because any CEK-holder can recompute snap_key, and the value is not a third-party-verifiable commitment to a segment set. Whether a fixed, externally authenticated snapshot value binds segment content against key holders depends on the authenticator (Section 4.7.1). An application that needs sender attribution or third-party verifiability MUST add a signing layer over the snapshot context, and the configured authenticator determines whether its value can carry that layer.¶
The snapshot authenticator adds authentication of the full segment set (over the authenticator's per-segment input) and the count n_seg as a single unit, which detects same-index rollback, segment-set modification, and count changes. Its mandatory index-set check catches a duplicated or dropped index that an authenticator's verify might otherwise accept (Section 4.7.1).¶
A profile MUST keep each of an authenticator's KDF labels inside the injective encode frame (Section 4.3) so distinct roles cannot collide on the primitive input.¶
Whole-object rollback to a previously valid snapshot is out of scope and is treated in Section 6.6. snap_key is not exposed through any public API. It is derived internally per message from the CEK and salt, so snap_key exposure is not a threat surface in the construction itself.¶
These usage limits are organized around independent limit classes, not around named suites. The accounting unit is one segment encryption under one segment-encryption key (an epoch key). For random-nonce AEADs that budget is a single per-epoch-key pool. For a derived-nonce MRAE AEAD there are two separate budgets: a per-epoch-key budget for distinct derived nonces, and a per-derived-nonce budget for repeated encryption of one segment. Such an AEAD therefore does not follow a simple "divide the per-key budget by 2^epoch_length" model.¶
The budgets depend on the AEAD's nonce, key, tag, and cipher block sizes (Nn, Nk, Nt, Nb), on the AEAD's own published confidentiality and integrity analysis, on the KDF's output size Nh and its mu-PRF security (Appendix C.2), and on the suite's segment_max. A suite that fixes those works out every figure below. The mu-PRF security is what makes each epoch key's budget independent of every other's, which the per-epoch-key accounting assumes.¶
The length-dependent limits in this section are computed at the suite's segment_max, the largest plaintext one segment encryption carries. Independent of these per-key budgets, a segment-encryption procedure MUST respect each underlying AEAD's per-invocation input limits.¶
One segment encryption consumes one write from a segment-encryption key's budget. Initial writes and rewrites are both segment encryptions. The budget belongs to the epoch key, not the CEK.¶
the segment encryptions allowed under one epoch key.¶
the first encryption of a segment.¶
a later encryption of the same segment.¶
the base-2 log of the number of segments that share one epoch key.¶
the blocks per segment, n_blk = segment_max / Nb, for a block-cipher AEAD.¶
the bound a budget is solved against. Every confidentiality budget in this section is computed at 2^-32. A budget quoted from an AEAD's own analysis holds at the target that analysis states.¶
Six phenomena bound how much may be encrypted. Each is set by one property of the AEAD or the segment size, and each bounds confidentiality or integrity.¶
| Limit class | Applies to | Depends on | Failure mode |
|---|---|---|---|
| Random-nonce collision | random nonce modes | nonce size | confidentiality |
| Forgery bound | all AEADs | MAC strength, segment size | integrity |
| Block-size birthday | block-cipher AEADs | total blocks | confidentiality |
| MRAE distinct-nonce | derived-nonce MRAE across segments | derived-nonce count | confidentiality and integrity |
| Fixed-nonce data volume | derived-nonce hot rewrites | segment size, rewrites | confidentiality |
| Epoch-key collision | 128-bit-key AEADs | key size | confidentiality |
Two limits bound a write-once object: each epoch key's AEAD write budget, and the 2^63 segment index ceiling. The same two bound an append-only object, at the write counts given at the end of this section.¶
Under a flat key (one epoch key for the whole object), the AEAD budget sets the size. Which class binds follows from the AEAD:¶
| Binding class | Flat-key write-once limit |
|---|---|
| Block-size birthday | 2^((8*Nb + 1 - 32)/2) / n_blk segments |
| MRAE distinct-nonce | the AEAD's distinct-derived-nonce ceiling |
| No block-cipher structure | 2^63 segments, the index ceiling |
| No published block-size analysis | 2^63 segments, the index ceiling |
A block-cipher AEAD binds on the block-size birthday, a confidentiality bound of about 2^36 segments at Nb = 16 and n_blk = 4096 (Section 5.8.6.2). A derived-nonce MRAE AEAD binds instead on the distinct-derived-nonce ceiling its own analysis gives (Section 5.8.6.4).¶
The last two classes carry the same limit for different reasons. An AEAD with no block-cipher structure, such as one built on a stream cipher, has no block-size term at all: that is a property of the AEAD. An AEAD whose analysis publishes no such term may still have one: that is a statement about the literature, not about the algorithm, and a suite that places an AEAD in this class says which of the two reasons applies.¶
These figures assume derived nonce mode, which a non-MRAE AEAD may use only under a nonce-uniqueness discipline, the write-once or append-only profiles (Section 4.5.3.2). In random nonce mode such an AEAD binds earlier, at the random-nonce-collision budget of 2^((8*Nn + 1 - 32)/2), about 2^32 per epoch key at Nn = 12 (Section 5.8.6.1).¶
An append-only object re-encrypts one retained segment per append (Section 4.9.3.1), so a segment appended one at a time is written twice rather than once, and an identical replay after a crash adds a further write without reusing the pair (Section 4.5.3.2). Every figure above that binds on a write count therefore halves, before any replays: the block-size birthday figure, which counts blocks, and the distinct-derived-nonce figure, which counts distinct nonces. The 2^63 entry is the index ceiling and is unchanged. An epoch key correspondingly spans at most 2^(epoch_length + 1) pairs, and the budget it needs is that many writes plus any replays (Section 4.9.3.1).¶
With epoch rotation each epoch key carries a fresh budget, so any suite can reach the 2^63-segment ceiling, about 2^79 octets at segment_max 65536. An AEAD subject to the epoch-key collision class reaches it only with a sufficiently large epoch_length. At epoch_length 0 that floor caps the object near 2^(4*Nk - 16) segments, about 2^48 at Nk = 16 (Section 5.8.5). Reaching 2^63 then needs epoch_length at least 15, so the distinct epoch keys stay near 2^48.¶
A rotating profile MUST keep each epoch key's 2^epoch_length initial writes within its per-key budget. For a derived-nonce MRAE AEAD that means 2^epoch_length below the AEAD's distinct-derived-nonce budget. An append-only profile adds one re-mark per superseded segment, so each epoch key spans up to 2^(epoch_length + 1) pairs, and an encryptor MUST size the per-key budget to cover those plus the replays an interrupted append costs (Section 4.9.3.1). Rewrite capacity is a separate condition (Section 5.8.4).¶
A rewrite consumes the same budget as an initial write. How that budget is shared depends on the nonce mode.¶
For a random-nonce AEAD, epoch_length divides one per-epoch-key pool among the 2^epoch_length segments that share the key. If rewrites are spread evenly, the count each segment can take falls as epoch_length rises:¶
rewrites per segment = 2^((8*Nn + 1 - 32)/2) / 2^epoch_length¶
At Nn = 12 that is about 2^(32 - epoch_length): about 2^32 rewrites per segment at epoch_length 0, about 2^26 at 6, and about 2^22 at 10.¶
For a derived-nonce MRAE AEAD the hot-segment rewrite cap is not divided by 2^epoch_length, because a rewrite reuses the segment's one derived nonce. The cap is length-dependent instead. Where the AEAD's per-record encryption is a block cipher in counter mode, the cap is the keystream block-size birthday over the rewritten blocks, quantified in Section 5.8.6.4, and applies at every epoch_length. An MRAE AEAD built another way has whatever cap its own analysis gives. The distinct-derived-nonce budget is separate and per epoch key. A derived-nonce MRAE AEAD thus has two separate constraints, the distinct derived nonces per epoch key and the repeated encryptions at one derived nonce. An AEAD whose nonce size makes a flat key safe uses one, and a forgery budget that exceeds any reachable rewrite workload leaves no rewrite constraint at all.¶
Applications MUST track segment encryptions per key and freeze the object before a budget is exceeded. The CEK is fixed per object and cannot be rotated in place, so continued writing requires a new object under a fresh CEK.¶
epoch_length means different things by mode. For the random-nonce AEADs, a smaller epoch_length puts fewer segments under each key, so each segment keeps a larger share of the random-nonce collision pool, at the cost of more epoch keys to derive and hold. For a derived-nonce MRAE AEAD, a smaller epoch_length reduces the distinct derived nonces under an epoch key but does not raise the hot-segment rewrite cap, which is per derived nonce and length-dependent. An AEAD with a 256-bit nonce (Nn = 32) makes collision negligible and has a high forgery floor, so a flat key (epoch_length 63) is the natural choice.¶
The cipher suite's default epoch_length, used when no epoch_length is chosen, is 0 for the suites with an epoch_length range and 63 for the suites whose nonce size makes a flat key safe. A referencing protocol may choose directly, or by citing a profile or named instantiation that fixes a value. Either way a value is chosen, and the default does not apply.¶
A referencing protocol that chooses an epoch_length tuned to its expected segment count, rather than taking the default, trades some rotation for fewer epoch keys while staying within budget. Because a segment may be shorter than segment_max, size epoch_length by the number of segments rather than the object's octet length. At a fixed octet size, smaller segments mean more segments and more epoch keys.¶
The epoch keys are independent under the PRF security of the KDF ([RFC8645]), so the epoch_length parameter controls budget distribution, not key strength.¶
An AEAD with a 128-bit key (Nk = 16) adds one further constraint. The epoch-key collision term (Appendix C.2) is about n_ep^2/2^(8*Nk) over the n_ep distinct epoch keys, so a profile whose AEAD has Nk = 16 SHOULD keep distinct epoch keys per payload_key below about 2^(4*Nk - 16), which is about 2^48 at Nk = 16 and holds that term within the 2^-32 target. At epoch_length 0 this caps such an object at about 2^48 segments, far below the 2^63 index ceiling, so it constrains only extreme object sizes.¶
For random nonce mode with Nn-octet nonces and q segment encryptions under one key, the collision probability follows the birthday bound:¶
P(collision) <= q^2 / 2^(8*Nn + 1)¶
When epoch_length = r is specified, q counts encryptions per epoch key (initial writes plus rewrites within that epoch), not across the whole content. Each epoch key has an independent budget.¶
For derived nonce mode, nonces are deterministic and distinct across segment indices, so this collision term does not apply. The limits on reusing a derived nonce are in Section 5.8.6.4.¶
Nonce collisions alone do not exhaust the confidentiality bound. Following the AEAD usage-limits analysis ([I-D.irtf-cfrg-aead-limits] Section 5), a block-cipher AEAD has a distinguishing bound that grows with the total number of cipher blocks processed under a key. If s is the total number of AAD-plus-plaintext blocks, each Nb octets, and q is the number of encryption queries under a key, the confidentiality advantage is at most:¶
CA <= ((s + q + 1)^2) / 2^(8*Nb + 1)¶
A segment holds n_blk blocks and s = n_blk * q in the worst case, so CA scales as q^2 * n_blk^2 / 2^(8*Nb + 1). Solved against the 2^-32 target, the per-key budget is:¶
q <= 2^((8*Nb + 1 - 32)/2) / n_blk¶
about 2^36 at Nb = 16 and n_blk = 4096. In random nonce mode the nonce-collision bound q^2 / 2^(8*Nn + 1) dominates for typical deployments because n_blk^2 / 2^(8*(Nb - Nn)) is small: the block-size term overtakes it only once n_blk^2 exceeds 2^(8*(Nb - Nn)), which is 2^32 at Nb = 16 and Nn = 12. In derived nonce mode the nonce-collision term vanishes and the block-size term is the binding confidentiality constraint.¶
An AEAD with no block-cipher structure, such as one built on a stream cipher, has no comparable block-size bound, leaving nonce collision and forgery as the only relevant terms. Nonce collision is negligible at a large Nn, and the per-query forgery bound governs integrity. Section 5.8.6.4 gives the derived-nonce block-size limits.¶
SEAL implementations MUST compute usage budgets from the AEAD-specific confidentiality and integrity bounds, not from nonce-collision probability alone.¶
Each AEAD decryption query gives the adversary a chance to forge a valid ciphertext. The forgery advantage per query depends on the AEAD's own analysis and on segment_max:¶
IA <= v * F(segment_max)¶
where v is the number of forgery attempts and F(segment_max) is the per-query forgery advantage the AEAD's analysis gives at a segment of segment_max octets. F grows with segment_max, so a larger segment lowers the attempt budget for a fixed target. The tag length bounds F from below, at about 2^(-8*Nt), so no AEAD offers more than about 8*Nt bits of forgery resistance per query. An AEAD's authenticator may achieve less than its tag length suggests, so F is not determined by Nt and a suite takes F from the analysis of its own AEAD. [I-D.irtf-cfrg-aead-limits] gives F for several AEADs.¶
A derived-nonce MRAE AEAD has two limits the random-nonce analysis above does not capture. Both come from the AEAD's own analysis rather than from its interface.¶
Across the distinct segment nonces under one epoch key, an MRAE AEAD that derives a per-record key from the key and the nonce has a derived-key collision ceiling on the number of distinct derived nonces. Its value is the AEAD's analysis to state.¶
For a single hot segment, every rewrite reuses that segment's one derived nonce, so n_rw rewrites of an n_blk-block segment encrypt n_rw * n_blk blocks under one derived per-record key. Where that per-record encryption is a block cipher in counter mode, the binding term is the keystream block-size birthday over the total blocks, in the form of [I-D.irtf-cfrg-aead-limits] Section 5 with s = n_rw * n_blk blocks and q = n_rw queries:¶
((s + q + 1)^2) / 2^(8*Nb + 1) <= 2^-32¶
The n_rw * n_blk term dominates, so this holds while n_rw * n_blk stays below about 2^((8*Nb + 1 - 32)/2), giving:¶
n_rw <= 2^((8*Nb + 1 - 32)/2) / n_blk¶
the same expression as the flat-key block-size budget, counting rewrites of one segment at one nonce instead of writes under one key. That is about 2^36 rewrites at Nb = 16 and n_blk = 4096, and about 2^38 at n_blk = 1024.¶
An MRAE AEAD whose per-record encryption gives distinct plaintexts distinct keystreams degrades under the fixed nonce only by this block-birthday term and not by nonce reuse. Two rewrites with identical plaintext and associated data produce identical ciphertext (deterministic-MRAE equality leakage), independent of this count.¶
raAE's ra-ROR security target (Section 5.1.1) rests on three assumptions: the AEAD is multi-user real-or-random (mu-ROR) secure, the KDF is a secure multi-user pseudorandom function (mu-PRF), and nonces do not collide.¶
Integrity is bounded by the AEAD tag: no AEAD offers more than approximately 8*Nt bits of forgery resistance per query. A larger AEAD key raises confidentiality margins, not the tag-length forgery floor.¶
The rows detected by SnapVerify assume a snapshot authenticator is configured. Without one, those modes go undetected.¶
The one exception is a dropped trailing segment under a write-once profile, which the per-segment finality requirement of Section 4.9.1.2 detects with no snapshot authenticator (the truncation row below).¶
| Failure mode | Detected by |
|---|---|
| Wrong CEK, salt, or parameter set | The commitment, before any segment decryption (Section 4.6), when reader and writer share the payload_info construction. |
| Modified ciphertext core or AEAD tag | AEAD.Decrypt for the affected segment. |
| Segment moved to a different index | AEAD.Decrypt: index and finality are bound by segment_aad (random nonce mode) or the nonce (derived nonce mode). |
| Segment copied from another object at the same index | AEAD.Decrypt, when the source used a different CEK or salt; a reused salt under the same CEK breaks this separation (Section 6.7). |
| Segment copied from another version at the same index | SnapVerify, not per-segment AEAD alone, unless the whole object including its snapshot is rolled back to that earlier valid version. |
| Dropped trailing segment(s) (truncation) | Write-once: the finality requirement of Section 4.9.1.2 (top present segment carries is_final = 0). Length-changing: SnapVerify, since a re-marked terminal (Section 4.9.3) can be replayed; the mutable profile always configures an authenticator. Truncation to zero: the zero-segment prohibition, or SnapVerify (Section 6.6). |
| Missing interior, duplicated, reordered, or inserted segment | SnapVerify (segment count, finality, recomputed snapshot value). |
| Stale snapshot value | SnapVerify, unless the stale value is part of a whole-object rollback. |
| Whole-object rollback | Not detected by raAE alone; the consuming protocol needs authenticated external freshness state. |
| Equality leakage in derived nonce mode | Not an authentication failure; rewriting equal plaintext at the same index can reveal equality (Section 4.5.3, Section 6.2). |
Nonce reuse under a non-MRAE AEAD leaks plaintext: an adversary who observes two ciphertexts under the same key and nonce recovers the XOR of the plaintexts (for CTR-based AEADs) and can forge new ciphertexts. For an AEAD whose authenticator is a polynomial universal hash, nonce reuse also recovers the authentication key, enabling forgery of arbitrary messages.¶
Random nonce mode depends entirely on the CSPRNG. If the CSPRNG returns duplicated state, segments collide. Derived nonce mode removes that dependence, since nonces are deterministic, but the determinism is itself a hazard: re-encrypting a segment with different content under its repeated derived nonce is a two-time pad, catastrophic for a non-MRAE AEAD.¶
A non-MRAE AEAD therefore uses derived nonces only under a discipline that keeps every (index, finality bit) pair unique, the write-once or append-only profiles, drawing a fresh salt per object and never reusing a pair under that salt, including after a crash (Section 4.5.3.2). Write-once meets this by never re-encrypting with changed inputs, and by drawing a fresh salt rather than retrying an object it did not complete. Append-only meets it by permitting at most one re-encryption per segment, the finality re-mark, under the three rules of Section 4.9.3.1. An MRAE AEAD instead degrades only to equality leakage, not plaintext recovery.¶
The plaintext-bound construction (Appendix A) partially defends against CSPRNG duplication: different plaintexts at the same index produce different nonces because the plaintext digest differs, but equal plaintexts still collide. Implementations that need a full defense against random number generator (RNG) state duplication MUST use derived nonce mode with an MRAE AEAD.¶
The full parameter set that affects encryption, decryption, AAD construction, nonce construction, and key derivation is bound into payload_info (see Section 4.2) and therefore into the commitment. A reader using a different parameter set than the writer triggers a commitment mismatch before any AEAD operation is attempted. A reader supplying a different G than the encryptor likewise triggers a commitment mismatch (Section 4.6). Consuming protocols MUST still reject unrecognized or unsupported parameter values before decryption, since the commitment check detects mismatch but does not by itself indicate which parameter value the recipient is unable to support.¶
One profile-level constant is bound transitively rather than via payload_info: aad_label is bound through protocol_id (each profile fixes its own aad_label). nonce_mode, by contrast, is carried in payload_info, so the commitment binds it directly. Profiles MUST NOT share a protocol_id across distinct aad_label values. Reusing a protocol_id with a changed aad_label produces objects whose commitment matches the wrong reader but whose per-segment AEAD verification fails with no clear error attribution.¶
Two classes of implementation error break cross-role isolation. A non-injective framing function maps distinct KDF input tuples to the same primitive input, correlating outputs that should be independent. Implementations MUST verify injectivity per Section 5.4. Reusing a label across roles (for example, "commit" for both commitment and payload key) has the same effect. Labels MUST be distinct within a protocol version, and a new version that changes any derivation MUST change the protocol_id.¶
The nonce mode and AEAD choice are coupled, and the table gives the rule for each pairing:¶
| AEAD class / profile | random nonce | derived nonce |
|---|---|---|
| non-MRAE, mutable | valid | unsafe |
| non-MRAE, write-once or append-only | valid | valid |
| MRAE, any profile | wasteful | valid |
Rewrite is where the coupling matters: a derived nonce is reused where a random nonce is redrawn, and the consequences of that reuse by AEAD class are in Section 6.2 and Section 4.5.3.2.¶
First, a reader that does not run snapshot verification (Section 4.7) does not verify that a segment belongs to the current authenticated snapshot. Such a reader gets per-segment AEAD authenticity only, without the snapshot integrity guarantee of Section 5.1.4: the ciphertext core is authenticated under the segment index and finality bit, but the reader cannot detect that the segment was substituted from a previous valid version of the same message at the same index, nor that other segments were dropped or rolled back. Such readers MUST still verify the commitment per Section 4.6. Applications that support random-access single-segment reads MUST either run snapshot verification on every read or explicitly document that they accept per-segment authenticity without snapshot freshness.¶
Conversely, snapshot verification authenticates the segment set, its positions, and the count. Without the segment commitment it does not confirm that a tag is a valid AEAD tag for the ciphertext beside it. With the segment commitment set it binds the ciphertext bytes and the tag, but not that the tag opens the ciphertext under the AEAD (Section 4.7.1). Decrypting the segment is what establishes that, so snapshot verification layers set, position, and count binding on top of per-segment AEAD without replacing it.¶
Second, snapshot integrity does not provide freshness against whole-object rollback. A storage adversary that rolls back the entire encrypted object, including the snapshot, to a previously valid version is not detected by raAE alone, because the rolled-back snapshot is itself a valid snapshot for that prior state. This is replay of an intact prior snapshot, not a forgery: it stays out of scope unless the consuming protocol authenticates external freshness into the snapshot context. Applications that require whole-object rollback resistance MUST bind an authenticated version field, timestamp, monotonic counter, or authenticated storage layer into that context. Relatedly, Section 4.9.2.1 requires a writer to update only trusted snapshot state, so a rewrite cannot launder a rolled-back snapshot into a valid ongoing history.¶
Third, same-index rollback within a snapshot is detected when snapshot verification runs. If an adversary replaces one segment with a previously valid same-index segment but leaves the current snapshot value in place, the recomputed snapshot value no longer matches the stored one, so verification fails except with negligible forgery probability (see Section 5.7).¶
Truncation, including removal of the final segment marked is_final = 1, is detected by snapshot verification. The removed segment is absent from the recomputed snapshot value and the count n_seg no longer matches the segments present, so verification fails. Truncation is a special case of the first limitation above, for readers that skip snapshot verification.¶
Reusing a salt with the same CEK across two files produces identical payload schedule outputs: the same payload_key, the same snap_key, and, in derived nonce mode, the same nonce_base. The damage depends on the nonce mode.¶
In every nonce mode, an adversary can silently swap same-index segments between the two files: both per-segment AEAD checks and the snapshot authenticator accept the swap. Salt reuse is therefore an integrity break in every configuration.¶
In derived nonce mode the nonces also repeat across the two files, so an MRAE AEAD degrades to deterministic encryption and leaks plaintext equality between same-index segments. A non-MRAE AEAD then permits plaintext recovery. Such an AEAD is safe under a derived nonce only within a single object, where the write-once or append-only discipline keeps each derived nonce unique (Section 4.5.3.2). Reusing the salt repeats nonce_base across two objects, hence every derived nonce, and breaks that scoping.¶
In random nonce mode (and with the plaintext-bound hedge) fresh nonces keep confidentiality intact, although the per-key nonce-collision budget of Section 5.8.6.1 then counts encryptions across both files under the single CEK. The integrity break above remains. Applications MUST therefore ensure salt uniqueness per CEK when creating a new message. The freshness requirement in Section 4.9.1.1 exists for this reason.¶
Applications MUST track total segment encryptions per key and freeze the object before exceeding the per-epoch-key budget of Section 5.8.4. For a 96-bit-nonce AEAD (Nn = 12) that budget is roughly 2^32 encryptions per epoch key, and exceeding it risks nonce collisions and plaintext recovery (Section 5.8.5).¶
A mutable profile with derived nonces and an MRAE AEAD reuses a segment's deterministic nonce across non-terminal rewrites, and a crash that replays a write reuses it again. Confidentiality there rests on the per-segment rewrite limit (Section 5.8.4), not on nonce uniqueness: the AEAD degrades to equality leakage between identical rewrites rather than to plaintext recovery, as long as that budget is respected.¶
The append-only profile has no such margin. It admits a non-MRAE AEAD, so its safety rests on never re-encrypting a segment with different content (Section 4.9.3.1), and a writer that breaks that rule gets a two-time pad rather than a bounded leak.¶
Per-segment associated data A_i is rewritable, so a reader cannot treat it as context fixed at creation (Section 4.4.2). An application that needs creation-fixed context places it in G, which the commitment binds (Section 4.6), not in A_i.¶
raAE also does not guarantee atomic rewrites. A segment rewrite touches the nonce metadata, ciphertext core, AEAD tag, and the snapshot value. A crash between any two of these leaves the content inconsistent. Applications MUST use write-ahead logging, copy-on-write, or an equivalent mechanism to make rewrites recoverable.¶
Several raAE operations handle secret data and MUST be implemented in constant time to prevent timing side-channels.¶
The KDF calls in the payload schedule and epoch key derivation take the CEK or payload_key as input keying material. Implementations MUST ensure that the KDF's keyed operations execute in constant time with respect to their key inputs. For an HMAC-based KDF this is satisfied by implementations that do not branch on key octets.¶
Both the commitment comparison (Section 4.9.1.2) and the snapshot comparison in SnapVerify MUST use a constant-time octet comparison. A variable-time comparison reveals the position of the first differing octet. An adversary who can retry tampered inputs learns the expected value one octet at a time and can then present a matching stored value. This reduces the forgery cost of either check to a linear number of trials (for the snapshot, the bound of Section 5.7).¶
AEAD.Encrypt and AEAD.Decrypt operations inherit the constant-time requirements of the underlying AEAD. Implementations SHOULD use AEAD libraries that document constant-time guarantees.¶
raAE protects segment content and binds segments together. Everything a complete storage or messaging system needs beyond that is the consuming protocol's responsibility. This section is the authoritative boundary of what raAE does not provide.¶
Snapshot integrity binds the segment set of one recorded object state, not its recency. A party holding the ciphertext can present an earlier honest state in full, and both per-segment and snapshot verification still pass. Telling the current state from a stale one requires external freshness (Section 6.6).¶
The CEK must remain available as long as any reader needs access, so raAE provides none.¶
Ciphertexts are not bound to any sender. A signing or MAC layer is needed for sender authentication.¶
Key identifiers and structural features of the encrypted format are visible, so unlinkability requires application-layer measures.¶
Snapshot verification covers segments within a message but cannot detect replacement of the message itself. An adversary who swaps one encrypted message for another goes undetected unless the application binds message identity externally.¶
raAE emits and consumes component values but defines no wire container, storage transactions, or key lifecycle. A consuming protocol supplies these.¶
Two applications that both reuse the same SEAL profile label with the same input keying material, the same payload_info, and the same G derive identical commitments, keys, nonces, and AADs. The protocol_id provides cross-application domain separation only when it is distinct per application. Applications whose keying material may be shared across systems SHOULD use an application-specific protocol_id (for example, "myapp-backup-v1") rather than a SEAL profile label. An application adopting a SEAL profile unchanged MAY use that profile's label.¶
This document has no IANA actions. It defines an abstract primitive and a parameterized construction and requests no code points or registries of its own. The cipher-suite code points and the registries of named instantiations and snapshot authenticators are specified in the companion ([I-D.sullivan-seal-concrete]).¶
The normative reference to [I-D.ietf-hpke-pq], which defines the one-step KDF class this document's combiner admits, is a reference to a work in progress, so publication of this document waits on it.¶
This appendix is informative. It describes an optional encryptor-side construction that mixes plaintext content into the per-segment nonce derivation to defend against RNG state duplication. The construction is encryptor-only: the decryptor reads the resulting nonce from the wire (in the same slot used by random mode) and never invokes any of the machinery below. Implementations MAY use this construction in place of a fresh CSPRNG call when generating nonces under nonce_mode "random". The wire format is indistinguishable from random mode and decryption is unaffected.¶
One such construction:¶
pt_digest(i) = LH(P_i)
encryption_params = [aead_id, segment_max_be, kdf_id]
pt_hash(i) = KDF(protocol_id, pt_hash_label,
[pt_digest(i)],
encryption_params, Nh)
nonce_ctx = encode(protocol_id, uint64(i), pt_hash(i))
nonce(i) = KDF(protocol_id, pt_nonce_label,
[Random(Nn), payload_key],
[...payload_info, nonce_ctx], Nn)
¶
This construction fixes two labels:¶
| Derivation role | Label variable | Value |
|---|---|---|
| Plaintext digest binding | pt_hash_label | "pt-nonce" |
| Final nonce derivation | pt_nonce_label | "nonce" |
Both labels are distinct from all other SEAL labels. Neither derivation uses nonce_base_label (Table 4): the "nonce_base" label belongs to derived nonce mode and does not appear in this construction. In the final derivation nonce_ctx enters the info list as one element after the payload_info elements: its encode output is framed behind a length prefix like any other element, not spliced flat into the list.¶
LH is the over-large-field digest of Section 4.3. Its collision resistance is justified in Section 4.3.¶
When the encryptor's CSPRNG produces duplicated state (for example, from a virtual machine snapshot or a fork without reseed), two encryptions of distinct plaintexts at the same segment index still produce distinct nonces because pt_hash(i) differs. Two encryptions of identical plaintexts at the same index produce identical nonces, resulting in deterministic encryption: the ciphertexts are identical, revealing only that the plaintexts are equal.¶
For rewrites under this construction, the encryptor recomputes pt_hash(i) with the new plaintext, derives a fresh nonce(i) using the new pt_hash(i) and a new Random(Nn), seals the new plaintext under the new nonce, and updates the snapshot value as in Section 4.9.2.1.¶
When a long-term symmetric key sk of at least Nh octets is available to the encryptor, implementations SHOULD mix it into random generation using the hedging pattern of [RFC8937]. If only an asymmetric private key is available, it MUST first be processed through a KDF to produce a uniform symmetric key.¶
hedge_key = KDF(protocol_id, hedge_label, sk, [], Nh)
HedgedRandom(n, label):
return KDF(protocol_id, label,
[hedge_key, Random(n)], [], n)
¶
This construction fixes one label, hedge_label = "hedge", distinct from all other SEAL labels.¶
HedgedRandom output depends on both the CSPRNG and sk, so a weak CSPRNG alone cannot predict it. Hedging does not help when the CSPRNG state itself is duplicated (VM snapshots, fork without reseed). Identical CSPRNG output still produces identical HedgedRandom output. Appendix A describes an optional encryptor-side construction that defends against state duplication. It is orthogonal to hedging and the two MAY be combined.¶
This appendix is informative. Table 15 gives each security claim's source, argument, and assumptions. Each is inherited from [FLRR25], argued in this document, referenced from external work, or deferred to the proof paper in preparation ([SEALPROOFS]). The one claim proved in full here is the KDF combiner's injectivity.¶
The combiner's input encoding is injective and canonical (Section 4.3, Section 5.4): distinct (protocol_id, label, ikm, info, L) tuples map to distinct primitive inputs, from the length-prefixed encoding rather than the KDF. Collision resistance is assumed only of LH, and only for over-large fields. A snapshot authenticator with the segment commitment additionally relies on LH collision resistance to bind each segment's ciphertext of any length (Section 4.7.1), and the commitment derivation carries its own separate collision-resistance assumption (Appendix C.4).¶
| Property | Source | Argument | Rests on |
|---|---|---|---|
| ra-ROR | [FLRR25] notion and base proof; SEAL's arbitrary-position ra-ROR syntax is a SEAL-specific obligation | Appendix C.2, Appendix C.3 | mu-PRF KDF (two keying levels), mu-ROR AEAD, no nonce collision, fresh per-message salt |
| ra-CMT (commitment, position-respecting) | [FLRR25] | Appendix C.4 | collision resistance of the commitment derivation map over (protocol_id, "commit", CEK, payload_info, G, commitment_length) |
| ra-CMT-p (per-segment associated-data and position commitment) | [FLRR25] notion and base proof; SEAL position-binding realization and per-segment associated-data and position accounting argued here, formal write-up in [SEALPROOFS] | Appendix C.4 | ra-CMT collision bound plus the underlying AEAD's commitment bound for the forged position, restored conditionally by the segment commitment over a strict ordering where the snapshot is authenticated outside the construction (Section 5.6) |
| KDF combiner injectivity and domain separation | this document (structural, proof-complete) | Section 4.3, Section 5.4 | injectivity of the length-prefixed encoding (KDF need not be injective); collision resistance of LH for over-large fields only |
| Snapshot integrity | this document (notion, Section 5.1.4); configured authenticator (bound) | Section 5.7 | unforgeability of the snapshot value by a party without snap_key; per-object separation via salt-bound snap_key |
| AEAD usage, rewrite, and max-object bounds | external work (operational, not proven here) | Section 5.8, Section 5.8.3, Section 5.8.6.4 | [I-D.irtf-cfrg-aead-limits], [RFC8452] Section 9 with [BHT18], 2^63 segment-index ceiling |
| Derived-nonce unique-nonce transform | precedent, not proof (TLS 1.3 [RFC8446], STREAM [HRRV15]) | Section 4.5.3.2, Appendix C.5 | write-once or append-only uniqueness discipline reducing to nonce-respecting AEAD security |
| Derived-nonce equality leakage (MRAE) | this document; full proof in [SEALPROOFS] | Appendix C.5 | MRAE AEAD ([RFC8452]); per-segment fixed-nonce data-volume cap |
For a ra-ROR adversary (Section 5.3) using at most u distinct CEKs and q_m messages, each encrypted with an independent fresh 32-octet per-content salt (Section 4.9.1.1), across n_ep total epochs and making q_e encryption and q_d decryption segment queries with sigma = q_e + q_d, the advantage is bounded by:¶
Adv_raROR(A) <= Adv_salt_coll
+ Adv_muPRF(KDF, B_KDF)
+ Adv_muROR(AEAD, B_AEAD)
+ Adv_nonce_coll
+ Adv_snap
¶
The probability that two messages encrypted under one CEK draw the same salt. Because the salt is a 32-octet value drawn uniformly at random, this term is at most q_m^2 / 2^256. A salt collision is not a PRF-distinguishing event: the KDF deterministically produces identical payload_key, snap_key, and nonce_base outputs whenever (CEK, payload_info) repeats, so the event must be charged separately. Conditioned on no salt collision, distinct messages under one CEK have distinct payload_info values and therefore independent KDF outputs under the mu-PRF assumption. This term holds only under the fresh per-message uniform salt requirement of Section 4.9.1.1. A profile that reuses salts or draws them non-uniformly forfeits it and must argue payload-schedule separation by other means (Section 6.7).¶
The multi-user PRF advantage of the KDF, covering two keying levels. Level A is keyed by the CEK and produces payload_key, snap_key, and nonce_base. It has u users, queried once per message. Level B is keyed by payload_key (producing epoch_key per Section 4.5.2 and, in the plaintext-bound construction, per-segment nonces per Appendix A) and by snap_key (producing the snapshot authenticator's keyed derivations). It has q_m users. The snap_key subset of Level B feeds the configured snapshot authenticator and is counted under Adv_snap below. It is not counted again here.¶
The multi-user real-or-random advantage of the AEAD over sigma total segment queries. There is one mu-ROR user per distinct segment key, for n_ep epoch keys total (n_ep = q_m when each message uses a single flat epoch key). This advantage absorbs a key-collision birthday term of about n_ep^2/2^(8*Nk) over the n_ep distinct keys. For the 256-bit-key suites (Nk = 32) that term is about n_ep^2/2^256 and is negligible. For the 128-bit-key suite (Nk = 16) it is about n_ep^2/2^128 and is the binding epoch-key collision floor. Section 5.8.5 bounds n_ep for that suite.¶
The probability of a nonce collision under any segment key. In derived nonce mode it is zero, conditional on no salt collision (already charged via Adv_salt_coll). In random nonce mode (with or without the plaintext-bound hedge) it is bounded per Section 5.8.6.1. The derived-mode statement relies on the deterministic-MRAE analysis in Appendix C.5.¶
The snapshot-forgery advantage of the configured snapshot authenticator, present only under an authenticator that produces a snapshot value and zero under None (Section 4.7.2). It covers the mu-PRF cost of the authenticator's snap_key derivations (the snap_key subset of Level B, counted here and not again in Adv_muPRF(KDF, B_KDF)) with the forgery and any further terms the authenticator's own construction requires. A key-holder content-binding property, where an authenticator provides one, is a separate notion from this without-key advantage and is not counted here.¶
The reduction follows the [FLRR25] hybrid over the two-level key schedule (Section 4.5). Condition on no salt collision (Adv_salt_coll). Replace the CEK-keyed KDF outputs and the payload_key-keyed outputs with uniformly random values, charged to Adv_muPRF; these are Level A and the payload_key subset of Level B. The snap_key-keyed outputs are idealized likewise, but their mu-PRF cost is the snap_key subset of Level B, charged to Adv_snap and not to Adv_muPRF (Section 5.7). Bound the resulting segment AEAD outputs (Adv_muROR). A flat epoch key is the single-epoch case of the same argument. The segment AEAD calls are nonce-respecting only under no nonce collision, the event charged to Adv_nonce_coll (Appendix C.2). In derived nonce mode this term is zero conditional on the same no-salt-collision event (Appendix C.5). The snapshot authenticator's forgery is the remaining part of Adv_snap (Section 5.7). The reduction constructs adversaries B_KDF against multi-user PRF security of the KDF and B_AEAD against multi-user real-or-random security of the AEAD, each running in time approximately that of A.¶
ra-CMT security reduces to collision resistance of the commitment derivation map over the tuple (protocol_id, "commit", CEK, payload_info, G, commitment_length), with G empty by default. PRF security alone is not sufficient for this reduction, because the commitment adversary may choose the CEK and the context values (Section 4.5).¶
For commitment_length = L octets, the relevant quantities are:¶
fixed-pair collision probability: 2^(-8*L) q-query birthday probability: q^2 / 2^(8*L + 1) collision-search work factor: about 2^(4*L)¶
Thus 16 octets gives about 2^64 collision-search work, 32 octets about 2^128, 48 octets about 2^192, and 64 octets about 2^256, each capped by the collision resistance of the underlying hash. The fixed-pair collision probabilities at 32, 48, and 64 octets are 2^(-256), 2^(-384), and 2^(-512), respectively.¶
The ra-CMT-p advantage is bounded by the sum of two terms: the commitment collision bound above and the underlying AEAD's commitment bound for the forged position.¶
Position binding is inherited from the underlying AEAD, restored by the segment commitment over a strict ordering where the snapshot is authenticated outside the construction (Section 5.6). The notion derives from [FLRR25], and the per-segment associated-data and position accounting is deferred to [SEALPROOFS] (Appendix C.1).¶
The unique-nonce transform of Section 4.5.3.2, which XORs the segment index and finality bit into nonce_base, follows TLS 1.3 ([RFC8446]) static-IV-XOR-identifier and STREAM ([HRRV15]) counter-plus-final-bit precedent. The transform is injective in the pair (i, is_final). The leading Nn - 8 octets of nonce_base are unchanged; XOR by its fixed low 8-octet value is a bijection on 8-octet strings; and (i, is_final) maps to the integer (i << 1) | is_final, which is a bijection from the index range i < 2^63 onto the 64-bit values. Distinct pairs therefore yield distinct nonces, and a derived nonce repeats only when an object encrypts twice at one pair.¶
Both disciplines of Section 4.5.3.2 keep every derived nonce carrying one plaintext, so each reduces unique-nonce record protection to nonce-respecting AEAD security. That reduction is precedent, not a SEAL-specific proof.¶
Under both disciplines no segment is ever re-encrypted with a different plaintext or associated data (Section 6.2, Section 4.9.3.1), so where a nonce is used twice both uses carry the same inputs, AEAD.Encrypt reproduces the same ciphertext, and a reduction answers the second query from the first one's output. The bound is therefore the same in both cases: the underlying AEAD's bound under distinct nonces.¶
In derived nonce mode the nonce-collision term is zero conditional on no salt collision, because each segment index maps to one derived nonce and a fresh per-message salt makes the segment keys distinct across messages. A rewrite reuses a segment's derived nonce, so the construction relies on the underlying MRAE AEAD: re-encrypting the same plaintext and associated data under the same key and nonce reproduces the same ciphertext, which leaks only equality of those inputs, while distinct inputs remain real-or-random secure ([RFC8452]). The formal real-or-random treatment of this equality leakage for derived nonce mode is in preparation ([SEALPROOFS]). That treatment builds on the [FLRR25] ra-ROR analysis and changes its nonce-respecting hypothesis: a rewrite repeats a segment's derived nonce, so the proof replaces the nonce-respecting AEAD assumption with deterministic-MRAE security, under which nonce reuse leaks only input equality.¶
This appendix is informative.¶
The per-content salt makes the payload schedule unique even when a CEK is reused across messages, which matters for applications that derive CEKs from group keys. The encryptor chooses the salt locally at write time. A per-message counter would instead require synchronized state.¶
The CEK is 32 octets regardless of the AEAD key size Nk, so a 128-bit-key AEAD still derives its keys from a 256-bit CEK.¶
Epoch keys bound the number of AEAD invocations under any one segment encryption key.¶
The per-key bounds of the underlying AEAD (birthday for 96-bit-nonce schemes, block-size for AES, integrity forgery for all schemes) become the limiting factor on security long before the nonce space is fully exhausted.¶
Large content with many segments can place many AEAD invocations under one key, even when the content is written once.¶
Rewrites consume additional invocations and can exhaust the same budget faster than write-once use, but they are not the only reason for epoching. Write-once large content benefits from the same partitioning.¶
Rekeying the entire content would defeat the random-access property, so epoch keys bound the per-key invocation count without requiring a full re-encryption.¶
Labels separate derivation roles: commitment and payload_key share the same inputs but different labels, making them independent under the PRF assumption. Once CEK and salt are chosen the hierarchy is fixed, with no mutable state to synchronize across writers.¶
Random mode is simplest but trusts the CSPRNG completely. Derived mode removes that trust. In a mutable profile it requires an MRAE AEAD, because an in-place rewrite reuses the segment nonce (Section 6.5). In the write-once profile the write-once rule keeps every derived nonce unique, and in the append-only profile the append-only rule does, so under either, derived mode pairs with any AEAD. A third rule was available and rejected: permitting a length change in either direction would let a truncation free indices the object had already spent, for a later append to encrypt new content at (Section 4.9.3.1).¶
The nonce mode is a profile parameter: a profile selects a valid (nonce_mode, snap_id) tuple, subject to the MRAE requirement above.¶
The author thanks Andrés Fábrega, Thomas Ristenpart, Gregory Rubin, Richard Barnes, Brendan McMillion, Thibault Meunier, Kenny Paterson, Christopher Patton, Martin Thomson, Stanislav V. Smyshlyaev, and Christopher A. Wood for their reviews, comments, and discussions.¶