Repository navigation
Security
- Group signature verification is now mandatory. Both
ValidateGroupMessageand
ProcessDistributionMessagepreviously skipped verification when a message carried no
signature, treating it as valid. BecauseSenderIdentityKeyis public and the transport
is untrusted, an attacker able to write to the transport could send an unsigned
sender-key distribution bearing a member's identity key and an attacker-chosen chain
key, then impersonate that member. No sending path in the library ever produced an
unsigned message, so this rejects only forgeries. (One receive path,
LibEmiddleClient.Helpers.cs'sRouteGroupMessageAsync, does construct an
EncryptedGroupMessagewithout aSignature— but it also omits
RotationEpoch, taking the type's default of0against an always-non-zero real value,
which already broke the AES associated data and made it unable to decrypt anything before
this release. Nothing in the library sends that message type and no test exercises it; it
is a pre-existing, non-functional route, not a consequence of this change.) - Null ciphertext no longer crashes the process (denial of service). A malformed group
message with a nullCiphertext, a valid member'sSenderIdentityKey, and a well-formed
nonce reached the AES decryption path uncaught:Ciphertext?.Length == 0is false when
Ciphertextis null, so the guard intended to reject empty ciphertext accepted null, and
the resultingArgumentNullExceptionpropagated out ofDecryptMessageAsyncbecause the
wrappingtryhad only afinally, nocatch. This was remotely triggerable by any
party able to submit a group message. Null ciphertext is now rejected explicitly before
it reaches the signing/decryption path. - Wrong-length signature no longer crashes the process (denial of service). A malformed
group message could carry aSignaturethat was non-null and non-empty but not exactly
64 bytes.ValidateGroupMessageandProcessDistributionMessagerejected only a null or
zero-length signature, so a wrong-length one reachedSodium.SignVerifyDetached, which
throwsArgumentExceptionrather than returningfalsefor any length other than
Constants.ED25519_SIGNATURE_SIZE. That exception propagated out of
DecryptMessageAsync/ProcessDistributionMessageuncaught, crashing the receiving
session. Both guards now reject a wrong-length signature the same way they reject an
absent one. - Replay-set eviction is now insertion-ordered. Eviction used
ConcurrentDictionary.Keys.Take(), which guarantees no ordering, so a recently seen
message ID could be evicted while older ones survived.
Breaking
-
Group signature format changed. Fields in the signed byte string are now
length-prefixed with a 4-byte big-endian length,SenderIdentityKeyis always
included, and a one-byte domain-separation tag (0x01for messages,0x02for
sender-key distributions) is prepended to the signed payload. Previously fields were
concatenated without prefixes, so distinct field decompositions could produce identical
signing input and a signature did not uniquely commit to the message it covered; and
without the domain tag, a byte string valid for one payload type could in principle
parse as the other, since both signing contexts share a single Ed25519 identity key. Not
exploitable in the shipped code today, but closed now because the wire format is about
to be frozen by golden vectors.2.8.0 peers cannot verify group messages or sender-key distributions from 2.7.x
peers, or vice versa. All members of a group must upgrade together. One-to-one chat
sessions are unaffected.This failure is silent. Group messages carry no protocol-version field, and
ProtocolVersion.IsCompatibletreats every 2.x peer as compatible, so a mixed-version
group does not detect the mismatch. Cross-version messages are rejected one at a time and
DecryptMessageAsyncreturnsnull— no exception reaches the caller. The rejections are
recorded viaLoggingManager.LogSecurityEvent(message: "signature verification failed"),
so enable security-event logging before upgrading a group incrementally, or the
symptom is simply that some members stop receiving messages with no error surfaced.Note on version numbering.
VERSIONING.mdreserves MAJOR for "breaking changes to
the public API or protocol", so by that rule this change belongs in 3.0.0. It ships as a
minor deliberately: the public API is unchanged, and the backward-compatible security
fixes above were judged too important to hold behind the v3.0 milestone. Do not infer
from the version number that 2.7.x and 2.8.0 group members interoperate — they do not.