| Internet-Draft | raAE | July 2026 |
| Sullivan | Expires 7 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 provides two profiles, immutable (write-once) and mutable (in-place ciphertext rewrite), 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, analyzes SEAL against those requirements, and provides example cipher suites and test vectors.¶
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.¶
Source for this draft and an issue tracker can be found at https://github.com/grittygrease/draft-sullivan-cfrg-raae.¶
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 7 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 tag 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 tags and the count are exactly the set the writer last recorded, under a content-derived key. 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 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 raAE primitive and its extended 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, and named instantiations are in Section 4.10, Section 4.11, and Section 4.12, and the appendices provide the test vectors.¶
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].¶
A subscript denotes a per-segment value: X_i is the value X for the segment at index i, as in A_i, P_i, ct_i, and tag_i. A value written f(i), as in segment_key(i) or nonce(i), is computed from the index by the named derivation. X[a:b] is the octet range of X from offset a inclusive to offset b exclusive.¶
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 (Section 4.10.2).¶
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.¶
AEAD verification for one segment only.¶
Verification of the complete segment tag set and snapshot.¶
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.5) but does not mandate this encoding. Another profile MAY use any injective encoding that meets Section 5.5.¶
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.¶
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. 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 per-segment authentication still passes on each segment it is shown. 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.2). 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.2.3).¶
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.¶
Rewriting gives a storage layer something to attack: it can roll a segment back to an older valid copy, substitute one valid same-index segment for another, or drop one while per-segment authentication still passes on what it presents. A scheme that supports rewriting MAY carry a snapshot, a value over the current set of segments that a reader checks to detect these changes.¶
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 present segments (the (position, ciphertext segment) pairs of the current object state), and the snapshot. 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. When the scheme supports incremental update, an update can run in time independent of the total segment count. This is an implementation goal, not a syntactic requirement of the interface. The base primitive has no snapshot value. The snapshot, the two signature changes above, RewriteSeg, and SnapVerify are defined by this document.¶
These additions define the extended raAE interface of this document: the tuple (KeyGen, StartEnc, EncSeg, StartDec, DecSeg, RewriteSeg, SnapVerify) over the auxiliary snapshot value. The first five algorithms are those of Section 3.2, with the snapshot outputs of StartEnc and EncSeg defined above. 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. Length change adds no further algorithm, as the next paragraph describes.¶
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.2.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.¶
Length changes occur only at the end of the message: a holder may append segments or drop a suffix, but cannot remove or renumber front or interior segments, because positions are index-addressed. Each step updates the snapshot, and Section 4.9.2.2 gives the construction for both directions.¶
SEAL is a parameterized construction for realizing the raAE primitive defined in Section 3. A SEAL instantiation combines an AEAD, a KDF, a key schedule, a nonce-generation method, a commitment mechanism, and an optional snapshot authenticator to produce an raAE scheme for segmented stored content.¶
The raAE interface defines what operations and security properties a scheme provides. SEAL defines how those operations are realized from standard cryptographic components. Different SEAL instantiations MAY select different AEADs, KDFs, nonce modes, commitment mechanisms, or snapshot authenticators, provided they satisfy the requirements of this section.¶
The remainder of this section specifies the common SEAL construction. Section 5 analyzes its security, and concrete parameter choices and interoperable suites are in Section 4.10.¶
The key hierarchy is shown below. A CEK and per-content salt feed the payload schedule, which derives four values. The payload key feeds per-segment keys (optionally through epoch keys), and each segment produces an AEAD tag. When a snapshot authenticator is configured, every tag feeds it to produce the snapshot.¶
A commitment lets a reader reject a wrong key or parameter set before decrypting anything. An optional snapshot authenticates the current set of segments.¶
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).¶
This section names every parameter, input, and derived symbol the construction uses. Table 2 summarizes them, grouped by what sets each one. The entries that follow define the suite choices, the profile-level constants, the algorithm-determined sizes, and the per-message inputs. Derived values are defined where they are computed, in the sections the table cites.¶
| Parameter | Type or range | Set by | Defined in |
|---|---|---|---|
| AEAD | RFC 5116 algorithm | suite | Section 4.10.1 |
| KDF | kdf_id from Table 10 | suite | Section 4.3 |
| segment_max | power of two, at least 4096 | suite | Section 4.10 |
| 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 ("SEAL-DATA" in SEAL) | profile | Section 4.4.2 |
| commitment_length | integer, at least 16, default Nh | profile | Appendix C.4 |
| Nk, Nn, Nt | 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, the commitment derivation, and the snapshot value for every suite. They are specified in Section 4.4.2, Section 4.6, and Section 4.7 respectively, and are encoded identically across all SEAL cipher suites. The resulting 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. See Table 10 for the permitted KDFs.¶
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.9 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 (Section 4.5.2). Profiles MAY restrict r further per AEAD (Section 4.10). The value sets a rotation granularity, not an on/off switch: r = 0 selects the finest rotation, a fresh epoch key for every segment, and no value of r disables epoch-key derivation. A flat key, one epoch key covering all segments, is r = 63.¶
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, the default mode each SEAL suite uses in the mutable profile (Table 9), and the valid (nonce_mode, snap_id) tuples for each profile in Section 4.10.2.¶
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. They are defined in Section 2.2, with concrete values in Table 9 and Table 10. 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.2.2 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 (Section 4.5) computes the per-message values commitment, payload_key, snap_key, and nonce_base once from the CEK and payload_info (the commitment also binds G, Section 4.6), and they are fixed for the message's lifetime. The per-segment values epoch_key(i), segment_key(i), nonce(i), segment_aad(i, is_final, A_i), and tag(i) are defined where they are computed: Section 4.5.2, Section 4.5.3, Section 4.4.2, and Section 4.8.¶
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 of at most 0xFFFE octets is emitted literally behind its 2-octet length, identical to a plain length prefix. The reserved length 0xFFFF is a typed prefix meaning that an Nh-octet digest LH(x) of an over-large field follows in place of the field itself.¶
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)
¶
Routing LH through the KDF would encode-frame x and recurse, because frame of an over-large x would re-enter LH. LH therefore runs the native primitive directly on x. The injectivity of encode and the domain-separation and collision properties of LH are established in Section 5.5.¶
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. SEAL's two profiles instantiate the combiner at protocol_id SEAL-RW-v1 and SEAL-RO-v1 (Section 4.10.2).¶
The KDF is built from the cipher suite's hash in one of two forms. The cipher suite's kdf_id (Table 10) 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.5 and demonstrated in Appendix E.3). The two forms are not mutually injective: a cipher suite's kdf_id (Table 10) 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.¶
The following example expands M for combiner vector KDF.33 (Appendix E.3): TurboSHAKE-256, label "commit", ikm a single 32-octet element of repeated 0xAA, info the two elements 010203 and 0405, and L = 64. Each frame is a 2-octet length followed by the element, and the ikm and info lists are each one nested encode:¶
M, 71 octets, in frame order:
000a 5345414c2d52572d7631 frame(protocol_id "SEAL-RW-v1")
0006 636f6d6d6974 frame(label "commit")
0022 0020 aa..aa (32 octets) frame(encode(...ikm)): one inner
frame wrapping the single element
0009 0003 010203 0002 0405 frame(encode(...info)): inner
frames of 3 and 2 octets
0002 0040 frame(uint16(L)), L = 64
XOF(M, 64) = the KDF.33 output
¶
With info = [], the empty sequence, the fourth frame is the two octets 0000: encode() of no elements is the empty octet string, and its frame is a zero length with no content. With info = [""], one zero-length element, the inner encode is 0000 and the fourth frame is 0002 0000. KDF.8 and KDF.28 (Appendix E.3) exercise the same distinction in the two-step form.¶
SEAL fixes the concrete encodings the combiner and the AEAD consume: the payload_info tuple, the segment AAD, and the derivation labels.¶
payload_info = [aead_id, segment_max_be, kdf_id, snap_id,
nonce_mode, epoch_length_u8, salt]
¶
Element encodings for SEAL:¶
aead_id: 2 octets, uint16(id), the unsigned 16-bit AEAD identifier from Table 9 (for example, 0x0002 for AES-256-GCM).¶
segment_max_be: 4 octets, uint32(segment_max). SEAL's segment sizes are given in Section 4.10.¶
kdf_id: 2 octets, uint16(id), the unsigned 16-bit KDF identifier from Table 10 (0x0001 for HKDF-SHA-256 per [RFC9180] Section 7.2; 0x0013 for TurboSHAKE-256 per [I-D.ietf-hpke-pq]).¶
snap_id: 2 octets, uint16(id), the snapshot authenticator identifier from Table 11 (0x0000 for none, 0x0001 for the masked multiset hash).¶
nonce_mode: 1 octet, the nonce construction (Table 12): 0x00 for random, 0x01 for derived. Table 9 gives the default mode each SEAL suite uses in the mutable profile. A profile MAY select another valid (nonce_mode, snap_id) tuple.¶
epoch_length_u8: 1 octet, uint8(epoch_length). Valid values per Table 9. 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. It is bound transitively via protocol_id. 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: index and is_final are bound in the nonce
if A_i is empty:
return "" ;; no AEAD associated-data pass
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 example test vectors use an empty A_i, so the random-mode vectors keep the three-element encoding and the derived-mode vector has an empty segment_aad.¶
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.¶
Unauthorized truncation is detectable, and Section 4.9.1.2 requires a decryptor to reject it. A reader that sees the highest-indexed present segment carry is_final = 0 knows more data must follow. Authorized truncation by the CEK holder instead re-marks the new final segment and updates the snapshot (Section 4.9.2.2). Extension is also caught: appending after a segment marked is_final = 1 either invalidates the original final segment on re-read or produces a ciphertext that cannot verify.¶
Each segment binds both its index and its finality bit, through the AAD in random nonce mode and through the nonce in derived nonce mode. This per-segment binding, rather than reliance on the snapshot alone, catches these attacks. A reader can check them without holding all tags before decrypting any one segment.¶
The per-segment AEAD binds a segment's index and finality, not its length (through the AAD in random nonce mode and through the nonce in derived nonce mode). 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 | "acc_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 label strings are frozen for wire compatibility. In particular the snapshot authenticator key snap_key uses the label string "acc_key", retained from an earlier name. These are the always-present key-schedule labels. The masked multiset hash (Section 4.7.4), 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: the 32-octet CEK and the per-content salt. All other keys are derived deterministically through the KDF of Section 4.3.¶
Four values are derived from the CEK and per-content salt. The commitment allows early rejection of a wrong key and additionally binds the global associated data G (Section 4.2.4). The payload key encrypts segments through epoch keys. The snapshot authenticator key is used by the configured snapshot authenticator. The nonce base (derived-mode only) seeds deterministic per-segment nonces.¶
payload_info = [aead_id,
uint32(segment_max), ;; segment_max_be
kdf_id,
snap_id,
nonce_mode,
uint8(epoch_length), ;; epoch_length_u8
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.¶
AES-128-GCM, AES-256-GCM, and ChaCha20-Poly1305 use a 96-bit nonce. At a 2^(-32) collision-probability target, the random-nonce budget is 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 is write-once (Section 4.10.2). 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 in a write-once profile |
| 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.9.¶
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))
¶
No per-segment nonce storage is required.¶
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.9.4 for the resulting size limit.¶
A derived nonce is fixed by the segment index, so re-encrypting a segment reuses its nonce. An MRAE AEAD such as AES-256-GCM-SIV ([RFC8452]) tolerates that reuse, degrading to equality leakage rather than plaintext recovery, within the per-segment rewrite limit given in Section 5.9. A non-MRAE AEAD does not: reusing its nonce leaks plaintext, so with a non-MRAE AEAD, derived nonce mode MUST be confined to a write-once profile, in which each segment is encrypted exactly once.¶
A write-once profile rewrites nothing and performs no length change. A rewrite would repeat a derived nonce, and a length change re-marks the terminal segment under a fresh nonce (is_final flips the low bit). The write-once discipline is the simplest rule that keeps every derived nonce unique. It MUST draw a fresh salt for every object, so the nonce base differs, and MUST NOT re-encrypt under that salt after a crash (Section 6.2).¶
Folding is_final into the nonce also removes a per-segment cost. The nonce binds the index and finality, so the per-segment associated data need not carry them: segment_aad is empty when the caller supplies no A_i, and is encode(aad_label, A_i) otherwise (Section 4.4.2). When A_i is empty the AEAD processes no associated data, removing the per-segment associated-data pass that random nonce mode incurs. This is the structural reason 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.¶
Per-segment AEAD authenticates the ciphertext core together with the segment index and finality bit (through the segment AAD in random nonce mode and through the nonce in derived nonce mode) so position binding for the segment being opened is already provided. Two further properties remain: context commitment, provided here, and snapshot membership, provided by the snapshot authenticator (Section 4.7). The commitment lets a reader reject an incorrect CEK or parameter context before attempting any segment decryption, providing ra-CMT-style context commitment (Section 5.2.2) for the construction.¶
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).¶
Before decrypting any segment, a reader derives the expected commitment from the CEK, payload_info, and G, and compares it octet-for-octet with the stored value. A reader MUST verify the commitment before decrypting any segment and MUST treat a mismatch as an authentication failure for the object. A mismatch indicates a wrong key, a wrong parameter set, a wrong G, or a corrupted header. This check provides ra-CMT context commitment. The reduction outline and bound are in Section 5.7.¶
G is the global associated data input of StartEnc in the raAE primitive (Section 3.2), binding whole-message application context such as a name, version, or policy. The key schedule commits G by appending it as one length-prefixed element after payload_info in the commitment info input (Section 4.5), so the commitment covers G just as the primitive's ciphertext header T_g does ([FLRR25]).¶
G is bound through the commitment, which is fixed when the content is created, so G is immutable: changing it invalidates the stored commitment. Segment contents remain rewritable through the snapshot authenticator (Section 4.7).¶
G defaults to the empty octet string, so a message whose application supplies no context commits over an empty final element. G is never stored. A decryptor supplies it from application context, and 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 set of segment tags into the public snapshot value. A SEAL profile selects one with snap_id (Table 11).¶
A snapshot authenticator provides the following operations:¶
Fold one segment's tag into the running value.¶
Take one segment's tag back out. An updatable authenticator provides this operation, so a single segment can be rewritten or dropped without rebuilding. An add-only authenticator omits it and rebuilds the value by re-adding the surviving segments.¶
Set the segment count to n.¶
Bind the current state into the snapshot value, which authenticates the (index, tag) set and the count n_seg.¶
Recompute the snapshot value over the current (index, tag) set and accept only if it matches the stored snapshot, in constant time.¶
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. Finality is not checked here: 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.¶
Call verify(snapshot) over the present (index, tag) set and the count n_seg.¶
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. SEAL's masked multiset hash realizes verify in Section 4.7.4.¶
SnapVerify above checks the whole-object target. An authenticator MAY also support a single-segment target that verifies one (index, tag) against the recorded set. The masked multiset hash reads all tags and supports the whole-object target only. The snapshot value stays a single target-independent value of length Na, with no per-segment proof stored.¶
A conforming authenticator's snapshot() MUST bind both the (index, tag) 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. How the value is computed, and whether a single-segment change updates it in place or rebuilds it, is the authenticator's choice. The masked multiset hash (Section 4.7.4) updates in place in O(1).¶
The snapshot value length is written Na and MAY depend on n_seg, with Na = 0 meaning no value. A fixed-length authenticator suits any layout, while a count-dependent one is supported only by the split layout of a mutable object (Section 4.11).¶
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 tag. 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 tag changes:¶
Under the current count, remove each changed segment's current tag (remove(i, tag)). 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 tag (add(i, tag)). 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 | Tags removed | Tags 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) |
Each operation re-encrypts only the segments whose tag changes, so a caller that cannot recover a re-encrypted segment's plaintext cannot perform it.¶
This sequence uses remove, so it is the updatable-authenticator path. An add-only authenticator instead resets and re-adds the surviving segments to rebuild the snapshot value.¶
A SEAL profile selects its snapshot authenticator with snap_id (Table 11): 0x0000 for none, or 0x0001 for the masked multiset hash.¶
With snap_id = 0x0000 there is 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).¶
With snap_id = 0x0001 the snapshot authenticator builds an updatable, masked multiset hash over the segment tags. It accumulates one indexed, keyed contribution per segment, in the MSet-XOR-Hash form of Clarke et al. ([MSetHash]), then publishes that accumulator behind a deterministic mask this document adds. Its per-segment contribution is:¶
contrib(i) = KDF(protocol_id, contrib_label,
[snap_key],
[uint64(i), tag(i)], Nh)
¶
The masked multiset hash fixes three labels: contrib_label = "acc_contrib", snapshot_tag_label = "snapshot_tag", and snapshot_mask_label = "snapshot_mask", each distinct from all other SEAL labels.¶
The finality bit is bound through tag(i): it is an AEAD input in both nonce modes (segment_aad in random mode, the low nonce bit in derived mode, Section 4.5.3.2), so flipping it changes tag(i), hence contrib(i), hence the accumulator. No explicit binding in contrib is needed.¶
The masked multiset hash aggregates the per-segment contributions into the accumulator acc by bitwise XOR:¶
acc = contrib(0) XOR contrib(1) XOR ... XOR contrib(n_seg-1)¶
The accumulator is an Nh-octet value, with the all-zero string as the value for the empty segment set. XOR is order-independent, so the accumulator does not depend on segment order, and it is its own inverse, so rewriting segment i removes its old contribution and adds the new one by XOR, in O(1) time and without re-reading any other segment.¶
An object with n_seg = 0 is a valid empty object. It holds the salt, the commitment, and the snapshot value, with no ciphertext segments and no per-segment tags. Its accumulator is the XOR over the empty segment set, namely 0^Nh, and the same masking as any other count applies, so the empty snapshot is not a fixed constant but a masked authenticator over zero segments:¶
acc = 0^Nh snapshot_tag = snaptag(0, 0^Nh) mask = snapmask(0, snapshot_tag) snapshot = snapval(0, 0^Nh) = mask || snapshot_tag¶
An empty object is distinct from a zero-length plaintext, which is one final segment (n_seg = 1) whose plaintext has length 0.¶
To rewrite segment i without re-reading the other tags, the writer first recovers the accumulator from the stored snapshot by removing the mask, acc = wrapped_acc XOR snapmask(n_seg, snapshot_tag). It then XORs contrib(i) computed from the old tag (stored alongside the ciphertext) out of acc, XORs in contrib(i) computed from the new tag, and produces the new snapshot value over the unchanged count. The wrapped_acc and snapshot_tag MUST come from locally trusted snapshot state (Section 4.9.2.1).¶
The accumulator is linear, and publishing it in the clear would let a write adversary recombine the differences between successive values into a non-historical segment set (Appendix C.5). The masked multiset hash therefore does two things. First it authenticates the count and accumulator with the snapshot tag, a message authentication code (MAC) under snap_key with its own label:¶
snaptag(n_seg, acc) = KDF(protocol_id, snapshot_tag_label,
[snap_key],
[uint64(n_seg), acc], Nh)
¶
Write snapshot_tag = snaptag(n_seg, acc) for the present count and accumulator. Second, the masked multiset hash derives a mask from snap_key under the third label, seeded by that synthetic tag rather than by a fresh nonce, and XORs it into the accumulator to form the masked accumulator wrapped_acc:¶
snapmask(n_seg, snapshot_tag) =
KDF(protocol_id, snapshot_mask_label,
[snap_key],
[uint64(n_seg), snapshot_tag], Nh)
mask = snapmask(n_seg, snapshot_tag)
wrapped_acc = acc XOR mask
snapshot = wrapped_acc || snapshot_tag
¶
Seeding the mask with the synthetic tag rather than a fresh nonce is the synthetic-IV derandomization of deterministic authenticated encryption ([DAE]): the snapshot stays a deterministic function of (n_seg, acc) while the published accumulator is hidden behind a one-time pad (Appendix C.5). The count n_seg is not stored in the snapshot value. The verifier supplies it as the number of segments present, and snaptag binds it under the MAC.¶
The masked multiset hash stores wrapped_acc and snapshot_tag, each Nh octets, so Na = 2*Nh. Section 4.7.1 defines Na in general.¶
As a function of the count and accumulator, the snapshot value is¶
snapval(n_seg, acc) = wrapped_acc || snapshot_tag¶
The snapshot tag binds n_seg and the accumulator under snap_key, and the mask hides the accumulator behind a tag-derived one-time pad. The forgery bound and the masking argument are in Section 5.8 and Appendix C.5.¶
SnapVerify runs only after the verifier has re-derived and checked the commitment as part of decryption (Section 4.9.1.2). The snapshot's per-object binding rests on snap_key (Appendix C.5).¶
The masked multiset hash realizes verify(snapshot) for the SnapVerify of Section 4.7.1. SnapVerify supplies the count n_seg and the present segment tags:¶
verify(snapshot):
;; Caller (SnapVerify, {{snapshot-interface}}) MUST already have
;; checked that the present indices are exactly 0..n_seg-1, each
;; once. This accumulator is order-independent under XOR, so a
;; duplicate or missing index is invisible here and MUST NOT be
;; skipped by the caller.
acc_calc = XOR over i of contrib(i) ;; present segment tags
tag_calc = snaptag(n_seg, acc_calc)
snapshot_calc = (acc_calc XOR snapmask(n_seg, tag_calc)) || tag_calc
compare snapshot_calc to snapshot in constant time
(on any mismatch, reject)
return accept
¶
The comparison runs in constant time (Section 6.9). Because the accumulator is order-independent under XOR, a duplicate or missing index can leave acc_calc recomputing to the genuine accumulator and the comparison passing, which is why SnapVerify's index-set check (Section 4.7.1) is mandatory.¶
verify returns only accept or reject. An implementation MUST NOT surface the recomputed accumulator or any value derived from it, and MUST NOT signal through an error code or timing whether the snapshot tag matched while the masked accumulator did not. The mask hides the accumulator only against a one-bit verifier: exposing it, or distinguishing a tag-valid accumulator-mismatch from a plain reject, reinstates the recombination attack of Appendix C.5.¶
A modified tag or count changes the recomputed snapshot, and a different key or parameter context fails the commitment check that precedes SnapVerify (Section 4.9.1.2). The forgery bound is in Section 5.8 and the rollback limitation in 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.2.2) 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 the operation-level mapping of Table 1 to the signature level. The primitive's position identifier p = (i, b) appears here as the explicit arguments i and is_final. Its immutable state S is realized by the payload schedule: the segment_key(i), nonce(i), and segment_aad calls below read the schedule from context rather than taking S as an argument. Its message segment M_i is the plaintext P_i, and its opaque ciphertext segment C_i is stored as the field triple (nonce_metadata(i), ct_i, tag_i).¶
| Subroutine | Realizes | Composed by |
|---|---|---|
| EncryptSegment | EncSeg | Section 4.9.1.1, Section 4.9.2.2 |
| DecryptSegment | DecSeg | Section 4.9.1.2 |
| RewriteSegment | the encryption core of RewriteSeg | Section 4.9.2.1 and Section 4.9.2.2, which add the snapshot bookkeeping |
These subroutines emit ct_i and tag_i as separate fields, and ct_i has length len(P_i), which can be shorter than segment_max. The consuming format conveys each segment's ciphertext length to the decoder so DecryptSegment can recover the two fields, as required by 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) ;; per nonce_mode 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 the AEADs in Table 9 the tag is the final Nt octets of C_i, so ct_i is the first len(P_i) octets and C_i = ct_i || tag_i. EncryptSegment splits C_i at that boundary, and DecryptSegment reassembles it.¶
The caller folds tag_i into the snapshot authenticator with add(i, tag_i) (Section 4.7). EncryptSegment itself does no snapshot work.¶
DecryptSegment returns the segment plaintext, or a decryption error:¶
DecryptSegment(i, is_final, A_i, nonce_metadata_i, ct_i, tag_i):
key = segment_key(i)
nonce = nonce_metadata_i or nonce(i) ;; per nonce_mode (above)
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 (new_nonce_metadata_i, new_ct_i, new_tag_i) replaces the stored nonce metadata, ciphertext, and tag for segment i.¶
RewriteSegment produces the replacement ciphertext and tag only. The caller updates the snapshot authenticator by reading the stored old tag and calling remove(i, old_tag_i) then add(i, new_tag_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 (add(i, tag), remove(i, tag), set_length(n), snapshot(), and verify()) without referring to how any is computed. The authenticator selected by snap_id (Table 11) realizes those operations. For the masked multiset hash they reduce to the accumulator, snapshot-tag, and mask formulas of Section 4.7.4.¶
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 (snap_id = 0x0000, Table 11), the snapshot steps below are omitted, and a reader relies on per-segment authentication alone.¶
Encryption runs in two phases. The first phase derives the per-message schedule and writes the commitment. The second encrypts each segment independently and, under a snapshot authenticator, adds its tag with add(i, tag). Then snapshot() binds the whole set into the stored snapshot value. Because the segments are independent, the second phase may run in any order or in parallel.¶
The per-segment tags then feed the snapshot authenticator, which adds each one and binds them into the snapshot value (Section 4.7).¶
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
nonce_metadata_i, ct_i, tag_i
= EncryptSegment(i, is_final, empty, P_i)
store nonce_metadata_i, ct_i, tag_i
if snapshot authentication is enabled (snap_id != 0x0000):
add(i, tag_i)
if snapshot authentication is enabled (snap_id != 0x0000):
set_length(n_seg)
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 (snap_id != 0x0000), whole-set authenticity is a separate and stronger check that requires every tag. It detects any added, dropped, reordered, or re-marked segment, and same-index rollback to an older sibling. 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 (snap_id = 0x0000) 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, nonce_metadata_i, ct_i, tag_i)
(return its error on failure)
if snapshot authentication is enabled and all tags are 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 (index, tag) pairs.¶
SnapVerify needs every tag, so a reader that streams segments in order cannot gate their release on snapshot integrity without first buffering all tags or using a layout that carries the tags in a header. 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 segment does not carry is_final = 1. A run of segments 0 through k-1 whose last segment (index k-1) 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 is unforgeable without the CEK: in derived nonce mode it occupies the low bit of the segment nonce (Section 4.5.3), and in random nonce mode it is bound through segment_aad (Section 4.4.2). An adversary therefore cannot re-mark a non-final segment as final and can only drop trailing segments, which this check detects.¶
Authorized truncation by the CEK holder re-encrypts the new final segment under is_final = 1 with a fresh nonce (Section 4.9.2.2), 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. A profile with no snapshot authenticator forbids the zero-segment object, so a valid object always has at least one segment, and a decryptor MUST reject one that has none. Under a snapshot authenticator the snapshot binds the count, so the empty object (n_seg = 0) is 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. Under a snapshot authenticator, when all segment tags are available (for example, a full read or an integrity audit), the reader MUST run SnapVerify (Section 4.7) on the stored snapshot before delivering any plaintext, and MUST return an error if SnapVerify fails. With no snapshot authenticator there is no such check, and per-segment authentication stands alone.¶
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 and length change are available only in a mutable profile, which configures a snapshot authenticator (snap_id = 0x0001). Each re-encrypts only the segments whose tag changes and updates the snapshot over the new segment set. A caller that cannot recover a re-encrypted segment's plaintext cannot perform them.¶
A rewrite touches exactly one segment and never reads another. Replacing a segment removes its old tag from the snapshot authenticator and adds the new one, then updates the snapshot over the unchanged count with snapshot(). The update trusts, rather than re-verifies, the stored snapshot state. The old tag is already stored alongside the old ciphertext, so no other segment is read or decrypted (Section 4.7). This single-segment update and the trust in stored snapshot state are properties of an updatable authenticator like the masked multiset hash, which folds the change in O(1). An add-only authenticator re-reads the surviving tags to rebuild on rewrite.¶
The procedure is:¶
read old_tag_i
new_nonce_metadata_i, new_ct_i, new_tag_i
= RewriteSegment(i, is_final, empty, new_P_i)
remove(i, old_tag_i) (count n_seg unchanged)
add(i, new_tag_i)
snapshot = snapshot()
store new_nonce_metadata_i, new_ct_i, new_tag_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 (AES-256-GCM-SIV). MRAE AEADs are designed to tolerate nonce reuse, degrading to equality leakage rather than plaintext recovery, within the per-segment rewrite limit in Section 5.9.5. The snapshot update proceeds identically.¶
The stored snapshot cannot be validated during an in-place rewrite. Validation is SnapVerify (Section 4.7), which recomputes the authenticator from every segment's tag and so requires reading all tags. During a rewrite the application trusts the stored snapshot state, applies remove and add for the one changed segment, and produces the new value with snapshot(). Because it trusts that 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 that an application performs when it has 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 (Figure 5). 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.¶
re-mark the terminal segment t:
append: t = n_seg - 1 is_final: 1 -> 0
truncate: t = m - 1 is_final: 0 -> 1
if a terminal segment t exists (n_seg >= 1 for append, m >= 1 for
truncate):
read salt, payload schedule, snapshot value, segment t
(truncate also reads old_tag_j for discarded j in m..n_seg-1)
P_t = plaintext of segment t (decrypt, or use if held)
meta, ct, new_tag_t
= RewriteSegment(t, is_final_t, empty, P_t)
under the old count: remove(t, old_tag_t)
else:
(append from empty: no old terminal; the appended loop below
marks the last new segment is_final = 1. truncate to m = 0:
no segment remains, so nothing is re-marked.)
truncate: for j in m .. n_seg-1: remove(j, old_tag_j)
n_seg' = n_seg+k (append) or m (truncate)
set_length(n_seg')
if a terminal segment t exists: add(t, new_tag_t)
append: for j in n_seg .. n_seg+k-1:
meta_j, ct_j, tag_j
= EncryptSegment(j, is_final_j, empty, P_j)
add(j, tag_j)
(is_final_j = 1 only for the last segment)
snapshot = snapshot()
store segment t and snapshot; append adds the new segments,
truncate drops segments m..n_seg-1
¶
Changing message length re-encrypts the terminal segment, except when truncating to the empty object (m = 0), where no segment remains. A caller that cannot recover that segment's plaintext cannot do it.¶
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 incremental 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.¶
Both operations count against the same per-CEK AEAD usage budget as initial encryption (Section 5.9). 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 can make the same (index, finality bit) pair recur, and only an MRAE AEAD tolerates the resulting nonce reuse (Section 4.5).¶
Applications that do not need length changes MAY simply 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.¶
Test vectors for the SEAL profile are provided in Appendix E, and the serialization layouts are in Section 4.11.¶
This section lists example algorithm suites for the SEAL construction. SEAL's concrete framing and procedures are defined in Section 4.¶
SEAL defines the following AEAD algorithms. Each has a code point, a key size, a nonce size, a default nonce mode, and an epoch_length range. The key size Nk is 16 octets for AES-128-GCM and 32 octets for the other suites. All use a 16-octet authentication tag (Nt = 16).¶
| Algorithm | aead_id | Nk | Nn | default mode | epoch_length |
|---|---|---|---|---|---|
| AES-128-GCM | 0x0001 | 16 | 12 | random | 0 to 63 (default 0) |
| AES-256-GCM | 0x0002 | 32 | 12 | random | 0 to 63 (default 0) |
| ChaCha20-Poly1305 | 0x001D | 32 | 12 | random | 0 to 63 (default 0) |
| AES-256-GCM-SIV | 0x001F | 32 | 12 | derived | 0 to 63 (default 0) |
| AEGIS-256 | 0x0021 | 32 | 32 | random | 63 |
| AEGIS-256X2 | 0x0024 | 32 | 32 | random | 63 |
The aead_id values are the unsigned 16-bit code points from the IANA AEAD Algorithms Registry ([RFC5116]), encoded as uint16(id). The registry's textual identifiers are, in table order, AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_CHACHA20_POLY1305, AEAD_AES_256_GCM_SIV, AEAD_AEGIS256, and AEAD_AEGIS256X2. AES-128-GCM and AES-256-GCM are specified in [NIST-SP-800-38D], ChaCha20-Poly1305 in [RFC8439], AES-256-GCM-SIV in [RFC8452], and AEGIS-256 and AEGIS-256X2 in [I-D.irtf-cfrg-aegis-aead]. The AEGIS code points 0x0021 and 0x0024 are early allocations that firm up when that I-D is published as an RFC.¶
The 96-bit-nonce AEADs (AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305) and the MRAE AES-256-GCM-SIV (in derived nonce mode) rotate the segment key and may set epoch_length anywhere in 0 to 63. The 256-bit-nonce AEADs (AEGIS-256, AEGIS-256X2) use a flat key (epoch_length = 63, one epoch key covering every segment). Section 5.9.6 gives the per-suite budgets that fix these ranges.¶
The default nonce_mode column gives each AEAD's mode in the mutable profile, SEAL-RW-v1. The immutable profile, SEAL-RO-v1, instead pairs a derived nonce with any of these AEADs, because its write-once rule keeps every derived nonce unique and needs no MRAE AEAD (Section 4.10.2).¶
SEAL permits the KDF cipher suites in Table 10, identified by entries from the HPKE KDF Registry ([RFC9180] Section 7.2 and [I-D.ietf-hpke-pq]):¶
| kdf_id | Name | Construction | Nh |
|---|---|---|---|
| 0x0001 | HKDF-SHA-256 | Two-step HKDF-Extract+Expand | 32 |
| 0x0002 | HKDF-SHA-384 | Two-step HKDF-Extract+Expand | 48 |
| 0x0003 | HKDF-SHA-512 | Two-step HKDF-Extract+Expand | 64 |
| 0x0013 | TurboSHAKE-256 | One-step XOF (absorb+squeeze) | 64 |
HKDF-SHA-256 is specified in [RFC5869]. TurboSHAKE-256 is the
extendable-output mode of the SHA-3 family selected by
[I-D.ietf-hpke-pq]. The kdf_id values are the unsigned 16-bit code
points from the HPKE KDF Registry, encoded as uint16(id). Nh and
commitment_length equal the KDF's primitive output size, 32 octets for
HKDF-SHA-256, 48 for HKDF-SHA-384, and 64 for HKDF-SHA-512 and
TurboSHAKE-256. HKDF-SHA-256 is the baseline KDF. The others are
optional alternatives.¶
The two-step Extract and Expand are HKDF-Extract and HKDF-Expand. The one-step XOF uses TurboSHAKE256 with D = 0x1F, the Derive convention's domain-separator byte, which an implementation MUST NOT change.¶
The snapshot authenticator is selected by snap_id:¶
| snap_id | Name | Snapshot value |
|---|---|---|
| 0x0000 | none | none produced |
| 0x0001 | masked multiset hash | snapshot, per Section 4.7 |
snap_id = 0x0000 selects no snapshot authenticator: no snapshot value is produced and a reader relies on per-segment authentication alone. An immutable (write-once) profile enforces snap_id = 0x0000. A profile that supports rewrite requires snap_id = 0x0001, so every rewritable object carries whole-object integrity (Section 4.9.2).¶
The nonce mode is carried explicitly by nonce_mode:¶
| nonce_mode | Name |
|---|---|
| 0x00 | random |
| 0x01 | derived |
The two constructions are defined in Section 4.5.3.¶
The supported maximum segment sizes are 16384 and 65536 octets. Both values are powers of two and at least 4096 octets. The 16384-octet size aligns to 16 KiB memory pages (for example, on Apple Silicon). The 65536-octet size aligns to 64 KiB (four 16 KiB pages).¶
The aad_label is "SEAL-DATA". Under snap_id = 0x0001 the snapshot authenticator is the masked multiset hash (Section 4.7), with wrapped_acc and the snapshot tag each an Nh-octet value, so Na = 2*Nh.¶
A SEAL suite fixes an AEAD and a KDF (Table 9, Table 10), a maximum segment size, a snapshot authenticator (snap_id, Table 11), a nonce mode (nonce_mode, Table 12), and an epoch length. The protocol_id identifies the parameters a profile fixes, and only certain (nonce_mode, snap_id) tuples are valid under each one. This document defines two named profiles, SEAL-RW-v1 and SEAL-RO-v1.¶
A profile's payload_info MUST carry the full parameter context affecting key derivation, AEAD operations, AAD construction, and nonce construction, so that the commitment (Section 4.6) binds that context.¶
SEAL-RW-v1 is the mutable profile (read-write). It requires snap_id 0x0001 (the masked multiset hash), so every rewritable object carries whole-object integrity, and permits a random nonce or a derived nonce with an MRAE AEAD. It supports rewrite, extend, and truncate, and carries SEAL's snapshot machinery: the accumulator, its mask, the snapshot value, and SnapVerify. Unauthorized truncation surfaces at two points: the terminal finality check of Section 4.9.1.2, and SnapVerify over the complete segment set, which also binds the segment count.¶
SEAL-RO-v1 is the immutable profile (read-only). Here "immutable" names the writer's write-once discipline, not a tamper-evidence guarantee. It selects a derived nonce and sets snap_id to 0x0000, so that no snapshot authenticator runs, and it is write-once: an encryptor MUST NOT rewrite a segment once it has been written. The rule is load-bearing: a rewrite would repeat the segment's derived nonce, with the consequences given in Section 6.2. Because each derived nonce is then used only once, the profile works with any AEAD, including a non-MRAE one. See Section 4.5.3.¶
SEAL-RO-v1 provides per-segment confidentiality and integrity, binding the segment index and the finality bit, and key commitment through the commitment field, but not snapshot or whole-object integrity. Whole-object integrity, when needed, comes from the snapshot in the mutable profile or a layer above SEAL. Truncation detection rests on the finality bit alone and surfaces only when the highest-indexed present segment verifies under is_final = 1 (Section 4.9.1.2), so a consumer of streamed plaintext has no completeness guarantee before that terminal check.¶
| protocol_id | nonce_mode | snap_id | mutability |
|---|---|---|---|
| SEAL-RW-v1 | random or derived | 0x0001 | rewrite/extend/truncate |
| SEAL-RO-v1 | derived | 0x0000 | write-once |
An encryptor MUST set payload_info to a (nonce_mode, snap_id) tuple that is valid for its protocol_id, and a decryptor MUST reject any object whose tuple is not. A derived nonce under SEAL-RW-v1 requires an MRAE AEAD; SEAL-RO-v1 admits any AEAD because the write-once rule keeps each derived nonce unique.¶
Pick the profile from how the stored content changes after it is first written.¶
SEAL-RW-v1 (mutable) fits content updated in place: editable files, mutable object stores, and append-or-truncate logs. It carries snap_id 0x0001, so a reader detects tampering with the current segment set as a whole: any added, dropped, reordered, or re-marked segment, a same-index rollback, or a count change (Section 5.2.3). Whole-object freshness beyond replay still comes from a layer above SEAL (Section 6.6). A derived nonce under this profile requires an MRAE AEAD, while a random nonce works with any of the suites.¶
SEAL-RO-v1 (immutable) fits write-once content: archives, backups, content-addressed blobs, and write-once media. It stores no per-segment nonce and works with any AEAD, so it is the smaller and simpler choice when content is never rewritten. Whole-object integrity, if needed, comes from a layer above SEAL.¶
Section 5.9.6 covers epoch_length selection, and Section 4.10.1 covers the per-AEAD trade-offs that further narrow the suite.¶
This section describes three serialization layouts a consuming protocol can use to store raAE output: linear, aligned, and split. The parameterized SEAL construction mandates none of them. Each named instantiation binds one, and the consuming protocol pins the remaining details (Section 4.12). The commitment field in every layout is commitment_length octets (Section 4.2.3); the figures annotate it with its default, commitment_length = Nh.¶
In a linear layout the salt, commitment, snapshot value, and segment data appear in sequence. The salt comes first because it is needed to derive all payload schedule values. The commitment follows so a reader can reject a wrong key before reading any segment data. The snapshot value (Na octets), the configured authenticator's output, precedes the segment data so a streaming reader has it before the segments and can check it once all are read. Segments then follow in index order.¶
When the object has at least one segment, that final segment carries is_final = 1. An empty object (n_seg = 0) has zero segments, so no segment is final.¶
Brackets mark the nonce, which precedes the ciphertext and tag for a segment and is stored only in random nonce mode. In derived nonce mode (AES-256-GCM-SIV) the nonce is recomputed from the key schedule, so no nonce is stored.¶
A streaming reader recovers segment boundaries from the segment lengths. Because a segment MAY be shorter than segment_max (Section 2), a linear layout that is to be read as a stream MUST keep every non-final segment at the full segment_max, leaving only the final segment short. A reader then finds each boundary at the fixed segment length. A layout that stores shorter interior segments MUST record their lengths so the reader can locate each segment.¶
Linear layout supports streaming writes. A writer emits the salt, commitment, and a placeholder for the snapshot value, then streams segments. After all segments are written the writer seeks back to the snapshot position and writes the final value in place.¶
In an aligned layout the ciphertext segments occupy slots aligned to segment_max, so a reader can seek to any segment with page-aligned I/O. An arbitrary-length prefix, which the consuming protocol uses for its own machinery and which raAE does not specify, comes first; the raAE header follows; the ciphertext follows the header.¶
Offsets are measured from index 0, the start of the prefix. Let B = segment_max and let H be the offset at which the ciphertext begins (the prefix length plus the header length). The leading slot, from H up to the first segment boundary M * B, is filled in one of two ways, chosen before writing:¶
No padding: the first ciphertext segment occupies the slot, with M = ceil(H / B). It is shorter than B and ends on the boundary M * B.¶
Padding: the slot is zero-padded to a multiple of B, with M >= ceil(H / B) (the next boundary, or a larger multiple to reserve whole segment slots for append headroom).¶
From M * B onward every ciphertext segment begins at a multiple of B and is a full B octets, with the final segment at most B. H, M, and the first-segment length all follow from the prefix and header sizes, so a writer computes them before emitting any ciphertext.¶
The header holds the salt, the commitment, the snapshot value, and one metadata entry per segment:¶
Each metadata entry holds the segment's stored nonce and its AEAD tag:¶
The bracketed nonce is present only in random nonce mode. The tag is always present, Nt = 16 octets in every SEAL suite (Table 9). Each metadata entry holds the Np-octet presented nonce and the Nt-octet tag, so meta_len = Np + Nt octets. A random-nonce entry sets Np = Nn and is Nn + Nt octets. A derived-nonce entry recomputes the nonce from the key schedule, so Np = 0 and the entry is Nt octets. The per-suite values are:¶
| AEAD | nonce mode | meta_len |
|---|---|---|
| AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 | random (Nn = 12) | 28 |
| AEGIS-256, AEGIS-256X2 | random (Nn = 32) | 48 |
| AES-256-GCM-SIV | derived | 16 |
The n_seg entries total n_seg * meta_len octets, so the whole header is 32 + commitment_length + Na + n_seg * meta_len octets. With the default commitment_length = Nh and the masked multiset hash's Na = 2 * Nh, the header is 32 + 3 * Nh + n_seg * meta_len octets.¶
A reader verifies the commitment and the snapshot value from the header alone, then seeks to any segment using these offsets. Because the snapshot is computed from the per-segment tags alone (the snapshot authenticator takes the segment tag, not the ciphertext), and the header holds every tag in its metadata entries, a reader authenticates the whole object's snapshot from the header without reading or streaming any ciphertext. This supports efficient random-access reads.¶
A split layout separates the ciphertext from the metadata into two streams that grow independently. The data stream holds the ciphertext segments, each a full segment_max except the last. Segment i is at offset i * B. The metadata stream holds the salt, the commitment, the n_seg per-segment metadata entries (each meta_len octets, broken out in Figure 9), and the snapshot value last. Because the snapshot is computed from the per-segment tags alone (Section 4.7.4) and every tag lives in the metadata stream, a reader authenticates the snapshot by reading only the metadata stream, never the data stream that holds the ciphertext.¶
Because neither stream embeds the other, both grow by appending. Extending a message appends one ciphertext segment to the data stream, appends one metadata entry to the metadata stream, and rewrites the trailing snapshot value. Truncating drops the tail of each stream and rewrites the snapshot value. Neither operation shifts an existing ciphertext segment, which the in-place aligned layout cannot avoid once the header grows.¶
Under an immutable profile (SEAL-RO-v1: snap_id = 0x0000 with a derived nonce), two fields drop out of the layouts above. No snapshot authenticator runs, so there is no snapshot value, and each nonce is recomputed from the key schedule, so Np = 0 and no nonce is stored. A metadata entry is then the Nt-octet tag alone. The linear layout reduces to the salt, the commitment, and the per-segment ciphertext and tags. The aligned and split headers reduce to 32 + commitment_length + Nt * n_seg octets, the salt and commitment followed by one tag per segment. In the aligned layout every ciphertext segment begins at a multiple of segment_max, so a reader seeks to any segment by arithmetic on its index and verifies it from its own tag, with no shared snapshot to read or maintain.¶
SEAL has many parameters (Section 4.2). A relying protocol that does not want to choose them all can cite one of the named instantiations in the table below. Each row fixes a profile, a segment size, the nonce mode, a snapshot authenticator, and one of the serialization layouts of Section 4.11, leaving the cipher suite (an aead_id and a kdf_id from Section 4.10) to the referencing protocol. Each instantiation applies to a cipher suite the same way, so a protocol writes SEAL-simple(aead_id, kdf_id), SEAL-disk(aead_id, kdf_id), and so on, to obtain a complete raAE scheme.¶
| Name | Profile | segment_max | nonce_mode | epoch | layout |
|---|---|---|---|---|---|
| SEAL-attachment | SEAL-RO-v1 | 65536 | derived | 32 | linear |
| SEAL-simple | SEAL-RW-v1 | 65536 | random | 16 | linear |
| SEAL-memory | SEAL-RW-v1 | 16384 | random | 16 | aligned |
| SEAL-disk | SEAL-RW-v1 | 16384 | random | 16 | split |
| SEAL-compact | SEAL-RW-v1 | 16384 | derived | 16 | aligned |
Each instantiation is named for its primary use case, described below. The snapshot authenticator follows the profile: SEAL-RO-v1 sets snap_id 0x0000 and SEAL-RW-v1 sets snap_id 0x0001 (Section 4.10.2).¶
Every instantiation uses commitment_length = Nh and a fresh 32-octet salt per object (Section 4.9.1.1). The epoch_length is the value in the table. The write-once SEAL-attachment performs no rewrites, so it takes a larger epoch (32), while the four mutable schemes take a conservative 16 that leaves the per-epoch-key budget headroom for rewrites, which reuse the epoch key (Section 5.9). A 256-bit-nonce suite (AEGIS-256, AEGIS-256X2) uses a flat key (epoch_length 63) regardless of the row (Table 9), and a referencing protocol MAY override the epoch. Each instantiation binds the layout named in its row (Section 4.11). The layout fixes field order and placement, and the consuming protocol pins the remaining serialization details, such as the aligned layout's prefix and leading slot, for byte-level interoperability. SEAL-compact is mutable with a derived nonce, so an in-place rewrite reuses that nonce, which requires an MRAE AEAD (Section 4.10.2). The write-once SEAL-attachment and the random-nonce SEAL-simple, SEAL-memory, and SEAL-disk admit any AEAD.¶
SEAL-attachment is for write-once content read whole. It carries no snapshot authenticator, because under the immutable profile per-segment authentication and the finality bit detect truncation, reordering, and tampering on a whole-object read, and write-once leaves no earlier version to roll a segment back to (Section 6.6). Its linear layout takes the reduced immutable form (Section 4.11.4).¶
SEAL-simple is the basic mutable object with whole-object integrity from the masked multiset hash (Section 4.7.4), stored in the linear layout (Section 4.11.1). SEAL-memory targets in-memory random access with the aligned layout (Section 4.11.2). SEAL-disk rewrites individual segments on stored media, with the split layout (Section 4.11.3) holding the headers apart from the segment data so a rewrite touches one segment and extension appends to both streams.¶
SEAL-compact is the aligned layout with a derived nonce. Like SEAL-memory it stores each segment at a fixed offset for random access, but the derived nonce is recomputed rather than stored, so each per-segment metadata entry is Nt octets instead of Nn + Nt (Section 4.11.2). Because that region scales with the object, removing the stored nonce saves Nn octets per segment, a substantial part of the metadata for the large random-access objects the aligned layout targets. It reuses the derived nonce on an in-place rewrite, so it requires an MRAE AEAD and the rewrite is deterministic.¶
This section states the construction's target security properties, the assumptions its components must satisfy, and the operational limits on its use, but does not reproduce the formal proofs. The division of labor is deliberate. The ra-ROR and ra-CMT notions are taken unchanged from [FLRR25], whose proofs apply to SEAL's realization of the base interface. Snapshot integrity is the one notion this document defines. Appendix C.5 argues its bound, and integrating that argument into a combined proof is deferred to [SEALPROOFS], the companion formal write-up, in preparation, that this document cites for every deferred proof. The limits of Section 5.9 are operational deployment bounds, not proof obligations. The security notions are defined in Section 5.2.¶
The construction provides the following properties against an adversary that observes ciphertexts, tampers with stored segments or their order, and attempts decryption with chosen keys, but does not know the CEK or per-content salt:¶
The construction targets ra-ROR (Section 5.2.1): the adversary learns nothing about the plaintext beyond what the underlying AEAD already permits. The adversary observes the message's segment count and the per-segment lengths from the wire format. Segment boundary metadata is not confidential.¶
A reader that runs snapshot verification, including its index-set check over the correct segment count n_seg (Section 4.7.1), detects any added, dropped, reordered, or re-marked segment, including same-index rollback to an older valid segment. Per-segment AEAD verification alone detects modification of individual segments and relocation of a segment to a different index, but reverting a segment to a previous version at the same index is not detected. Snapshot verification realizes the snapshot integrity notion of Section 5.2.3. Replacement of the entire message with a previous valid version under the same CEK is not detected, even when performing snapshot verification. Rollback resistance at the message level is the application's responsibility (Section 6.6).¶
A reader that verifies the commitment (Section 4.6) rejects a wrong CEK, parameter context, or global associated data before decrypting any segment, per the ra-CMT notion (Section 5.2.2). Commitment to segment positions is out of scope. See Section 5.7 for that gap and its non-normative mitigations.¶
An individual segment can be rewritten in place under the same CEK without re-derivation of other keys, provided the per-key AEAD usage budget (Section 5.9) is not exceeded. A rewrite reuses the segment's derived nonce, so in derived nonce mode it MUST use an MRAE AEAD (Section 4.5.3.2). Rewrite safety is otherwise a corollary of ra-ROR and snapshot integrity (Section 5.3).¶
Atomicity of writes (the application MUST use write-ahead logging or copy-on-write); confidentiality of segment count or layout; traffic-analysis resistance; side channels in the application's storage layer; protection of the CEK before encryption or after decryption.¶
raAE has two security tiers. The base tier comprises the two notions inherited from [FLRR25], ra-ROR and ra-CMT, 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 and ra-CMT 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 a rewrite reuses the position's derived nonce. This is a deliberate nonce-repeating query. The underlying MRAE AEAD handles it under the equality-leakage relaxation that derived nonce mode documents (Appendix C.6), not the unmodified nonce-respecting game.¶
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.2.3.¶
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 the position-respecting variant ra-CMT the two contexts share a position. The ra-CMT-p variant lets the adversary choose the positions freely. Because the scheme is random access, committing one segment's context commits the full ciphertext's context. Both game variants are 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.¶
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, snapshot integrity, is the one this document adds. The construction that realizes it is in Section 4.7, its bound is in Section 5.8, and the supporting reduction is in Appendix C.5. 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 (snap_id != 0x0000) and omits it otherwise (Section 4.10.2).¶
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.2.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.2.1) and snapshot integrity (Section 5.2.3). 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.6). Each rewrite also counts against the per-key AEAD usage budget (Section 5.9).¶
Appending and truncating likewise add no algorithm (Section 4.9.2.2) 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.2.3, 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.2.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. Every bound in this section assumes the CEK is secret and uniform: 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 construction claims the properties below against this adversary.¶
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 and, in random nonce mode, per-segment nonces.¶
Stored object metadata. It authenticates the segment tag 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.¶
For the HKDF cipher suites (HKDF-SHA-256, HKDF-SHA-384, and HKDF-SHA-512), the PRF assumptions are placed on HKDF keyed by the CEK or payload_key, and the collision-resistance assumption on the commit derivation reduces to the collision resistance of the underlying SHA-2 variant (SHA-256, SHA-384, or SHA-512). Because every SEAL KDF output is at most Nh octets, HKDF-Expand makes a single HMAC call per derivation. The extractor-then-PRF analysis of HKDF is in [RFC5869].¶
For TurboSHAKE-256 (the one-step XOF cipher suite defined in [I-D.ietf-hpke-pq]), the same PRF and collision-resistance assumptions are placed on the XOF as instantiated by that HPKE KDF registry entry, which fixes the primitive and its parameters but carries no security assumptions of its own.¶
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).¶
The per-component requirements any raAE construction must satisfy are as follows.¶
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.¶
The construction MUST define the encoding of the tuple (protocol_id, label, ikm, info, L) into the underlying primitive input, and that encoding MUST be injective: distinct tuples MUST map to distinct primitive inputs. This is a requirement on the encoding, not on the KDF. The KDF need not itself be injective, because injectivity is supplied by the encoding placed in front of it. 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.¶
A KDF output for one derivation role, such as payload_key, MUST be computationally independent of the output for any other role, such as snap_key, even when derived from the same CEK and payload_info. This property follows from label uniqueness and PRF security of the underlying primitive.¶
The construction MUST bind the current set of segment tags and the segment count into a public snapshot value that an adversary without the authenticator's secret key cannot forge for a modified set of tags 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), any of the cipher suites in Table 10 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, the masked multiset hash's labels in Section 4.7.4, 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. SEAL's masked multiset hash (Section 4.7) satisfies the snapshot authenticator requirement, with its forgery argument in Section 5.8.¶
encode is injective. Among fields of at most 0xFFFE octets each frame entry is self-delimiting, so the concatenation parses uniquely regardless of the number of arguments, and distinct input tuples produce distinct output strings. For over-large fields the encoding is injective only up to the collision resistance of LH, not unconditionally: a literal entry carries a length prefix in the range 0x0000 through 0xFFFE while a digest entry carries the reserved prefix 0xFFFF, so a literal entry and a digest entry never collide, and a collision between two digest entries reduces to a collision in LH. A 0xFFFF entry is bind-only: it commits to the field but does not carry it, so a protocol that must recover a field from the encoding MUST keep that field at most 0xFFFE octets. No SEAL input approaches 0xFFFE octets, so for SEAL frame is byte-identical to a plain 2-octet length prefix and LH is never invoked.¶
The "raAE-LP-v1" prefix (the HKDF-Extract salt in the two-step class, a fixed input prefix in the one-step class) is distinct from every protocol_id and label, so LH inputs are domain-separated from encode inputs. LH is a shared collision-resistant digest rather than a per-protocol separator: the same over-large field yields the same LH value in every protocol that reuses this combiner. Cross-protocol separation comes from protocol_id in the KDF inputs and from aad_label in the segment AAD, not from LH.¶
A concrete SEAL suite specifies the following.¶
One of the algorithms from Section 4.10.1, with associated Nk, Nn, and Nt values.¶
A per-object payload_info field constrained by the profile, not a suite-fixed lock. Table 9 gives the default mode each suite uses in the mutable profile. A profile MAY select any valid (nonce_mode, snap_id) tuple (Section 6.5): in the mutable profile an MRAE AEAD MAY use either mode, and the immutable profile SEAL-RO-v1 pairs a derived nonce with any AEAD because write-once keeps every derived nonce unique.¶
The key-rotation granularity, with range given by Table 9. Per Section 5, a 96-bit-nonce AEAD rotates the segment key to bound per-key nonce-collision risk and an MRAE AEAD rotates to bound its per-key encryption count, so both take epoch_length in 0 to 63. A 256-bit-nonce AEAD makes both negligible and uses a flat key.¶
One of the sizes from Section 4.10: 16384 or 65536 octets.¶
Fábrega et al. ([FLRR25]) proved ra-ROR and ra-CMT security for a construction that instantiates the same component contracts enumerated above, and SEAL's analysis of those notions follows the same structure. SEAL's additional components (the epoch keys and the snapshot maintained across rewrite and length change) are analyzed in Section 5.6 and Section 5.8.¶
The construction targets ra-ROR security (Section 5.2.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]). This section states the property and the operational quantities a deployment needs.¶
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.¶
The multi-message advantage grows linearly in the number of messages. In derived nonce mode the salt-collision term q_m^2/2^256 is the only quadratic floor for the 256-bit-key suites. For AES-128-GCM the epoch-key collision floor E^2/2^128 (Appendix C.2) is an additional quadratic term that dominates. In random nonce mode, with or without the plaintext-bound hedge, the nonce-collision term is also quadratic:¶
nonce collision: q^2 / 2^(8*Nn + 1) at Nn = 12: q^2 / 2^97¶
where 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.9.¶
Segment size enters the analysis only through the forgery and block-size terms, both bounded in Section 5.9. 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.¶
The ra-CMT notion is defined in Section 5.2.2.¶
raAE's commitment target is ra-CMT (random-access context commitment), not the per-AEAD notion CMT-1 ([RFC9771]). CMT-1 is key commitment defined for AEADs, but raAE is a higher-level construction whose commitment binds the CEK and the full payload_info, which together carry the full parameter context that affects encryption, decryption, AAD construction, nonce construction, and key derivation.¶
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 with HKDF-SHA-256 and about 2^256 with TurboSHAKE-256. 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.2.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 natively key-committing, 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.¶
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 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 position commitment, on the order of 16 to 64 octets per segment, bound alongside the segment so that no segment opens at two positions.¶
Several AEADs including AES-GCM ([NIST-SP-800-38D]) and ChaCha20-Poly1305 ([RFC8439]) lack strong native key or context commitment. CMT-4 ([RFC9771]) is full commitment to key, nonce, associated data, and plaintext. None of the SEAL AEADs is relied upon to provide CMT-1 or CMT-4. In particular, AES-256-GCM-SIV is not a key-committing AEAD, and key-commitment attacks against it are known (for example partitioning oracles). raAE obtains ra-CMT context commitment from the external commitment over (CEK, payload_info, G) regardless of the AEAD's own committing or non-committing properties, provided that the commitment check is not bypassed and that payload_info contains the full parameter context that influences any later derivation or AEAD operation.¶
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.9). 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 ra-CMT and ra-CMT-p notions are defined in [FLRR25]. SEAL realizes ra-CMT through the external commitment over (CEK, payload_info, G), independent of the underlying AEAD's own commitment level. Position binding for ra-CMT-p is inherited from the underlying AEAD's commitment level, as described above. This document states these properties and their component bounds. The exact accounting of the per-segment associated data and position terms in the ra-CMT-p reduction is the open item deferred to the proof in preparation ([SEALPROOFS]).¶
Snapshot authentication is an extension this document adds to the [FLRR25] raAE framework. The raAE primitive and its ra-ROR and ra-CMT notions are taken unchanged from [FLRR25], whose proofs apply to SEAL's realization of the base interface. The snapshot authenticator adds two operations (RewriteSeg, SnapVerify) over an auxiliary snapshot value and one new security property, snapshot integrity (Section 5.2.3). Integrating its reduction into a single combined proof with the ra-ROR framework is in preparation and will be published separately ([SEALPROOFS]).¶
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 added, dropped, reordered, re-marked, or otherwise altered segment. It gives no protection among parties that share the CEK, because any CEK-holder can recompute snap_key, and it is not a third-party-verifiable commitment to a segment set. An application that needs sender attribution or third-party verifiability MUST add a signing layer over the snapshot context.¶
Per-segment AEAD binds each segment's index and finality bit. The snapshot authenticator adds authentication of the full set of segment tags and the count n_seg as a single unit, which detects same-index rollback, segment-set modification, and count changes. The index-set check in SnapVerify (Section 4.7.1) is mandatory: an authenticator's verify MAY accept a malformed index multiset, so without the check a duplicated or dropped index could pass.¶
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.¶
SEAL's masked multiset hash (snap_id 0x0001) publishes snapshot = wrapped_acc || snapshot_tag. The snapshot tag is a MAC over the count and accumulator under snap_key, and wrapped_acc is that accumulator hidden behind a deterministic, tag-derived one-time pad. The mask is what stops a write adversary from recombining the differences between published accumulators into a non-historical segment set. Its three labels, contrib_label, snapshot_tag_label, and snapshot_mask_label, are distinct under the encode frame.¶
Its forgery advantage Adv_acc is bounded by a fresh-input MAC term, a mix-and-match term, and a birthday term in the number of published snapshots that the deterministic masking introduces, on no assumption beyond the multi-user PRF the key schedule already uses. Appendix C.2 states the bound and Appendix C.5 derives it.¶
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 (AES-256-GCM-SIV in SEAL) 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 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.¶
For a random-nonce AEAD an epoch key has one budget, shared by the segments under it. For a derived-nonce MRAE AEAD an epoch key has two: how many distinct segment nonces it may cover, and how many times any one segment may be re-encrypted at its fixed nonce.¶
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 |
Random-nonce collision and the block-size birthday bound the confidentiality of fresh-nonce encryption, and forgery bounds integrity for every decryption. Two further classes apply only in derived-nonce MRAE mode: a derived-key collision across the distinct segment nonces under an epoch key, and a data-volume limit on repeated encryption of a single hot segment, one rewritten many times at its one derived nonce. A sixth class applies only to 128-bit-key AEADs: a collision across the distinct epoch keys that holds the number of epoch keys per payload_key below about 2^48 (Section 5.9.6).¶
For each profiled suite the binding limit is the smallest applicable class. At 65536-octet (64 KiB) segments and a 2^-32 advantage target:¶
| Suite / mode | Binding limit | Budget | Failure mode |
|---|---|---|---|
| AES-128-GCM, random | random-nonce collision | ~2^32 per epoch key | confidentiality |
| AES-256-GCM, random | random-nonce collision | ~2^32 per epoch key | confidentiality |
| ChaCha20-Poly1305, random | random-nonce collision | ~2^32 per epoch key | confidentiality |
| AES-256-GCM-SIV, derived, distinct segments | MRAE distinct-nonce | ~2^48 per epoch key | confidentiality and integrity |
| AES-256-GCM-SIV, derived, hot segment | fixed-nonce data volume | ~2^36 per segment at 64 KiB | confidentiality |
| AEGIS-256, random | forgery bound | ~2^83 per key | integrity |
| AEGIS-256X2, random | forgery bound | ~2^83 per key | integrity |
The 96-bit-nonce suites bind on random-nonce collision. AEGIS makes nonce collision negligible with a 256-bit nonce (~2^112) and binds on forgery. A derived-nonce MRAE AEAD has the two limits above, the distinct-derived-nonce ceiling and the fixed-nonce data-volume cap, not a single shared pool.¶
Two limits bound a write-once object: each epoch key's AEAD write budget, and the 2^63 segment index ceiling.¶
Under a flat key (one epoch key for the whole object), the AEAD budget sets the size:¶
| Suite | Flat-key write-once limit |
|---|---|
| AES-128-GCM, AES-256-GCM | ~2^36 segments |
| AES-256-GCM-SIV | ~2^48 segments |
| ChaCha20-Poly1305, AEGIS-256, AEGIS-256X2 | 2^63 segments |
For the block ciphers this limit is a confidentiality bound. The AES-GCM suites (AES-128-GCM and AES-256-GCM) bind on the block-size birthday and AES-256-GCM-SIV on the distinct-derived-nonce budget. ChaCha20-Poly1305 and AEGIS have no write-count confidentiality bound below the index ceiling. The AES-128-GCM, AES-256-GCM, and ChaCha20-Poly1305 figures assume derived nonce mode, which these non-MRAE suites may use only in a write-once profile. In the default random nonce mode they bind earlier, at the random-nonce-collision budget of about 2^32 per epoch key (Table 17).¶
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 64 KiB segments. For AES-128-GCM this requires a sufficiently large epoch_length. At epoch_length 0 its epoch-key collision floor caps the object near 2^48 segments (Section 5.9.6). Reaching 2^63 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 distinct-derived-nonce budget of about 2^48. Rewrite capacity is a separate condition (Section 5.9.5).¶
A rewrite consumes the same budget as an initial write. How that budget is shared depends on the nonce mode.¶
For the random-nonce AEADs, 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:¶
| Suite / mode | epoch_length | Segments per epoch key | Rewrites per segment |
|---|---|---|---|
| AES-128-GCM, AES-256-GCM, random | 0 | 1 | ~2^32 |
| AES-128-GCM, AES-256-GCM, random | 6 | 64 | ~2^26 |
| AES-128-GCM, AES-256-GCM, random | 10 | 1024 | ~2^22 |
| ChaCha20-Poly1305, random | 0 | 1 | ~2^32 |
| ChaCha20-Poly1305, random | 6 | 64 | ~2^26 |
| ChaCha20-Poly1305, random | 10 | 1024 | ~2^22 |
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:¶
| Suite / mode | epoch_length | Segments per epoch key | Hot-segment rewrite cap |
|---|---|---|---|
| AES-256-GCM-SIV, derived | 0 | 1 | ~2^36 at 64 KiB |
| AES-256-GCM-SIV, derived | 6 | 64 | ~2^36 at 64 KiB |
| AES-256-GCM-SIV, derived | 10 | 1024 | ~2^36 at 64 KiB |
The hot-segment cap is length-dependent: about 2^36 at 64 KiB segments and about 2^38 at 16 KiB. The distinct-derived-nonce budget stays about 2^48 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. AEGIS uses a flat key, and its forgery budget, about 2^83 per key from [I-D.irtf-cfrg-aegis-aead], exceeds any reachable rewrite workload.¶
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. AEGIS has a 256-bit nonce and a high forgery floor, so a flat key (epoch_length 63) is the natural choice.¶
The cipher suite's default epoch_length, used when a referencing protocol does not choose one, is 0 for the suites with an epoch_length range and 63 for the AEGIS suites. epoch_length 0 derives a fresh epoch key per segment, the finest rotation, giving each segment the largest share of the random-nonce collision pool. The AEGIS 256-bit nonce makes a flat key safe. The named instantiations (Section 4.12) do not take this default: each pins an epoch_length tuned to its profile and object size, trading some rotation for fewer epoch keys while staying within budget.¶
The epoch keys are independent under the PRF security of the KDF ([RFC8645]), so no epoch key is weaker than another. The epoch_length parameter controls budget distribution, not key strength.¶
For AES-128-GCM the 128-bit AEAD key adds one further constraint. The epoch-key collision term (Appendix C.2) is about E^2/2^128 over the E distinct epoch keys, so an AES-128-GCM profile SHOULD keep distinct epoch keys per payload_key below about 2^48, which holds that term within the 2^-32 target. At epoch_length 0 this caps an AES-128-GCM object at about 2^48 segments, far below the 2^63 index ceiling, so it constrains only extreme object sizes.¶
The per-suite figures come from the bounds below.¶
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.9.7.4.¶
Nonce collisions alone do not exhaust the confidentiality bound. Following the AEAD usage-limits analysis ([I-D.irtf-cfrg-aead-limits] Section 5), block-cipher AEADs have a distinguishing bound that grows with the total number of cipher blocks processed under a key. For the AES-GCM suites (AES-128-GCM and AES-256-GCM), if s is the total number of AAD-plus-plaintext 128-bit blocks and q is the number of encryption queries under a key, the confidentiality advantage is at most:¶
CA <= ((s + q + 1)^2) / 2^129¶
For 65536-octet segments (L = 4096 blocks per segment), s = L * q in the worst case, so CA scales as q^2 * L^2 / 2^129. In random nonce mode the nonce-collision bound q^2 / 2^97 dominates for typical deployments because L^2 / 2^32 is small. In derived nonce mode the nonce-collision term vanishes and the block-size term is the binding confidentiality constraint.¶
For ChaCha20-Poly1305 there is no comparable block-size bound because ChaCha20 is a stream cipher, leaving nonce collision and forgery as the only relevant terms. Section 5.9.7.4 gives the derived-nonce block-size limits. No analogous block-size confidentiality bound has been published for AEGIS-256 or AEGIS-256X2. Nonce collision (negligible at a 256-bit nonce) and the per-key margin govern AEGIS confidentiality. The per-query forgery bound governs integrity.¶
SEAL implementations MUST compute usage budgets from the AEAD-specific confidentiality and integrity bounds, not from nonce-collision probability alone. Table 17 gives the binding limit per suite.¶
Each AEAD decryption query gives the adversary a chance to forge a valid ciphertext. The forgery advantage per query depends on the AEAD and segment_max.¶
For the AES-GCM suites (AES-128-GCM and AES-256-GCM) ([I-D.irtf-cfrg-aead-limits] Section 5.1):¶
IA <= 2 * v * (L + 1) / 2^128¶
where v is the number of forgery attempts and L = 4096 blocks per segment.¶
For ChaCha20-Poly1305 ([I-D.irtf-cfrg-aead-limits] Section 5.2):¶
IA <= v * (L' + 1) / 2^103¶
Here v is the number of forgery attempts and L' is the segment length in 16-octet Poly1305 blocks (about 4096 at 65536-octet segments), per [I-D.irtf-cfrg-aead-limits]. The 2^103 denominator (not 2^128) reflects Poly1305's per-query forgery bound. At 65536-octet segments, the integrity limit for ChaCha20-Poly1305 is tighter than for AES-GCM.¶
A derived-nonce MRAE AEAD has two limits the random-nonce analysis above does not capture.¶
Across the distinct segment nonces under one epoch key, the derived-key analysis of [RFC8452] Section 9 ([BHT18]) bounds the number of distinct derived nonces at about 2^48 for a 2^-32 advantage.¶
For a single hot segment, every rewrite reuses that segment's one derived nonce, so N rewrites of an L-block segment run AES-CTR under one derived per-record key. 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 * L blocks and q = N queries:¶
((s + q + 1)^2) / 2^129 <= 2^-32¶
The N * L term dominates, so this holds while N * L stays below about 2^48, giving N <= about 2^48 / L. That is about 2^36 rewrites of one 64 KiB segment (L = 4096) and about 2^38 at 16 KiB. This per-nonce cap does not divide by 2^epoch_length, because the segment reuses the same derived nonce regardless of epoch_length.¶
Distinct plaintexts under the fixed nonce produce distinct synthetic IVs and hence distinct counter keystreams (the GCM-SIV synthetic-IV construction, [RFC8452]), so confidentiality degrades 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.2.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. Each subsection below describes a way one of these can fail and what breaks.¶
Integrity across the SEAL suite is bounded by the 16-octet AEAD tag (Nt = 16): no suite member offers more than approximately 128 bits of forgery resistance per query (Table 17). The AEGIS algorithms' 256-bit keys raise confidentiality margins, not the tag-length forgery floor.¶
The table below maps common failure modes to the check that detects them and notes the cases where raAE provides no detection. The rows detected by SnapVerify assume a snapshot authenticator is configured (snap_id != 0x0000). Without one, those modes go undetected. The one exception is a dropped trailing segment: the per-segment finality requirement of Section 4.9.1.2 detects it in any profile, with no snapshot authenticator.¶
| Failure mode | Detected by |
|---|---|
| Wrong CEK, salt, or parameter set | The commitment, before any segment decryption, provided reader and writer use the same payload_info construction. |
| Modified ciphertext core or AEAD tag | AEAD.Decrypt for the affected segment. |
| Segment moved to a different index | AEAD.Decrypt: the segment index and finality bit are authenticated by segment_aad (random nonce mode) or by the nonce (derived nonce mode). |
| Segment copied from another object at the same index | Detected when the copied segment was encrypted under a different CEK or salt. Reusing a salt with the same CEK breaks this separation (Section 6.7). |
| Segment copied from another version at the same index | Not detected by per-segment AEAD alone; detected by SnapVerify, unless the whole object (including snapshot) is rolled back to that earlier valid version. |
| Dropped trailing segment(s) (truncation) | The finality requirement of Section 4.9.1.2, in any profile: the highest-indexed present segment then carries is_final = 0 and the decryptor rejects. Truncation to zero present segments is rejected by the zero-segment prohibition without a snapshot authenticator, and detected by SnapVerify with one (Section 6.6). |
| Missing interior, duplicated, reordered, or inserted segment | SnapVerify, which checks the segment count, finality, and recomputed snapshot value. |
| Stale snapshot value | SnapVerify, unless the stale value is part of a whole-object rollback to a previously valid snapshot. |
| Whole-object rollback | Not detected by raAE alone; the consuming protocol needs authenticated external freshness state. |
| Concurrent lost update | Not a cryptographic forgery; the consuming protocol needs writer serialization or another way to publish object state atomically. |
| Equality leakage in derived nonce mode | Not an authentication failure; rewriting equal plaintext at the same index under derived nonce mode 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 AES-GCM and ChaCha20-Poly1305, nonce reuse also recovers the polynomial 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 in a write-once profile that draws a fresh salt per object and never re-encrypts under it after a crash (Section 4.5.3.2). 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. Each SEAL AEAD additionally sets a default nonce mode (see Table 9). 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.5. 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 | random nonce | derived nonce |
|---|---|---|
| non-MRAE | valid | mutable: unsafe; write-once: valid |
| MRAE | wasteful | valid |
Rewrite is where the coupling matters. A derived nonce is fixed by the segment index, so a rewrite reuses it, while a random nonce is drawn fresh every time. A repeat is catastrophic for a non-MRAE AEAD and only equality-leaking for an MRAE one, which is what the table reflects. Section 4.5.3.2 gives the rules and their consequences.¶
The optional plaintext-bound construction (Appendix A) is an encryptor-side hedge over random nonce mode, not a separate nonce mode. See Section 4.10.1 for SEAL's per-AEAD guidance.¶
Snapshot integrity has the following limitations.¶
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.2.3: 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 over the segment tags authenticates the tag set, its positions, and the count. It does not confirm that a tag is a valid AEAD tag for the ciphertext beside it. Decrypting that 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.8).¶
Detecting these substitutions is a benefit of the design, not a limitation. It is the property that distinguishes raAE's snapshot integrity from per-segment AEAD authenticity.¶
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 not a separate limitation. It 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 under derived nonces, a combination the construction forbids (Section 4.5.3.2), would permit plaintext recovery.¶
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.9.7.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.¶
Rewriting introduces the following hazards.¶
Applications MUST track total segment encryptions per key and freeze the object before exceeding the budget in Section 5.9.5. For the 96-bit-nonce AEADs (AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305) the binding limit is the per-epoch-key collision pool of roughly 2^32 encryptions, counted across every segment that shares the key. Spread evenly, that pool gives each segment a uniform rewrite share of roughly 2^(32 - epoch_length), as tabulated in Section 5.9.5. See Section 5.9.6. Exceeding the per-epoch-key pool risks nonce collisions and plaintext recovery.¶
A mutable profile with derived nonces and an MRAE AEAD (SEAL-RW-v1) 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.9.5), 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.¶
For the masked multiset hash, when multiple writers rewrite different segments concurrently, each computes an independent accumulator difference (old_contrib XOR new_contrib). Applying these differences is commutative, but the read-modify-write on the stored accumulator requires coordination. Supplying that coordination, for example a compare-and-swap on the published snapshot value and segment count, is the consuming protocol's responsibility, consistent with the serialization and storage transactions this document places out of scope. The snapshot tag and the mask MUST be recomputed over the final accumulator and segment count inside the same critical section that publishes the snapshot update. Otherwise a storage layer can publish a masked accumulator and a snapshot tag that were computed for different accumulators. As a recovery mechanism, the accumulator can always be rebuilt from scratch by XOR-ing all contrib values and then re-masked.¶
Length changes are a separate concurrency case. Append and truncate change the segment count and re-mark a terminal segment. Those updates do not commute with each other or with a concurrent rewrite of the old or new terminal segment unless the consuming protocol serializes them.¶
Per-segment associated data A_i is rewritable, not fixed at creation. Unlike a global associated data value G, which the commitment fixes when the object is created (Section 4.6), A_i rides the per-segment AEAD associated data (Section 4.4.2), so a rewrite can replace a segment's A_i, for example to change a policy or version field. A reader therefore cannot treat A_i as context fixed at creation the way it can treat G. An application that needs context fixed at creation places it in G, which the commitment binds, rather than 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 HKDF-Extract and HKDF-Expand execute in constant time with respect to their key inputs. In practice this is satisfied by HMAC 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. SnapVerify MUST compare the full recomputed snapshot value, the masked accumulator and the snapshot tag together, in one constant-time comparison, so that no observable difference reveals which half differs (Section 4.7.4). 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.8).¶
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. It deliberately does not address four concerns that belong to the consuming protocol.¶
The CEK must remain available as long as any reader needs access, so there is no forward secrecy. Ciphertexts are not bound to any sender identity. 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. Finally, 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.¶
These concerns are not artifacts of raAE's design. Any raAE construction inherits them from the consuming protocol's scope.¶
Two applications that both reuse the same SEAL profile label (Section 4.10.2) 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 (Section 4.10.2) unchanged MAY use that profile's label.¶
This document has no IANA actions.¶
SEAL consumes identifiers from existing IANA registries: the AEAD
Algorithms Registry ([RFC5116]) for aead_id values and the HPKE KDF
Registry ([RFC9180] Section 7.2) for kdf_id values. No new
raAE-side registries are created.¶
The following code points are early allocations in their respective registries. Each registry reference firms up when the referenced I-D is published as an RFC, and the corresponding SEAL normative reference is a downref under IRTF stream conventions until then.¶
| Registry | Code point | Algorithm | Reference |
|---|---|---|---|
| AEAD Algorithms ([RFC5116]) | 0x0021 | AEGIS-256 | [I-D.irtf-cfrg-aegis-aead] |
| AEAD Algorithms ([RFC5116]) | 0x0024 | AEGIS-256X2 | [I-D.irtf-cfrg-aegis-aead] |
| HPKE KDF ([RFC9180] Section 7.2) | 0x0013 | TurboSHAKE-256 | [I-D.ietf-hpke-pq] |
Future SEAL profiles MAY consume additional entries from either registry without revising this document.¶
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(plaintext_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. Component vectors for both KDF classes are in Appendix E.19.¶
LH is the over-large-field digest of Section 4.3, which runs the cipher suite's native KDF primitive directly on its input and returns Nh octets. It is collision-resistant per Section 5.5. Decryptors never compute pt_digest.¶
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. No additional information beyond this equality is leaked.¶
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.¶
The construction is observationally equivalent to nonce_mode "random" at the wire format. A decryptor cannot distinguish whether the encryptor used a fresh CSPRNG call or this construction to produce the stored nonce. Implementations that elect to use this construction internally do not need to advertise it.¶
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. An optional encryptor-side construction that mixes plaintext content into the per-segment nonce derivation to defend against state duplication is described in Appendix A. That construction is orthogonal to hedging and the two MAY be combined.¶
This appendix is informative. It states what is proven, where, and by whom. Each result below is inherited from [FLRR25], argued in this document, or deferred to a companion proof paper in preparation ([SEALPROOFS]).¶
The ra-ROR and ra-CMT games, their advantages, and their proofs apply to SEAL's realization of the base interface, summarized in Section 5.6 and Section 5.7. The ra-CMT-p notion and its base proof are likewise inherited, but SEAL's realization of its position binding is argued here (see below).¶
The mechanisms this document adds beyond [FLRR25] are argued in the subsections below. These are the snapshot authenticator that realizes snapshot integrity, the nonce modes, the per-segment associated data and position binding of ra-CMT-p (bound stated here, formal accounting deferred), and the injectivity and domain separation of the KDF combiner. The combiner argument is structural and proof-complete (Section 4.3). The others state their bounds. Their full proofs are not here.¶
The capacity, rewrite, and maximum-object-size bounds (Section 5.9) are operational ceilings derived from [I-D.irtf-cfrg-aead-limits], [RFC8452] Section 9, and [BHT18], not security reductions proven here.¶
The formal proof of snapshot integrity as an extension of the ra-ROR framework, the real-or-random treatment of the derived-nonce equality leakage (which relaxes that framework's nonce-respecting hypothesis to deterministic MRAE), and the per-segment associated data and position accounting of ra-CMT-p.¶
The derived-nonce unique-nonce transform rests on TLS 1.3 ([RFC8446]) and STREAM ([HRRV15]) precedent, treated in Appendix C.6. That precedent does not prove SEAL-RO-v1. The KDF hierarchy, the arbitrary-position ra-ROR syntax, the ra-CMT commitment, the epoch accounting, and the object-level semantics remain SEAL-specific proof obligations.¶
The combiner's input encoding is injective and canonical, so distinct (protocol_id, label, ikm, info, L) tuples map to distinct primitive inputs and a given tuple has one encoding across implementations (Section 4.3, Section 5.5). Injectivity comes from the length-prefixed encoding, not from the KDF, which need not itself be injective. Collision resistance is assumed only of LH, and only for over-large fields. The commitment derivation carries its own separate collision-resistance assumption (Appendix C.4).¶
The capacity and usage limits in Section 5.9, including the maximum write-once object size (Section 5.9.4) and the derived nonce bounds (Section 5.9.7.4), are operational ceilings. They are referenced from [I-D.irtf-cfrg-aead-limits], [RFC8452] Section 9 with [BHT18], and the 2^63 segment-index ceiling, not proven here.¶
| 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 |
| KDF combiner injectivity and domain separation | this document (structural, proof-complete) | Section 4.3, Section 5.5 | injectivity of the length-prefixed encoding (KDF need not be injective); collision resistance of LH for over-large fields only |
| Snapshot integrity (masked multiset hash) | this document, Appendix C.5 | Appendix C.5 | mu-PRF KDF under snap_key; MAC unforgeability of snapshot_tag over (n_seg, acc); deterministic-masking birthday and mix-and-match terms; per-object separation via salt-bound snap_key |
| AEAD usage, rewrite, and max-object bounds | external work (operational, not proven here) | Section 5.9, Section 5.9.4, Section 5.9.7.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.6 | write-once uniqueness discipline reducing to nonce-respecting AEAD security |
| Derived-nonce equality leakage (MRAE) | this document; full proof in [SEALPROOFS] | Appendix C.6 | MRAE AEAD ([RFC8452]); per-segment fixed-nonce data-volume cap |
For a ra-ROR adversary (Section 5.4) 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 E 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_acc
¶
The terms in the bound are as follows.¶
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_acc 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 E epoch keys total (E = q_m when each message uses a single flat epoch key). This advantage absorbs a key-collision birthday term of about E^2/2^(8*Nk) over the E distinct keys. For the 256-bit-key suites (Nk = 32) that term is about E^2/2^256 and is negligible. For AES-128-GCM (Nk = 16) it is about E^2/2^128 and is the binding epoch-key collision floor. Section 5.9.6 bounds E 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.9.7.1. The derived-mode statement relies on the deterministic-MRAE analysis in Appendix C.6.¶
The masked multiset hash's snapshot forgery advantage, present only under snap_id 0x0001 and zero otherwise. SEAL authenticates the XOR accumulator (Section 4.7) with a snapshot tag, a MAC under snap_key, and masks the published accumulator with a deterministic tag-derived pad. A different snapshot authenticator contributes its own term in place of Adv_acc.¶
The Adv_acc term is bounded by:¶
Adv_acc <= Adv_muPRF_acc
+ q_s^2 / 2^(8*Nh) (snapshot-collision birthday)
+ q_v / 2^(8*Nh) (fresh-input tag forgery)
+ q_s * q_v / 2^(8*Nh) (mix-and-match accumulator guess)
¶
Here Adv_muPRF_acc is the snap_key subset of B_KDF's Level B queries, counted here and not again in Adv_muPRF(KDF, B_KDF). q_s is the number of published snapshot states the adversary observes and q_v the number of SnapVerify queries it makes. The q_s^2 birthday term is the cost of the deterministic masking (Appendix C.5). The dominant terms are that birthday and the mix-and-match q_s * q_v / 2^(8*Nh), and for the SEAL suites (8*Nh of 256, 384, or 512) every term is negligible.¶
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, then the payload_key- and snap_key-keyed outputs, with uniformly random values (Adv_muPRF at the two keying levels). 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.6). The snapshot authenticator's forgery enters separately as Adv_acc (Section 5.8). 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. The collision-resistance assumption captured in Section 4.5 is what makes the reduction sound.¶
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 gives about 2^128 (capped by SHA-256's own 2^128 collision resistance for HKDF-SHA-256), 48 octets gives about 2^192 (capped by SHA-384's 2^192 for HKDF-SHA-384), and 64 octets gives about 2^256 (for HKDF-SHA-512, capped by SHA-512's 2^256, and for TurboSHAKE-256). 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 for ra-CMT-p is inherited from the underlying AEAD's commitment level (Section 5.7). The notion and base proof are inherited from [FLRR25]. The exact per-segment associated data and position accounting is deferred to [SEALPROOFS].¶
SEAL's snapshot authenticator (snap_id 0x0001) is the MSet-XOR-Hash of Clarke et al. ([MSetHash]) with a deterministic mask this document adds. Each segment contributes a keyed KDF evaluation of its index and tag, the contributions XOR into an accumulator, and the published value masks that accumulator under snap_key. A different authenticator that meets the requirement of Section 5.5 carries its own argument.¶
The adversary has full read and write access to the stored segments, their metadata, and the snapshot, and wins by making SnapVerify accept a (segment set, snapshot) pair other than the writer's current state. Whole-object rollback to an earlier honest state is excluded and is the application's freshness responsibility (Section 6.6).¶
Publishing the accumulator in the clear would be insecure. That is the MSet-XOR-Hash with its mask removed, which is only set-collision resistant. A write adversary reads the accumulator across honest rewrites, collects the contribution differences old_contrib XOR new_contrib, and after more than 8*Nh of them solves a GF(2) system for a subset of segments it can revert without changing the accumulator. The current snapshot tag still verifies, so a non-historical mixture of versions is accepted with no MAC forgery.¶
The deterministic mask defeats this attack. Here wrapped_acc = acc XOR snapmask(n_seg, snapshot_tag) hides the accumulator behind a one-time pad keyed by snap_key and seeded by the synthetic snapshot tag, the synthetic-IV derandomization of deterministic authenticated encryption ([DAE]). Two published states that collide on (n_seg, acc), or on the snapshot tag at equal count, would expose a raw accumulator difference. Over q_s published states that costs a birthday term q_s^2 / 2^m, with m = 8*Nh. Off that event the masks are independent one-time pads, the published transcript is independent of every accumulator, and the recombination above has no linear system left to solve. Security reduces to the PRF security of the KDF. The mask protects the accumulator only against a verifier that returns one bit, which is why SnapVerify does not surface the recovered accumulator (Section 4.7.4).¶
A forgery is then one of two events. Either a fresh snapshot-tag input is guessed, a MAC forgery bounded by q_v / 2^m over q_v verifications, or a different segment set is made to hit a published accumulator by chance, a mix-and-match bounded by q_s * q_v / 2^m against q_s published states. The mix-and-match term is a set collision on the keyed contribution function and relies on the set-collision resistance of the MSet-XOR-Hash-style construction ([MSetHash]). The formal bound for this term is discharged in [SEALPROOFS]. Collecting the terms, the snapshot forgery advantage Adv_acc of Appendix C.2 is¶
Adv_acc <= Adv_muPRF_acc + q_s^2 / 2^m + q_v / 2^m + q_s * q_v / 2^m¶
with m = 8*Nh and dominant terms q_s^2 / 2^m and q_s * q_v / 2^m, both negligible at Nh of 32, 48, or 64. The argument relies on segment AEAD authenticity, already charged as Adv_muROR, and adds no term beyond Adv_acc. Per-object separation comes from the salt-bound snap_key, and the authenticator needs no assumption beyond the multi-user PRF the key schedule already uses. It does not need the commitment's collision resistance. Integrating this reduction into the combined ra-ROR proof is in preparation ([SEALPROOFS]).¶
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. Under the write-once uniqueness discipline every derived nonce is distinct, which reduces unique-nonce record protection to nonce-respecting AEAD security. This is precedent, not a SEAL-specific proof.¶
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. Several considerations motivate this.¶
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.¶
This matters even for content written once: large content with many segments can place many AEAD invocations under a single key.¶
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 immutable profile SEAL-RO-v1 the write-once rule keeps every derived nonce unique, so derived mode pairs with any AEAD. Table 9 gives the default nonce_mode each suite uses in the mutable profile, and a profile MAY select another valid (nonce_mode, snap_id) tuple.¶
Three shapes were considered for whole-object integrity over an updatable segment set. A MAC over the concatenated tag list is the simplest, but a rewrite changes one tag in the middle of the input, so the writer recomputes over all n_seg tags on every update. A Merkle tree over the tags updates in O(log n) and can offer per-segment inclusion proofs, but it either stores interior nodes that grow with the object or re-reads segment tags to rebuild paths, and it rests on collision resistance of a hash. The masked multiset hash updates in O(1) per rewrite, stores a single value of 2*Nh octets regardless of object size, needs no per-segment proofs, and needs only the multi-user PRF assumption the key schedule already carries. Its costs are the ones this document states explicitly: the accumulator stays masked, the verifier returns a single bit (Section 4.7.4), and the deterministic mask adds the q_s^2 birthday term to the bound (Appendix C.5). A profile that needs per-segment inclusion proofs or third-party verifiability needs a different authenticator under its own snap_id (Section 4.7.1).¶
The accumulator is on the wire so a stateless writer can resume the O(1) update from the stored snapshot alone, rather than holding it in trusted state or re-reading every tag. Publishing it is why the mask and the one-bit verifier are needed: an exposed accumulator would let a write adversary recombine observed values (Appendix C.5).¶
This appendix is informative.¶
All vectors use protocol_id = "SEAL-RW-v1", CEK = 32 octets of 0xAA, and salt = 32 octets of 0x04. Each block opens with its parameter set, grouped into three buckets: the cipher suite (aead_id, kdf_id), the geometry (segment_max), and the operational parameters (epoch_length, nonce_mode, snap_id). aead_id and kdf_id are the 2-octet IANA code points from Table 9 and Table 10. Then come the payload_info elements as encoded on the wire, the payload schedule outputs, the per-segment values, and the snapshot fields. Hexadecimal values wrap at 16 octets per line.¶
The snapshot fields are the internal accumulator acc, the mask snapmask(n_seg, snapshot_tag), the published wrapped_acc = acc XOR mask, and the snapshot tag. The snapshot stored on the wire is wrapped_acc || snapshot_tag (Section 4.7.4). The accumulator is an intermediate value and is not on the wire, and the count is recovered from the segment set.¶
Single-segment plaintexts are "Hello, SEAL!" (12 octets). Two-segment messages append "Two segments of SEAL" (20 octets) as the final segment. Most vectors use nonce_mode "random" with stored nonces 0x03 and 0x07 repeated to the AEAD nonce length. The derived-nonce vector in Appendix E.17 instead recomputes each nonce from nonce_base, and the cross-epoch vector in Appendix E.18 sets epoch_length 0 and exposes the intermediate epoch_key and segment_key for each segment. Appendix E.1 gives the full KDF trace for one commitment. The other blocks list schedule outputs only. The eighteen computed positive vectors, plus the negative SnapVerify vector in Appendix E.20, are published byte-for-byte as raae-v1-vectors.json in the draft repository.¶
The vectors are organized by purpose:¶
Appendix E.1 shows one complete HKDF-SHA-256 trace, including the commitment KDF inputs. Use it to debug framing, payload_info construction, commitment derivation, segment AAD, accumulator contribution, and snapshot_tag computation.¶
Appendix E.3 exercises the KDF combiner in isolation and demonstrates encode() injectivity, including the empty-sequence and same-octets-different-grouping cases. The 38 combiner vectors are published as raae-v1-combiner-kdf-vectors.json.¶
The single-segment and two-segment vectors exercise the listed AEAD and segment-size combinations with HKDF-SHA-256. Use these to check AEAD code points, segment_max encoding, nonce storage, and finality-bit handling.¶
Appendix E.15 covers the TurboSHAKE-256 KDF suite and its 64-octet Nh outputs.¶
Appendix E.16 shows a segment rewrite and the corresponding accumulator and snapshot_tag update.¶
Appendix E.17 covers AES-256-GCM-SIV in derived nonce mode, where the nonce is recomputed rather than stored.¶
Appendix E.18 shows epoch_length = 0, the finest rotation, and exposes per-segment epoch_key and segment_key values. The AEGIS-256 blocks exercise the opposite endpoint, the flat key at epoch_length = 63.¶
Appendix E.19 exposes the component values of the optional plaintext-bound nonce construction, which no end-to-end vector can reach.¶
Appendix E.2 pins the commitment's G input at its empty default, which equals the Appendix E.1 commitment, and at a nonempty value.¶
All vectors use empty per-segment A_i. As specified in Section 4.4.2, an empty A_i is omitted from the encoding rather than encoded as a zero-length fourth element.¶
Appendix E.20 changes the accumulator without recomputing the snapshot tag; SnapVerify rejects the stored snapshot.¶
The JSON file is the complete corpus for automated tests. The text below is intended for debugging and review.¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 16384
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00004000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
47ea0ec7409b9b95d676019917a19f1c
5831eb236aba459063458e525d130d0c
payload_key (32 octets):
c1f2663e99977428dc0fec1566ce15e9
1398634ab9b1d004945de48560707062
snap_key (32 octets):
bc314ecaa8ff6c1c4ebc13b54597a10d
2bcf412b40c428a0e411a828fcfb52ef
Segment 0 (is_final=1):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000101
ciphertext (12 octets):
6a2b84e72ce8edbf4259eebd
tag (16 octets):
a821834621b302b0eb00b0245fff2efb
contrib (32 octets):
97b74013b135f6fe1739da05e1720b90
dc596a6d09e29bfe437bb710391c9ba8
accumulator (32 octets):
97b74013b135f6fe1739da05e1720b90
dc596a6d09e29bfe437bb710391c9ba8
mask (32 octets):
9356a1f7905b40b4561315a6892503d6
2b0c7162aa19285f939325709f5847fb
wrapped_acc (32 octets):
04e1e1e4216eb64a412acfa368570846
f7551b0fa3fbb3a1d0e89260a644dc53
snapshot_tag (32 octets):
4e7bb00b4216798e02e511b26f0167c2
a3f6c791407994e1f503f3923e591438
KDF trace for the commitment (HKDF-SHA-256):
extract_input = encode(protocol_id, "commit", CEK):
000a5345414c2d52572d76310006636f
6d6d69740020aaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaa
prk = HKDF-Extract(salt = protocol_id, extract_input):
8eb0007f1e8ac96904742b4fb4448aba
9d8a4b319f72f7e4a6d7bb8aab95737d
expand_info = encode(protocol_id, "commit", ...payload_info, G,
I2OSP(32, 2)):
000a5345414c2d52572d76310006636f
6d6d6974000200020004000040000002
00010002000100010000010100200404
04040404040404040404040404040404
04040404040404040404040404040000
00020020
commitment = HKDF-Expand(prk, expand_info, 32):
47ea0ec7409b9b95d676019917a19f1c
5831eb236aba459063458e525d130d0c
¶
These vectors pin the G input (Section 4.6) against the schedule of Appendix E.1: the same CEK, salt, and payload_info with G at its empty default and at a nonempty value. The default case equals the Appendix E.1 commitment, since every commitment includes the G element. These values are printed here only and are not part of the end-to-end corpus.¶
G default (empty):
commitment (32 octets):
47ea0ec7409b9b95d676019917a19f1c
5831eb236aba459063458e525d130d0c
(the Single Segment, AES-256-GCM, HKDF-SHA-256, 16384
commitment)
G = "raae-demo-g":
G (11 octets):
726161652d64656d6f2d67
commitment (32 octets):
d8eedb1fa0f77428cc33d252eb307796
ae3bb911c2f6ea7a9e5b0bde312afd73
¶
These vectors exercise the KDF combiner (Section 4.3) in isolation. They demonstrate that encode() is injective over (protocol_id, label, ikm, info, L): distinct inputs, including inputs whose octets concatenate to the same string under a different grouping, produce distinct framed inputs and therefore distinct outputs. The full set of 38 combiner vectors, covering both KDF classes and every SEAL label, is published byte-for-byte as raae-v1-combiner-kdf-vectors.json in the draft repository. All blocks below use ikm equal to a single 32-octet element of 0xAA. Hexadecimal values wrap at 16 octets per line.¶
The first pair frames the same five info octets 01 02 03 04 05 two ways. Because encode length-prefixes each element, the two expand_info values differ, so the outputs differ. The second pair shows that the empty sequence and a one-element sequence whose element is the empty octet string are distinct. The third pair repeats the distinction in the one-step form, where encode(...ikm) and encode(...info) are each a single nested element of the message.¶
KDF.29 HKDF-SHA-256, label "commit" (info = [010203, 0405])
expand_info = encode(protocol_id, "commit", ...info,
I2OSP(32, 2)):
000a5345414c2d52572d76310006636f
6d6d6974000301020300020405000200
20
output (32 octets):
0dfb8948fcc220f61f43f291648903b7
1fe6e5208647b6e18d3308f59fca0fa0
KDF.30 HKDF-SHA-256, label "commit" (info = [0102, 030405])
expand_info = encode(protocol_id, "commit", ...info,
I2OSP(32, 2)):
000a5345414c2d52572d76310006636f
6d6d6974000201020003030405000200
20
output (32 octets):
299aa40869ae880bc8a064bb5afe38c4
13f420ff30bcbfac7651d5e248b3db98
KDF.8 HKDF-SHA-256, label "commit" (info = [], the empty sequence)
expand_info = encode(protocol_id, "commit", ...info,
I2OSP(32, 2)):
000a5345414c2d52572d76310006636f
6d6d697400020020
output (32 octets):
90073e3e9f1c855c2b7460e851f75d1a
063a1daf007f81e4a695da1a0f97fca6
KDF.28 HKDF-SHA-256, label "commit" (info = [""], one empty element)
expand_info = encode(protocol_id, "commit", ...info,
I2OSP(32, 2)):
000a5345414c2d52572d76310006636f
6d6d6974000000020020
output (32 octets):
5a4b4b2d59f5989c598f05a0a448acda
faf27e10914c3894430ee7482c9a1913
KDF.33 TurboSHAKE-256, label "commit" (info = [010203, 0405])
encoded_input = encode(protocol_id, "commit",
encode(...ikm), encode(...info),
I2OSP(64, 2)):
000a5345414c2d52572d76310006636f
6d6d697400220020aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa0009000301020300
02040500020040
output (64 octets):
dcefbcdced8b413e18303d2ffe1cac63
44ee71b13324caa91d1712efc6b81ca8
9ce2b62ce3aedde0ed16e14d7d17e2f2
bc69f5e856eb96f9e4845f8522b0a9b5
KDF.34 TurboSHAKE-256, label "commit" (info = [0102, 030405])
encoded_input = encode(protocol_id, "commit",
encode(...ikm), encode(...info),
I2OSP(64, 2)):
000a5345414c2d52572d76310006636f
6d6d697400220020aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa0009000201020003
03040500020040
output (64 octets):
02958e6256193c71b0cc4b8ac3273b31
15d1ac30fd9aab537ad6916206be9828
9146b56526054b3c11e22f2c375d0b24
43e8d2ad2e6a89b874aa9ef4ce2c9c77
¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
9285553e10209c27bb5858b621426513
b0832f26d7ee813d9dd62c218ce6972a
payload_key (32 octets):
bb78da70d5e99d36c78e8a8b1a79b620
e4a4250dd6b471024c379917dfbb2de7
snap_key (32 octets):
953950ab75bdefd67ef15bbd7665b8af
d3c9ced50ce7cb369e789606fc455025
Segment 0 (is_final=1):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000101
ciphertext (12 octets):
1815f12b13f7ee2532f0fcca
tag (16 octets):
df4b1428af3c5ecb6d804159fec249e0
contrib (32 octets):
73e62d2574a38dc44b406a0c2f2d57b1
2b7ca777b053cdbb4e9d6f1b3257991a
accumulator (32 octets):
73e62d2574a38dc44b406a0c2f2d57b1
2b7ca777b053cdbb4e9d6f1b3257991a
mask (32 octets):
dfd59806ded61dbf83bab8a7e143da0e
01714d207c2f53af86e00c875590093b
wrapped_acc (32 octets):
ac33b523aa75907bc8fad2abce6e8dbf
2a0dea57cc7c9e14c87d639c67c79021
snapshot_tag (32 octets):
5a7713eab7ce2f7f246647aa407e14fa
f295a04333f06c27cdc1193252a9b8bc
¶
Parameter set:
cipher suite:
aead_id 0x001d (AEAD_CHACHA20_POLY1305)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 16384
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 001d
segment_max_be ( 4 octets): 00004000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
9ef7166bbce42787fd834f79d29f85b6
6a050b24f372ecfb79a66b3f2fdc1acb
payload_key (32 octets):
d0f1d392a371642db684a23858c0193c
2d7406cb4360c81ef9190391cacf885f
snap_key (32 octets):
b156708dc559791d78014bae5e01b5fd
f8a397c2d140fd9b9468e3cceeb8aa5d
Segment 0 (is_final=1):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000101
ciphertext (12 octets):
f1d968bc047a7bf6a15ef400
tag (16 octets):
567deca2d91b732fd1f814a65335df79
contrib (32 octets):
235bfbd70b8ce751d3720bea8351f039
dac89c0055d817a5b949a8582c590035
accumulator (32 octets):
235bfbd70b8ce751d3720bea8351f039
dac89c0055d817a5b949a8582c590035
mask (32 octets):
3beeed96dd10cd4bc3ec5f439a789d84
acd1542b0edabd0c48c1bfb4766af53e
wrapped_acc (32 octets):
18b51641d69c2a1a109e54a919296dbd
7619c82b5b02aaa9f18817ec5a33f50b
snapshot_tag (32 octets):
b7a4b41dfb8de76d9ebaf0833f72d03b
277bab9453c9085553dee456c998f4b7
¶
Parameter set:
cipher suite:
aead_id 0x001d (AEAD_CHACHA20_POLY1305)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 001d
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
ed76666a233fc9724c82f209aea191fa
bdf8e65f12fa97f0a4e317839ee56f19
payload_key (32 octets):
a859c3a684d35378bbbcf7ed48286313
3e3af8d2cbf8d40687d693243c32cdea
snap_key (32 octets):
aa42a852946818754780e48a9209a451
345367bd07a04ad794c62a703366aa90
Segment 0 (is_final=1):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000101
ciphertext (12 octets):
db9cf72ba226e3210aa9fcb5
tag (16 octets):
f5048c0d08372770fe066f2b5c052ab8
contrib (32 octets):
aec6ba40c8e7234e704d3a689866dcb0
efb84a6f0c75e27ea45756486d2b9a72
accumulator (32 octets):
aec6ba40c8e7234e704d3a689866dcb0
efb84a6f0c75e27ea45756486d2b9a72
mask (32 octets):
b372584c8a52e2daec61e6b3b2740b87
c56913270ebf83456636645bccbd125c
wrapped_acc (32 octets):
1db4e20c42b5c1949c2cdcdb2a12d737
2ad1594802ca613bc2613213a196882e
snapshot_tag (32 octets):
2eb1e1d010fa697a4c577445e36aacc6
3cfbaafc893b8cd6730099c560d3524a
¶
Parameter set:
cipher suite:
aead_id 0x0021 (AEAD_AEGIS256)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 16384
operational:
epoch_length 63
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0021
segment_max_be ( 4 octets): 00004000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 3f
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
c4e0d853da06a0d7da0f062952ce8c1d
c9936ec06b883accd2117aed9475f0cb
payload_key (32 octets):
7b69e4c6a70d806c97315c4f37e698f8
ad104677c20b4336ad81c9de7544246a
snap_key (32 octets):
fb987468cf2e7f1321b8130b68933c7f
039a39b0606fc7bc106d3169a9323d73
Segment 0 (is_final=1):
nonce (32 octets):
05050505050505050505050505050505
05050505050505050505050505050505
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000101
ciphertext (12 octets):
91c6c2100dd9d365e5a6df47
tag (16 octets):
f48dd988d409eb7b90b4fe6447ec7b25
contrib (32 octets):
176e02c54d994c99f50928a7c67386fb
ea17424dbcf70230e9f4bea73f5265a4
accumulator (32 octets):
176e02c54d994c99f50928a7c67386fb
ea17424dbcf70230e9f4bea73f5265a4
mask (32 octets):
5e9bb90f0671afe58fb72584dce62ae6
6bfaa33159a97eefc2de2304c8b2528a
wrapped_acc (32 octets):
49f5bbca4be8e37c7abe0d231a95ac1d
81ede17ce55e7cdf2b2a9da3f7e0372e
snapshot_tag (32 octets):
cb35b42e68a265b2c6a09de9b381044b
29f46e98342a44f6eaae3eb8f7470789
¶
Parameter set:
cipher suite:
aead_id 0x0021 (AEAD_AEGIS256)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 63
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0021
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 3f
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
6bb3b3000bbdba28de3a8fcf29fd862e
094e2e28c6df0d677aeba07ab747fe18
payload_key (32 octets):
425c1db85e1d3c85025ab41d5e263db9
c4969b4599942fa582d2394f0c5870e4
snap_key (32 octets):
4bb7fb2a1e5036cbe9aa018af5b5fe56
2fd60a1c388c54168621a40530ae4237
Segment 0 (is_final=1):
nonce (32 octets):
05050505050505050505050505050505
05050505050505050505050505050505
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000101
ciphertext (12 octets):
f53c634ccf8aab1157fa44fd
tag (16 octets):
90b6fa24f3cd9cb2b18574536f7bfdfc
contrib (32 octets):
0b649e2b2702281a67ac5ab72c4b8e68
afbe4135ee9a5fe693f34b6383377a24
accumulator (32 octets):
0b649e2b2702281a67ac5ab72c4b8e68
afbe4135ee9a5fe693f34b6383377a24
mask (32 octets):
462e3c1a58d6f5f665988d3a28769729
3bb3fa914b43a3d99b69da1b9491d1a3
wrapped_acc (32 octets):
4d4aa2317fd4ddec0234d78d043d1941
940dbba4a5d9fc3f089a917817a6ab87
snapshot_tag (32 octets):
34303daf7ba4f37eeddd4fd2bb382bb2
439ceaecb5c240fc2839e2602d3a7d33
¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 16384
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00004000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
47ea0ec7409b9b95d676019917a19f1c
5831eb236aba459063458e525d130d0c
payload_key (32 octets):
c1f2663e99977428dc0fec1566ce15e9
1398634ab9b1d004945de48560707062
snap_key (32 octets):
bc314ecaa8ff6c1c4ebc13b54597a10d
2bcf412b40c428a0e411a828fcfb52ef
Segment 0 (is_final=0):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
6a2b84e72ce8edbf4259eebd
tag (16 octets):
1574936244d54aedf589c87002dbac90
contrib (32 octets):
322c4622cd9f50552740d1eee7a530eb
3fd371e44178b8ce18816b53dd3f1587
Segment 1 (is_final=1):
nonce (12 octets):
070707070707070707070707
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
50b0eda5d7a86a5a94e501f317020173
6d755d56
tag (16 octets):
f22fb2b4bfed679d1d90d2a0814e429c
contrib (32 octets):
7455e0472187dcd04a5747d508587a8c
9f3e281818188be0edd46be8e32d4a6e
accumulator (32 octets):
4679a665ec188c856d17963beffd4a67
a0ed59fc5960332ef55500bb3e125fe9
mask (32 octets):
ad55f19bb8997c8ad0def92830f0ce09
4542ec804f7b99e66f294164985979a8
wrapped_acc (32 octets):
eb2c57fe5481f00fbdc96f13df0d846e
e5afb57c161baac89a7c41dfa64b2641
snapshot_tag (32 octets):
a08c73b5c414542cc06830d893d0eaca
c749418dea32b11d5cc121d6b2db93b8
¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
9285553e10209c27bb5858b621426513
b0832f26d7ee813d9dd62c218ce6972a
payload_key (32 octets):
bb78da70d5e99d36c78e8a8b1a79b620
e4a4250dd6b471024c379917dfbb2de7
snap_key (32 octets):
953950ab75bdefd67ef15bbd7665b8af
d3c9ced50ce7cb369e789606fc455025
Segment 0 (is_final=0):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
1815f12b13f7ee2532f0fcca
tag (16 octets):
2597e2f2243b98c4bb7f320dc2f46ce3
contrib (32 octets):
0d8b3cb23192377e88232945f623150b
1a1c0b61745a4fa39a5f65e162b6e672
Segment 1 (is_final=1):
nonce (12 octets):
070707070707070707070707
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
f17c9cac3693dcb4bdd524714da804d4
c4390056
tag (16 octets):
97d4f0d1303a2b112eac5aae081ef6bf
contrib (32 octets):
a8e8df627852127e058558353da2def9
9a83928b4c96ef37a5d0b08068b1a87c
accumulator (32 octets):
a563e3d049c025008da67170cb81cbf2
809f99ea38cca0943f8fd5610a074e0e
mask (32 octets):
946a2744cf3fd572a03993d434705704
47d1ce03f4769035a5058780660e5eff
wrapped_acc (32 octets):
3109c49486fff0722d9fe2a4fff19cf6
c74e57e9ccba30a19a8a52e16c0910f1
snapshot_tag (32 octets):
5ce50c9e90db4bbc28297372e401625c
2e43203ce8008c452ea4355f0941ef67
¶
Parameter set:
cipher suite:
aead_id 0x001d (AEAD_CHACHA20_POLY1305)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 16384
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 001d
segment_max_be ( 4 octets): 00004000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
9ef7166bbce42787fd834f79d29f85b6
6a050b24f372ecfb79a66b3f2fdc1acb
payload_key (32 octets):
d0f1d392a371642db684a23858c0193c
2d7406cb4360c81ef9190391cacf885f
snap_key (32 octets):
b156708dc559791d78014bae5e01b5fd
f8a397c2d140fd9b9468e3cceeb8aa5d
Segment 0 (is_final=0):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
f1d968bc047a7bf6a15ef400
tag (16 octets):
0359d2adbe709e374e6fae7830001295
contrib (32 octets):
92c78ada192f60346b35491986711454
9e20d72ede4b78a9567cf414365bc781
Segment 1 (is_final=1):
nonce (12 octets):
070707070707070707070707
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
4d8f7a1b6aa589808c384a52bd90e4d0
a44ed35e
tag (16 octets):
61c3caca0ecda929269f52978f4a60e1
contrib (32 octets):
85db0789dfd84dfaeb095fc28101d929
af58800d6ae8882353c83c9a6013492c
accumulator (32 octets):
171c8d53c6f72dce803c16db0770cd7d
31785723b4a3f08a05b4c88e56488ead
mask (32 octets):
e399bc51986eaf7f8d7cbcb3e76da43a
b9607c01e413dd462639c4fd50191f32
wrapped_acc (32 octets):
f48531025e9982b10d40aa68e01d6947
88182b2250b02dcc238d0c730651919f
snapshot_tag (32 octets):
f4bfbdff53178451463ef23c73a31ab5
06a6acfb49282af14598b2f49f32ceeb
¶
Parameter set:
cipher suite:
aead_id 0x001d (AEAD_CHACHA20_POLY1305)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 001d
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
ed76666a233fc9724c82f209aea191fa
bdf8e65f12fa97f0a4e317839ee56f19
payload_key (32 octets):
a859c3a684d35378bbbcf7ed48286313
3e3af8d2cbf8d40687d693243c32cdea
snap_key (32 octets):
aa42a852946818754780e48a9209a451
345367bd07a04ad794c62a703366aa90
Segment 0 (is_final=0):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
db9cf72ba226e3210aa9fcb5
tag (16 octets):
6423011bc81f13a30ef51ff32f209cff
contrib (32 octets):
fab6f671b544bb356337a2b811475663
cc5fab22c29a6770c6cd035bc44d0554
Segment 1 (is_final=1):
nonce (12 octets):
070707070707070707070707
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
85ead0c3a974dd014622ec0e2ba9a6e6
199a7b88
tag (16 octets):
3ee3399f906110b8e7900805f2b72a85
contrib (32 octets):
361a4ce5127403db2e54950d48a81af1
a71913771db2885bcb6bcdab971a3611
accumulator (32 octets):
ccacba94a730b8ee4d6337b559ef4c92
6b46b855df28ef2b0da6cef053573345
mask (32 octets):
b62dbfa20e0725e6cc0bb81b169f6294
a8b1d7b94968ac589c9b7ebcffc37882
wrapped_acc (32 octets):
7a810536a9379d0881688fae4f702e06
c3f76fec96404373913db04cac944bc7
snapshot_tag (32 octets):
e0b3b1cadba56bb4994d7872b57aff7f
42215a8e2b7a2241cffd5107f571d560
¶
Parameter set:
cipher suite:
aead_id 0x0021 (AEAD_AEGIS256)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 16384
operational:
epoch_length 63
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0021
segment_max_be ( 4 octets): 00004000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 3f
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
c4e0d853da06a0d7da0f062952ce8c1d
c9936ec06b883accd2117aed9475f0cb
payload_key (32 octets):
7b69e4c6a70d806c97315c4f37e698f8
ad104677c20b4336ad81c9de7544246a
snap_key (32 octets):
fb987468cf2e7f1321b8130b68933c7f
039a39b0606fc7bc106d3169a9323d73
Segment 0 (is_final=0):
nonce (32 octets):
05050505050505050505050505050505
05050505050505050505050505050505
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
91c6c2100dd9d365e5a6df47
tag (16 octets):
c4837c14751196e622301d3e8a5e00b2
contrib (32 octets):
002805d6180149a3852b8afd82b40db9
4d4e2af23a11bfb1e2922e5e3efb4210
Segment 1 (is_final=1):
nonce (32 octets):
09090909090909090909090909090909
09090909090909090909090909090909
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
006704b222fa9285da01b706be831b76
ead7332c
tag (16 octets):
9965b57015a2b058514fb1fca7708242
contrib (32 octets):
f1edcc4f336fb96b7319d30bca9a9194
e6795e34a3a0b19652521d749dd2e767
accumulator (32 octets):
f1c5c9992b6ef0c8f63259f6482e9c2d
ab3774c699b10e27b0c0332aa329a577
mask (32 octets):
0affb88aa98af47cf91aa6c80b733679
070bc7b44df36f22859f0c89a91a3cc9
wrapped_acc (32 octets):
fb3a711382e404b40f28ff3e435daa54
ac3cb372d4426105355f3fa30a3399be
snapshot_tag (32 octets):
3d8899806c647a198c1c4e23f6ce8a2e
be5d8cba11d65a16bbd2e7d5c4f415fc
¶
Parameter set:
cipher suite:
aead_id 0x0021 (AEAD_AEGIS256)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 63
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0021
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 3f
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
6bb3b3000bbdba28de3a8fcf29fd862e
094e2e28c6df0d677aeba07ab747fe18
payload_key (32 octets):
425c1db85e1d3c85025ab41d5e263db9
c4969b4599942fa582d2394f0c5870e4
snap_key (32 octets):
4bb7fb2a1e5036cbe9aa018af5b5fe56
2fd60a1c388c54168621a40530ae4237
Segment 0 (is_final=0):
nonce (32 octets):
05050505050505050505050505050505
05050505050505050505050505050505
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
f53c634ccf8aab1157fa44fd
tag (16 octets):
c466896ca6da6509959b1c0c10dd688d
contrib (32 octets):
e558127502262f71ac5ce07b9e0e101f
177a5428a10ecb464cfcd731d75b35fe
Segment 1 (is_final=1):
nonce (32 octets):
09090909090909090909090909090909
09090909090909090909090909090909
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
40865396160e5aa905a96d3a6552f6cc
fecf30ec
tag (16 octets):
ddd6e4979a6d648400b236abc617d033
contrib (32 octets):
23f4cfab2a5601a75d7f87148c5f32fd
d8ab5ac1a16d25c9688e61b7d09171bb
accumulator (32 octets):
c6acddde28702ed6f123676f125122e2
cfd10ee90063ee8f2472b68607ca4445
mask (32 octets):
8d8832a7c865352b6ebcfabaa1c85c69
0aacfbfb2bf08a73cc090281bea1ca42
wrapped_acc (32 octets):
4b24ef79e0151bfd9f9f9dd5b3997e8b
c57df5122b9364fce87bb407b96b8e07
snapshot_tag (32 octets):
016300d12db61dc1b6b11fbeee8ecaf8
614efe5802f3fc919ab8aa665cdf9294
¶
These two vectors exercise the one-step TurboSHAKE-256 cipher suite (Table 10), for which Nh = 64. The commitment, snap_key, contrib, the accumulator, and the snapshot tag are therefore 64 octets, while payload_key (Nk) and the nonces (Nn) are unchanged.¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0013 (TurboSHAKE-256)
geometry:
segment_max 65536
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0013
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (64 octets):
b19991b71ed275d98070eab735179d60
be397354a85f6d6f58e74dcb90f0ff43
8271da594a267d81aaa74a88736ba549
d9e88c1b9d9a972135220b76c9568483
payload_key (32 octets):
bdecddf1340029c520b9b4a9e1b15144
d283209261a58113294728e337d14ea8
snap_key (64 octets):
92fc2e47ac72bbdbcac62a67ced07dad
a1e907bd82e92a68ba5f6098ec067931
e59683904d5213ddd0abe237ac0f9450
ef33180028f2ea7e47d738e6f3faed01
Segment 0 (is_final=1):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000101
ciphertext (12 octets):
429b98993decaa7c1792d52d
tag (16 octets):
be8cd796db427f283859fa6722708d5b
contrib (64 octets):
371669ebb5f158b73bd65f0f66a33878
3e2372d1cbfced76ff866e264fb6c85f
151dcdf2662d286cc065844a2b5aff57
200d689ad4ee7d027df20be43747bbf2
accumulator (64 octets):
371669ebb5f158b73bd65f0f66a33878
3e2372d1cbfced76ff866e264fb6c85f
151dcdf2662d286cc065844a2b5aff57
200d689ad4ee7d027df20be43747bbf2
mask (64 octets):
39bc62cc8b75a8745da4cf36061350a1
a1c439359cde43df5355ce7c8527cb0f
23dbf74cd0534517d729fc1ebad29d15
b27c8f0373ca74ccc7a19de9e174ce39
wrapped_acc (64 octets):
0eaa0b273e84f0c36672903960b068d9
9fe74be45722aea9acd3a05aca910350
36c63abeb67e6d7b174c785491886242
9271e799a72409ceba53960dd63375cb
snapshot_tag (64 octets):
75dd576b0a3f5ba7181b3e183e6ef741
b1e7ea93a7852fc0c7657ed905f74eb6
45570ac0b639c2f901797f1152c8c7d4
196aed0cf4af2308431af242399b0c22
¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0013 (TurboSHAKE-256)
geometry:
segment_max 65536
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0013
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (64 octets):
b19991b71ed275d98070eab735179d60
be397354a85f6d6f58e74dcb90f0ff43
8271da594a267d81aaa74a88736ba549
d9e88c1b9d9a972135220b76c9568483
payload_key (32 octets):
bdecddf1340029c520b9b4a9e1b15144
d283209261a58113294728e337d14ea8
snap_key (64 octets):
92fc2e47ac72bbdbcac62a67ced07dad
a1e907bd82e92a68ba5f6098ec067931
e59683904d5213ddd0abe237ac0f9450
ef33180028f2ea7e47d738e6f3faed01
Segment 0 (is_final=0):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
429b98993decaa7c1792d52d
tag (16 octets):
c2afc8ba9d2d163d600a88ed34420cc8
contrib (64 octets):
97b151445a7f50b4f41811e8b9d0f4c9
04648ba6d13085e7d7180d94321aa3f8
4f895847053ce9c66e4b27f4251f6ffc
356e72ec1a867e2b405163732b65b8c1
Segment 1 (is_final=1):
nonce (12 octets):
070707070707070707070707
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
7597ac827f175c664e7bebb8b828753b
fc778463
tag (16 octets):
78545349b2e5092b9d279c97abfe1511
contrib (64 octets):
df8a6116f5535741f7973cfbb2aa6ef0
78f54da534fea79f953ef95b6a60c5d8
eaa6a034c9dc8186f27726ae99772694
0cb46e83581e1466f34a46a76abbca41
accumulator (64 octets):
483b3052af2c07f5038f2d130b7a9a39
7c91c603e5ce22784226f4cf587a6620
a52ff873cce068409c3c015abc684968
39da1c6f42986a4db31b25d441de7280
mask (64 octets):
49cc05dd25532375d143f69b276b0b8d
5246388217b39b7ef2262b6e7f293fdc
a43408f13b9172cb3e98a72c735d2546
e15b860122ebea099a7cbd987bfee991
wrapped_acc (64 octets):
01f7358f8a7f2480d2ccdb882c1191b4
2ed7fe81f27db906b000dfa1275359fc
011bf082f7711a8ba2a4a676cf356c2e
d8819a6e607380442967984c3a209b11
snapshot_tag (64 octets):
f6b15b8cf01fd11587f9fe7a32185abe
31ac84194ae3eed199a5b19ba83b0cdf
bf0c8e3b4f48199b01bdd1710d87c076
9d0cdc82f5292a913b7f1d14cd93b843
¶
This vector applies RewriteSegment (Section 4.9.2.1) to segment 0 of a two-segment AES-256-GCM message, replacing its plaintext under a fresh nonce. acc_delta = old_contrib XOR new_contrib, the new accumulator is the old accumulator XOR acc_delta, and the snapshot tag is recomputed over the count and the new accumulator.¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 1
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 01
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
9285553e10209c27bb5858b621426513
b0832f26d7ee813d9dd62c218ce6972a
payload_key (32 octets):
bb78da70d5e99d36c78e8a8b1a79b620
e4a4250dd6b471024c379917dfbb2de7
snap_key (32 octets):
953950ab75bdefd67ef15bbd7665b8af
d3c9ced50ce7cb369e789606fc455025
Segment 0 (is_final=0):
nonce (12 octets):
030303030303030303030303
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
1815f12b13f7ee2532f0fcca
tag (16 octets):
2597e2f2243b98c4bb7f320dc2f46ce3
contrib (32 octets):
0d8b3cb23192377e88232945f623150b
1a1c0b61745a4fa39a5f65e162b6e672
Segment 1 (is_final=1):
nonce (12 octets):
070707070707070707070707
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
f17c9cac3693dcb4bdd524714da804d4
c4390056
tag (16 octets):
97d4f0d1303a2b112eac5aae081ef6bf
contrib (32 octets):
a8e8df627852127e058558353da2def9
9a83928b4c96ef37a5d0b08068b1a87c
accumulator (32 octets):
a563e3d049c025008da67170cb81cbf2
809f99ea38cca0943f8fd5610a074e0e
mask (32 octets):
946a2744cf3fd572a03993d434705704
47d1ce03f4769035a5058780660e5eff
wrapped_acc (32 octets):
3109c49486fff0722d9fe2a4fff19cf6
c74e57e9ccba30a19a8a52e16c0910f1
snapshot_tag (32 octets):
5ce50c9e90db4bbc28297372e401625c
2e43203ce8008c452ea4355f0941ef67
Rewrite of segment 0:
new_nonce (12 octets):
0b0b0b0b0b0b0b0b0b0b0b0b
new_ciphertext (22 octets):
f20adfd4e5ab8016903d7eaa022f65c7
0a3f9988b1e5
new_tag (16 octets):
ff3268f44b36f41c84a2e85d0a975d38
old_contrib (32 octets):
0d8b3cb23192377e88232945f623150b
1a1c0b61745a4fa39a5f65e162b6e672
new_contrib (32 octets):
d1799e445a84608aae7dfca35bc03813
138fd92f58adb6c1fec48907e9a90f9e
acc_delta (32 octets):
dcf2a2f66b1657f4265ed5e6ade32d18
0993d24e2cf7f962649bece68b1fe9ec
new_accumulator (32 octets):
7991412622d672f4abf8a4966662e6ea
890c4ba4143b59f65b1439878118a7e2
new_mask (32 octets):
76615f532f527eaecac7d2fa4232fc32
10596f72a1e2637cfbb5db656d53c9ac
new_wrapped_acc (32 octets):
0ff01e750d840c5a613f766c24501ad8
995524d6b5d93a8aa0a1e2e2ec4b6e4e
new_snapshot_tag (32 octets):
bba55e58311ebbd38d7880a9ebec3d19
3212c0600ce04aeeb7c18ee62e33b9cc
¶
This vector exercises AES-256-GCM-SIV (Table 9), the MRAE cipher suite that uses derived nonce mode. There is no stored per-segment nonce: each nonce is recomputed from nonce_base by the formula in Section 4.5.3.2, which XORs uint64((i << 1) | is_final), the segment index and finality bit, into the low 8 octets of nonce_base (here Nn = 12). The block applies RewriteSegment to segment 0. Because the nonce is recomputed deterministically, the rewrite reuses the same nonce as the original segment 0. AES-256-GCM-SIV is misuse-resistant, so the reuse leaks only equality of identical plaintext-and-context pairs, not plaintext.¶
Parameter set:
cipher suite:
aead_id 0x001f (AEAD_AES_256_GCM_SIV)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 0
nonce_mode derived
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 001f
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 01
epoch_length_u8 ( 1 octets): 00
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
bf20f8c7691934f0ccf767b2a5ac19e4
67228674414f68d839a6698a3edd1813
payload_key (32 octets):
d2f4ae67c4024a3b61b902188a75cdc4
757245350393608e8d4af530b91a4411
snap_key (32 octets):
18e303acfa725f11d6a75a48723fb408
c481e5703032b0e5f1724461f8901bbc
nonce_base (12 octets):
8a2860a4c1e733427aaa7aeb
Segment 0 (is_final=0):
nonce (12 octets): (derived from nonce_base, not stored)
8a2860a4c1e733427aaa7aeb
epoch_key (32 octets):
a65f10ea805ada25b9f1ef7527383cde
423b104e8813edaed5490747633291c6
segment_key (32 octets):
a65f10ea805ada25b9f1ef7527383cde
423b104e8813edaed5490747633291c6
segment_aad ( 0 octets): (empty)
ciphertext (12 octets):
b3b34ccfb3481851057f1eab
tag (16 octets):
2d143f398a84cdfd47146c194e1c177c
contrib (32 octets):
fce4eed2d488b1ee8994c270cd145202
ea63b9fd0c0f7401c5cad41c767e91c6
Segment 1 (is_final=1):
nonce (12 octets): (derived from nonce_base, not stored)
8a2860a4c1e733427aaa7ae8
epoch_key (32 octets):
3cd69f36c405895994a14b8b5aafaf09
09517de95d17e6b903cf350fa8769826
segment_key (32 octets):
3cd69f36c405895994a14b8b5aafaf09
09517de95d17e6b903cf350fa8769826
segment_aad ( 0 octets): (empty)
ciphertext (20 octets):
3392acaffbaaa0224644ee4b0efa53ad
c0d21628
tag (16 octets):
408046d4fb0789a8ae0c41ddb0f66fc3
contrib (32 octets):
7e2e00b404bf5962b6220c4196298f4b
d25bff62bd9e3230ddd0d344e2b78cc1
accumulator (32 octets):
82caee66d037e88c3fb6ce315b3ddd49
3838469fb1914631181a075894c91d07
mask (32 octets):
b0c48fd39186cb0b55dd036ffd1c46ef
73c6b282977ecd28e6065cdcc64e36d2
wrapped_acc (32 octets):
320e61b541b123876a6bcd5ea6219ba6
4bfef41d26ef8b19fe1c5b8452872bd5
snapshot_tag (32 octets):
44dea9b3ed7c07f3e95bfe10848430f3
cc1059251c3cf85d2c2e717634155aaf
Rewrite of segment 0:
new_nonce (12 octets):
8a2860a4c1e733427aaa7aeb
new_ciphertext (22 octets):
f1cb96bd31369de3d8f26e007bf71759
51a0c59330b0
new_tag (16 octets):
74105b3b59dd624421f67f295921841d
old_contrib (32 octets):
fce4eed2d488b1ee8994c270cd145202
ea63b9fd0c0f7401c5cad41c767e91c6
new_contrib (32 octets):
64064c4b612f62cafad57b458158d125
08164dc2bef7594568a978f6f2a1ae55
acc_delta (32 octets):
98e2a299b5a7d3247341b9354c4c8327
e275f43fb2f82d44ad63acea84df3f93
new_accumulator (32 octets):
1a284cff65903ba84cf7770417715e6e
da4db2a003696b75b579abb210162294
new_mask (32 octets):
5f4185e7b2b940639b4fa672174edf71
a88c8015423ed35d14134b1741d869a2
new_wrapped_acc (32 octets):
4569c918d7297bcbd7b8d176003f811f
72c132b54157b828a16ae0a551ce4b36
new_snapshot_tag (32 octets):
a4ad2d7fa8abbbec998af5c3ced3514f
e420923c403ba57771bad99a8d03034a
¶
This vector sets epoch_length = 0, the finest epoch partition, so the epoch index equals the segment index and each segment is sealed under a distinct epoch key (Section 4.5.2). The block exposes the intermediate epoch_key(i) and segment_key(i) for each segment. At epoch_length = 0 the shift is the identity, so segment_key(i) equals epoch_key(i), and epoch_key(0) and epoch_key(1) differ.¶
Parameter set:
cipher suite:
aead_id 0x0002 (AEAD_AES_256_GCM)
kdf_id 0x0001 (HKDF-SHA-256)
geometry:
segment_max 65536
operational:
epoch_length 0
nonce_mode random
snap_id 0x0001 (masked multiset hash)
payload_info (the KDF frames each element):
aead_id ( 2 octets): 0002
segment_max_be ( 4 octets): 00010000
kdf_id ( 2 octets): 0001
snap_id ( 2 octets): 0001
nonce_mode ( 1 octets): 00
epoch_length_u8 ( 1 octets): 00
salt (32 octets):
04040404040404040404040404040404
04040404040404040404040404040404
Payload schedule:
commitment (32 octets):
248167fa761884de975ed84dd2464c7b
0e85cfaf205470750ca644137da76517
payload_key (32 octets):
e27e393efb0b8abec87b27fa0ae3f19c
0f19093877aae8267d14be74b035eeb6
snap_key (32 octets):
f152bcb8e03852f726a7824c902e9b4f
aa9b849478cd115c1a3de02b8f04ddb8
Segment 0 (is_final=0):
nonce (12 octets):
030303030303030303030303
epoch_key (32 octets):
cfe9ccdc21e8021fd5cada3fff397f2b
86431ec14eb0ac60809e4aee4a497f36
segment_key (32 octets):
cfe9ccdc21e8021fd5cada3fff397f2b
86431ec14eb0ac60809e4aee4a497f36
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000000000100
ciphertext (12 octets):
04b77b3d7370bf0ce5baeb78
tag (16 octets):
4fd2abdb95a887e32aacced927ea7666
contrib (32 octets):
22f6f5e04dc24fcd3ea57d2aab707c37
7530de5b5cde77959c85888afe189675
Segment 1 (is_final=1):
nonce (12 octets):
070707070707070707070707
epoch_key (32 octets):
c6b0ed6d72fe7fe3114623c98f527e3d
3644cfdc88c56f6e4550495dc92b3fe2
segment_key (32 octets):
c6b0ed6d72fe7fe3114623c98f527e3d
3644cfdc88c56f6e4550495dc92b3fe2
segment_aad (24 octets):
00095345414c2d444154410008000000
0000000001000101
ciphertext (20 octets):
550d0675f7ed31bf5377805fcf64bb30
38167fe1
tag (16 octets):
15c5bbacef49c9e0cc6f4a16a1fb0204
contrib (32 octets):
e9341dfae1376b08122e20e8580b1e94
7b771e1cf4b11cf5472868cc34a18870
accumulator (32 octets):
cbc2e81aacf524c52c8b5dc2f37b62a3
0e47c047a86f6b60dbade046cab91e05
mask (32 octets):
4b770b765273b67eda59116abc4521c7
28998adf914b868f6c94bcdda7f57910
wrapped_acc (32 octets):
80b5e36cfe8692bbf6d24ca84f3e4364
26de4a983924edefb7395c9b6d4c6715
snapshot_tag (32 octets):
7ec104576a2197294a35d2d28000331e
30dc4172f1f18492950e15ef0c1f1742
¶
These vectors exercise the optional plaintext-bound nonce construction (Appendix A) component by component: the plaintext digest pt_digest, the bound digest pt_hash, the framed nonce_ctx, and the final nonce. The construction is encryptor-only and its output is indistinguishable from random mode on the wire, so no end-to-end vector exercises it; component values are the only way to check an implementation byte for byte. The labels are those of Table 24, and nonce_ctx enters the final derivation as a single framed info element after the payload_info elements.¶
Each block reuses the payload_key, payload_info, and salt of a published cipher-suite block, so the values chain into the payload schedules above. The random_input value stands in for the fresh Random(Nn) draw of the construction, and both blocks bind the plaintext "Hello, SEAL!". These component vectors are printed in this appendix only and are not part of the end-to-end corpus.¶
The payload schedule is that of Appendix E.1.¶
plaintext (12 octets):
48656c6c6f2c205345414c21
random_input (12 octets):
0f0f0f0f0f0f0f0f0f0f0f0f
pt_digest (32 octets):
e66fec4cada0ccdb73930622ef393d5b
a05fb73bdd81205a9f828f75e85ded81
pt_hash (32 octets):
072fa800d5069a226a7322c5b3fb704f
8564fd2075dd9de6de274a2b5645faaa
nonce_ctx (56 octets):
000a5345414c2d52572d763100080000
0000000000000020072fa800d5069a22
6a7322c5b3fb704f8564fd2075dd9de6
de274a2b5645faaa
nonce (12 octets):
ef044c5a935e8bd52db61582
¶
The payload schedule is that of the single-segment TurboSHAKE-256 block (Appendix E.15). The segment index is 1, visible in the second framed element of nonce_ctx.¶
plaintext (12 octets):
48656c6c6f2c205345414c21
random_input (12 octets):
0f0f0f0f0f0f0f0f0f0f0f0f
pt_digest (64 octets):
f809e1b9b0e28d0fd1dce5cf9e4aae59
fde2b08a551c311b621323a5d2f3c78d
e55c22edcdd091231c4509849acf592a
85ab446dfcff4fac008194e6ef59d9b4
pt_hash (64 octets):
ad6a35f94960ee1004391bdfaee16149
ef56bca4fd5b98abfec56fdb80752624
ade63d71f3b15e9aa5dd9fb0ffa8e533
f7ef5caa7620432cf1c1bf7d97611536
nonce_ctx (88 octets):
000a5345414c2d52572d763100080000
0000000000010040ad6a35f94960ee10
04391bdfaee16149ef56bca4fd5b98ab
fec56fdb80752624ade63d71f3b15e9a
a5dd9fb0ffa8e533f7ef5caa7620432c
f1c1bf7d97611536
nonce (12 octets):
dbd450bb6f147795caeeee12
¶
This vector demonstrates the snapshot integrity check. It takes the honest state of the two-segment AES-256-GCM, HKDF-SHA-256, 65536 vector and flips the first octet of the stored masked accumulator, leaving the snapshot tag unchanged. SnapVerify recomputes the snapshot from the two present segment tags (Section 4.7.4): the recomputed accumulator is the honest one, so the recomputed snapshot tag equals the stored tag, but the recomputed wrapped accumulator differs from the tampered one. The single constant-time comparison of the full recomputed snapshot against the stored value therefore fails, and SnapVerify returns reject without revealing which half differed. A consumer MUST treat this entry as expect-reject and MUST NOT accept it as a valid snapshot.¶
Source: the two-segment AES-256-GCM, HKDF-SHA-256, 65536 vector;
the tamper flips the first octet of the stored masked
accumulator, with the snapshot_tag left unchanged.
tampered_accumulator (32 octets):
a463e3d049c025008da67170cb81cbf2
809f99ea38cca0943f8fd5610a074e0e
mask (32 octets):
946a2744cf3fd572a03993d434705704
47d1ce03f4769035a5058780660e5eff
tampered_wrapped_acc (32 octets):
3009c49486fff0722d9fe2a4fff19cf6
c74e57e9ccba30a19a8a52e16c0910f1
snapshot_tag (32 octets):
5ce50c9e90db4bbc28297372e401625c
2e43203ce8008c452ea4355f0941ef67
SnapVerify recomputes the snapshot from the present segment tags
and compares it, whole, against the stored value in constant
time. The recomputed wrapped accumulator differs from the stored
tampered one, so the comparison fails.
SnapVerify result: reject
¶
This appendix is informative. It summarizes the substantive changes from the -00 revision.¶
SEAL: the -00 monolithic raAE-v1 profile became SEAL, a parameterized construction with two named profiles, SEAL-RW-v1 (mutable) and SEAL-RO-v1 (write-once). payload_info gained snap_id (the snapshot authenticator identifier) and dropped aad_label, segment_max is a power of two of at least 4096 octets, and the five named instantiations fix complete parameter sets and a serialization layout (Section 4.12).¶
Structural split: the raAE primitive (Section 3), the SEAL construction that realizes it (Section 4), and the cipher suites (Section 4.10) are now specified separately.¶
Snapshot authenticator (Section 4.7): -00 published the bare XOR accumulator as its whole-object check. That design is superseded for a security reason: a write adversary can recombine observed accumulator differences into a non-historical segment set that still verifies (Appendix C.5), so the -00 snapshot offers no whole-object integrity against rewrites. The replacement is the masked multiset hash: a snapshot tag MACs the count and accumulator, and a deterministic, tag-derived mask hides the accumulator, so the published value is wrapped_acc || snapshot_tag.¶
Wire format, code points: aead_id and kdf_id are now 2-octet IANA code points.¶
Wire format, salt: the per-content salt is now a fixed 32 octets.¶
Wire format, KDF output size: TurboSHAKE-256 uses Nh = 64.¶
KDF framing: the KDF framing function is total, and the one-step form frames ikm and info each as one element.¶
Security analysis: reorganized to be property-oriented. The body states properties, assumptions, requirements, and operational limits. The reductions, bounds, and proof symbols are in Appendix C, and the formal proofs are deferred to a companion paper in preparation.¶
Integrity requirements: unauthorized truncation detection is a normative decryptor requirement, n_seg = 0 is disallowed without a snapshot authenticator, snapshot verification is required when all segment tags are available, commitment_length is at least 16 octets, and the snapshot comparison is constant-time.¶
SEAL-compact named instantiation: added SEAL-compact (SEAL-RW-v1, derived nonce, aligned layout), naming an already-buildable combination that gives large random-access objects compact per-segment metadata. Byte-neutral.¶
The wire-format and snapshot changes above are reflected in a regenerated test-vector corpus (Appendix E).¶
Normative tightenings from implementation evidence: epoch-key derivation applies at every epoch_length and payload_key is never a segment key (Section 4.5.2), a SEAL-RO-v1 encryptor MUST NOT rewrite a written segment (Section 4.10.2), streamed plaintext is unverified for completeness until the terminal finality check (Section 4.9.1.2), and the SnapVerify comparison covers the full snapshot value in one constant-time comparison (Section 6.9).¶
Presentation, from the same evidence: the plaintext-bound construction's labels are tabulated adjacent to the construction (Appendix A), the profiles state where truncation detection surfaces, and the layout size formulas carry Nt symbolically.¶
Test vectors, from the same evidence: component vectors for the plaintext-bound nonce construction (Appendix E.19), a worked byte-layout example for the one-step KDF (Section 4.3.2), and regenerated TurboSHAKE-256 cipher-suite vectors matching the nested one-step framing this document specifies.¶
Terminology: the interface extension is named the extended raAE interface throughout, replacing the earlier "mutable raAE" phrasing, so no text suggests two primitives named raAE. Mutable and immutable now describe only the SEAL profiles' write discipline.¶
Attribution: the introduction credits the base interface and security notions to Fábrega et al., related work names FLOE as the base-interface construction of that work, and the extension section states which interface tier each construction realizes.¶
Global associated data: G is a per-message input, the StartEnc G of the primitive, always the last element of the commitment derivation and empty by default. It is never stored. Every commitment value changes, and the test-vector corpus is regenerated. Appendix E.2 pins the default and a nonempty G.¶
The author thanks Andrés Fábrega, Thomas Ristenpart, Gregory Rubin, Richard Barnes, Thibault Meunier, Kenny Paterson, Christopher Patton, and Christopher A. Wood for their reviews, comments, and discussions.¶