xmtp_mls_common: add invite::encrypted_group_info + XMTP_EXTERNAL_INVITE_LABEL#3671
xmtp_mls_common: add invite::encrypted_group_info + XMTP_EXTERNAL_INVITE_LABEL#3671tylerhawkes wants to merge 1 commit into
Conversation
|
Claude finished @tylerhawkes's task —— View job Code Review — June 6, 2026 05:09 UTCI've reviewed the encryption helpers for the QR-invite flow. The implementation is solid overall, with one outstanding security concern that should be addressed: Security: Expiry Check Ordering (High Priority)The existing review comment about expiry-check ordering at Issue: Impact: Denial-of-service vector — attackers can make valid, non-expired invites appear expired by tampering with Fix: Move the expiry check after line 195 (after successful AEAD unwrap), when Example: // After line 194, before Ok((plaintext, v1)):
if let Some(now) = now_ns
&& v1.expires_at_ns != 0
&& now >= v1.expires_at_ns
{
return Err(EncryptedGroupInfoError::Expired {
expires_at_ns: v1.expires_at_ns,
now_ns: now,
});
}Test gap: The existing Other ObservationsCode Quality: ✅ Excellent
Test Coverage: ✅ Strong (98.76%)
Performance: ✅ No concerns
Best Practices: ✅ Followed
SummaryThis is high-quality cryptographic code. The only blocker is the expiry-check ordering issue, which is a real security concern (DoS vector) but straightforward to fix. Once that's addressed, this is ready to merge. Tasks
|
ApprovabilityVerdict: Needs human review Introduces new cryptographic functionality for encrypted group info with AEAD encryption. Unresolved review comments identify a potential security issue where expiry is checked before AEAD authentication, which warrants human attention. You can customize Macroscope's approvability policy. Learn more. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3671 +/- ##
==========================================
+ Coverage 84.23% 84.36% +0.12%
==========================================
Files 407 409 +2
Lines 59350 59519 +169
==========================================
+ Hits 49994 50213 +219
+ Misses 9356 9306 -50 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
2c61537 to
d8d71d5
Compare
ce61b41 to
32a32fb
Compare
Dismissing prior approval to re-evaluate d8d71d5
d8d71d5 to
e369a4e
Compare
Dismissing prior approval to re-evaluate e369a4e
be11fef to
e8a88aa
Compare
Dismissing prior approval to re-evaluate e369a4e
7c45f8a to
03ad9fe
Compare
| /// * [`EncryptedGroupInfoError::Expired`] when `now_ns` is supplied and the | ||
| /// blob's `expires_at_ns` is non-zero and `<= now_ns`. | ||
| /// * [`EncryptedGroupInfoError::Unwrap`] for any AEAD-level failure. | ||
| pub fn unwrap_group_info<'a>( |
There was a problem hiding this comment.
🟢 Low invite/encrypted_group_info.rs:155
unwrap_group_info checks v1.expires_at_ns against now_ns at line 167–174 before the AEAD unwrap at line 178–184. Because the envelope fields are attacker-controlled until verified, an attacker can modify expires_at_ns to any nonzero past timestamp and cause the function to return EncryptedGroupInfoError::Expired instead of EncryptedGroupInfoError::Unwrap. This allows unauthenticated expiry invalidation of valid invites. The expiry check should occur after successful AEAD authentication so the expires_at_ns value is integrity-protected by the AAD.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/xmtp_mls_common/src/invite/encrypted_group_info.rs around line 155:
`unwrap_group_info` checks `v1.expires_at_ns` against `now_ns` at line 167–174 before the AEAD unwrap at line 178–184. Because the envelope fields are attacker-controlled until verified, an attacker can modify `expires_at_ns` to any nonzero past timestamp and cause the function to return `EncryptedGroupInfoError::Expired` instead of `EncryptedGroupInfoError::Unwrap`. This allows unauthenticated expiry invalidation of valid invites. The expiry check should occur after successful AEAD authentication so the `expires_at_ns` value is integrity-protected by the AAD.
| /// * [`EncryptedGroupInfoError::Expired`] when `now_ns` is supplied and the | ||
| /// blob's `expires_at_ns` is non-zero and `<= now_ns`. | ||
| /// * [`EncryptedGroupInfoError::Unwrap`] for any AEAD-level failure. | ||
| pub fn unwrap_group_info<'a>( |
There was a problem hiding this comment.
🟢 Low invite/encrypted_group_info.rs:155
unwrap_group_info checks v1.expires_at_ns against now_ns at line 167–174 before the AEAD unwrap at line 178–184. Because the envelope fields are attacker-controlled until verified, an attacker can modify expires_at_ns to any nonzero past timestamp and cause the function to return EncryptedGroupInfoError::Expired instead of EncryptedGroupInfoError::Unwrap. This allows unauthenticated expiry invalidation of valid invites. The expiry check should occur after successful AEAD authentication so the expires_at_ns value is integrity-protected by the AAD.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/xmtp_mls_common/src/invite/encrypted_group_info.rs around line 155:
`unwrap_group_info` checks `v1.expires_at_ns` against `now_ns` at line 167–174 before the AEAD unwrap at line 178–184. Because the envelope fields are attacker-controlled until verified, an attacker can modify `expires_at_ns` to any nonzero past timestamp and cause the function to return `EncryptedGroupInfoError::Expired` instead of `EncryptedGroupInfoError::Unwrap`. This allows unauthenticated expiry invalidation of valid invites. The expiry check should occur after successful AEAD authentication so the `expires_at_ns` value is integrity-protected by the AAD.
| /// * [`EncryptedGroupInfoError::Expired`] when `now_ns` is supplied and the | ||
| /// blob's `expires_at_ns` is non-zero and `<= now_ns`. | ||
| /// * [`EncryptedGroupInfoError::Unwrap`] for any AEAD-level failure. | ||
| pub fn unwrap_group_info<'a>( |
There was a problem hiding this comment.
🟢 Low invite/encrypted_group_info.rs:155
unwrap_group_info checks v1.expires_at_ns against now_ns at line 167–174 before the AEAD unwrap at line 178–184. Because the envelope fields are attacker-controlled until verified, an attacker can modify expires_at_ns to any nonzero past timestamp and cause the function to return EncryptedGroupInfoError::Expired instead of EncryptedGroupInfoError::Unwrap. This allows unauthenticated expiry invalidation of valid invites. The expiry check should occur after successful AEAD authentication so the expires_at_ns value is integrity-protected by the AAD.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/xmtp_mls_common/src/invite/encrypted_group_info.rs around line 155:
`unwrap_group_info` checks `v1.expires_at_ns` against `now_ns` at line 167–174 before the AEAD unwrap at line 178–184. Because the envelope fields are attacker-controlled until verified, an attacker can modify `expires_at_ns` to any nonzero past timestamp and cause the function to return `EncryptedGroupInfoError::Expired` instead of `EncryptedGroupInfoError::Unwrap`. This allows unauthenticated expiry invalidation of valid invites. The expiry check should occur after successful AEAD authentication so the `expires_at_ns` value is integrity-protected by the AAD.
03ad9fe to
54721d0
Compare
Summary
Adds the encryption envelope for the QR-invite flow.
xmtp_mls_common::invite::encrypted_group_info— wraps/unwraps a TLS-serialized GroupInfo into anEncryptedGroupInfoBlobproto using ChaCha20Poly1305 (via thepayload_encryptionhelpers from PR-L2).wrap_group_infogenerates a fresh nonce per call;wrap_group_info_with_noncefor tests / explicit nonce management.unwrap_group_infoverifies envelope version + nonce length before decryption.XMTP_EXTERNAL_INVITE_LABELconstant toxmtp_configuration::common::mls(passed towrap_payload_hpkeby the create-invite path, not used here directly).The blob's
epochandgroup_state_hashfields are populated by the L-10/L-11 callers where the MlsGroup is available — the pure-codec helpers in this PR leave them zero.Stack
mls_ext::payload_encryption. The merge commit in this PR brings L-2 in; once L-2 lands the diff cleans up.Test plan
Note
Add
invite::encrypted_group_infomodule withwrap_group_info/unwrap_group_infoandXMTP_EXTERNAL_INVITE_LABELEncryptedGroupInfoBlob::V1, with AAD binding epoch, expiry, and group state hash to authenticate envelope metadata.XMTP_EXTERNAL_INVITE_LABELin mls.rs as a distinct HPKE domain-separation label reserved for external-invite flows.wrap_payload_symmetricandunwrap_payload_symmetricin payload_encryption.rs from positional-argument functions to#[bon::builder]-style, adding an optionalaadparameter that defaults to empty bytes.wrap_payload_symmetric/unwrap_payload_symmetricmust be updated to use builder syntax; AAD defaults to empty preserving prior decryption behavior.Macroscope summarized 54721d0.