embassy-rs/embassy · error

Cannot update AAD after starting payload!

Error message

Cannot update AAD after starting payload!

What it means

The CRYP AES driver enforces GCM/CCM phase ordering: once the AAD (header) phase is complete (aad_complete set, payload phase started), additional_header() can no longer be called. AAD must be fully supplied before any payload data; interleaving AAD after payload is not supported by this hardware flow.

Solutions

  1. Collect all AAD first and call additional_header() for all of it (last block flagged), then start the payload phase.
  2. Buffer payload bytes until AAD is complete if AAD arrives late.
  3. Use a fresh cipher context — phase state cannot be rewound.

Example fix

// before
ctx.additional_header(aad1, false);
ctx.payload(chunk, &mut out, false);
ctx.additional_header(aad2, true); // panics
// after
ctx.additional_header(aad1, false);
ctx.additional_header(aad2, true);
ctx.payload(chunk, &mut out, false);
Defensive patterns

Strategy: type-guard

Validate before calling

if !ctx.aad_complete { ctx.additional_header(more_aad, false); } else { /* buffer or error */ }

Type guard

fn can_add_aad(ctx: &CipherContext) -> bool { !ctx.aad_complete }

Prevention

When it happens

Trigger: Calling additional_header() on a context after the payload phase has begun (ctx.aad_complete == true), e.g. appending more AAD after encrypting a chunk.

Common situations: Protocols that interleave header and body; AEAD callers using frameworks that allow AAD/payload interleaving mapped onto this sequential driver; reusing a context across calls without tracking phase.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of embassy-rs/embassy@463a07b963 (2026-09-10). Data as JSON: /api/errors/b0436fb1dc398581. Report an issue: GitHub.

Appendix: source

Thrown at embassy-stm32/src/cryp/mod.rs:1306

    /// This function is only valid for authenticated ciphers including GCM, CCM, and GMAC.
    /// All additional associated data (AAD) must be supplied to this function prior to starting the payload phase with `payload_blocking`.
    /// The AAD must be supplied in multiples of the block size (128-bits for AES, 64-bits for DES), except when supplying the last block.
    /// When supplying the last block of AAD, `last_aad_block` must be `true`.
    pub fn aad_blocking<
        'c,
        const TAG_SIZE: usize,
        C: Cipher<'c> + CipherSized + IVSized + CipherAuthenticated<TAG_SIZE>,
    >(
        &self,
        ctx: &mut Context<'c, C>,
        aad: &[u8],
        last_aad_block: bool,
    ) {
        self.load_context(ctx);

        // Perform checks for correctness.
        if ctx.aad_complete {
            panic!("Cannot update AAD after starting payload!")
        }

        ctx.header_len += aad.len() as u64;

        // Header phase
        T::regs().cr().modify(|w| w.set_crypen(false));
        T::regs().cr().modify(|w| w.set_gcm_ccmph(1));
        T::regs().cr().modify(|w| w.set_crypen(true));

        // First write the header B1 block if not yet written.
        if !ctx.header_processed {
            ctx.header_processed = true;
            let header = ctx.cipher.get_header_block();
            ctx.aad_buffer[0..header.len()].copy_from_slice(header);
            ctx.aad_buffer_len += header.len();
        }

        // Fill the header block to make a full block.

View on GitHub (pinned to 463a07b963)