Skip to content

feat: add header integrity check and replay protection to control messages#1740

Open
markpmarton wants to merge 19 commits into
agntcy:mainfrom
markpmarton:control-message-header-integrity
Open

feat: add header integrity check and replay protection to control messages#1740
markpmarton wants to merge 19 commits into
agntcy:mainfrom
markpmarton:control-message-header-integrity

Conversation

@markpmarton

@markpmarton markpmarton commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

closes #1721

Description

In the current implementation only publish message headers are e2e validated because it is built around the MLS payload encryption. To prevent malicious control message replay attacks, we need to add manual integrity check for pre- and post-session messages.

Changes

  • per sender monotonic increasing sequence_number added for replay protection
  • AAD signature e2e_header_sig added for header fields (signed by MLS key)
  • control message header fields signing and verification (with sequence number tracking)

Type of Change

  • Bugfix
  • New Feature
  • Breaking Change
  • Refactor
  • Documentation
  • Other (please describe)

Checklist

  • I have read the contributing guidelines
  • Existing issues have been referenced (where applicable)
  • I have verified this change is not present in other open pull requests
  • Functionality is documented
  • All code style checks pass
  • New code contribution is covered by automated tests
  • All new and existing tests pass

markpmarton and others added 5 commits June 11, 2026 16:07
…l messages

Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
…l messages

Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
…l messages

Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
Signed-off-by: Mark Marton <30534230+markpmarton@users.noreply.github.com>
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
Comment thread data-plane/core/session/src/session_layer.rs Fixed
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

markpmarton and others added 3 commits June 15, 2026 12:59
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
@markpmarton markpmarton marked this pull request as ready for review June 15, 2026 13:31
@markpmarton markpmarton requested a review from a team as a code owner June 15, 2026 13:31
markpmarton and others added 2 commits June 16, 2026 07:36
Signed-off-by: Mark Marton <30534230+markpmarton@users.noreply.github.com>
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
@markpmarton markpmarton force-pushed the control-message-header-integrity branch from 62f81dc to 9c2ec9e Compare June 16, 2026 13:04
markpmarton and others added 2 commits June 16, 2026 15:08
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
@markpmarton markpmarton marked this pull request as draft June 16, 2026 14:23
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
Signed-off-by: Mark Marton <mark.p.marton@gmail.com>
session_ctx,
))) => return session_ctx,
Some(Ok(_)) => continue,
Some(Err(e)) => {
@markpmarton markpmarton marked this pull request as ready for review June 17, 2026 08:01
);
// Require E2E verification only when the sender included a signature (matches pre-session path).
let e2e_required =
is_post_session_control && msg.get_slim_header().e2e_header_sig.is_some();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be always mandatory otherwise we can always send fake packets with e2e_header_sig set to None

optional uint64 incoming_conn = 9;
optional bool error = 10;
optional bytes header_mac = 11;
optional uint64 sequence_number = 12;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need a new sequence_number here? isn't enough to use the message_id in session header?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK message_id is getting a random u32 for control messages, that is not viable for replay protection. Maybe it can be changed to a monotonic counter, I'm going to check it.

let seen = seen_control_seqs.entry(sender.clone()).or_default();
if !seen.insert(s) {
// Duplicate delivery (network retransmission or fanout). Handlers are idempotent.
return Ok(());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure here, but should we drop the message here instead of return ok? If it is in seen it means that the message was already processed. Notice that if we use the message_id in the session header RTX message will have a different id with respect to the original message so the two messages will be handled as two different messages.

}
Err(e) => {
debug!(
tracing::error!(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should do back to debug

custom_claims: Option<CustomClaims>,
}

let claims_res = verifier.get_claims(&identity).await;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I think we can reuse the IdentityClaims in Auth. This part can be replaced with something like

 let claims_json = verifier.get_claims(&identity).await?;
 let identity_claims = slim_auth::identity_claims::IdentityClaims::from_json(&claims_json)?;
 let pubkey = identity_claims.public_key;


let aad = crate::mls_state::build_aad(m);
let private_key = identity_provider.get_signature_secret_key()?;
let public_key = identity_provider.get_signature_public_key()?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved out of the loop

let private_key = identity_provider.get_signature_secret_key()?;
let public_key = identity_provider.get_signature_public_key()?;


if let Err(e) =
crate::session_controller::verify_identity(&message, &layer.identity_verifier).await
let e2e_required = message.get_slim_header().e2e_header_sig.is_some();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here this also should always be mandatory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add header integrity validation to control messages

3 participants