fix(relay): canonicalise envelope ID and timestamp on mailbox.Store
The mailbox previously trusted the client-supplied envelope ID and SentAt,
which enabled two attacks:
- replay via re-broadcast: a malicious relay could resubmit the same
ciphertext under multiple IDs, causing the recipient to receive the
same plaintext repeatedly;
- timestamp spoofing: senders could back-date or future-date messages
to bypass the 7-day TTL or fake chronology.
Store() now recomputes env.ID as hex(sha256(nonce||ct)[:16]) and
overwrites env.SentAt with time.Now().Unix(). Both values are mutated
on the envelope pointer so downstream gossipsub publishes agree on the
normalised form.
Also documents /relay/send as non-E2E — the endpoint seals with the
relay's own key, which breaks end-to-end authenticity. Clients wanting
real E2E should POST /relay/broadcast with a pre-sealed envelope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,9 +167,16 @@ func relayInboxCount(rc RelayConfig) http.HandlerFunc {
|
|||||||
// "msg_b64": "<base64-encoded plaintext>",
|
// "msg_b64": "<base64-encoded plaintext>",
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// The relay node seals the message using its own X25519 keypair and broadcasts
|
// WARNING — NOT END-TO-END ENCRYPTED.
|
||||||
// it on the relay gossipsub topic. No on-chain fee is attached — delivery is
|
// The relay node seals the message using its OWN X25519 keypair, not the
|
||||||
// free for light clients using this endpoint.
|
// sender's. That means:
|
||||||
|
// - The relay can read the plaintext (msg_b64 arrives in the clear).
|
||||||
|
// - The recipient cannot authenticate the sender — they only see "a
|
||||||
|
// message from the relay".
|
||||||
|
// For real E2E messaging, clients should seal the envelope themselves and
|
||||||
|
// use POST /relay/broadcast instead. This endpoint is retained only for
|
||||||
|
// backwards compatibility with legacy integrations and for bootstrap
|
||||||
|
// scenarios where the sender doesn't have a long-lived X25519 key yet.
|
||||||
func relaySend(rc RelayConfig) http.HandlerFunc {
|
func relaySend(rc RelayConfig) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
|
|||||||
@@ -100,13 +100,31 @@ func (m *Mailbox) Close() error { return m.db.Close() }
|
|||||||
//
|
//
|
||||||
// Anti-spam checks (in order):
|
// Anti-spam checks (in order):
|
||||||
// 1. Ciphertext > MailboxMaxEnvelopeSize → returns ErrEnvelopeTooLarge.
|
// 1. Ciphertext > MailboxMaxEnvelopeSize → returns ErrEnvelopeTooLarge.
|
||||||
// 2. Duplicate envelope ID → silently overwritten (idempotent).
|
// 2. env.ID is recomputed to the canonical value hex(sha256(nonce||ct)[:16])
|
||||||
// 3. Recipient already has MailboxPerRecipientCap entries → oldest evicted first.
|
// — prevents a malicious relay from storing the same ciphertext under
|
||||||
|
// multiple IDs (real content-level replay protection).
|
||||||
|
// 3. env.SentAt is overwritten with server time — senders can't back-date
|
||||||
|
// or future-date messages to bypass ordering or TTL expiry.
|
||||||
|
// 4. Duplicate envelope ID → silently no-op (idempotent).
|
||||||
|
// 5. Recipient already has MailboxPerRecipientCap entries → oldest evicted first.
|
||||||
|
//
|
||||||
|
// NOTE: Store MUTATES env.ID and env.SentAt to the canonical / server values.
|
||||||
|
// Callers that re-broadcast (gossipsub publish) after Store will see the
|
||||||
|
// normalised envelope, which is desirable — peer nodes then agree on the
|
||||||
|
// same ID and timestamp.
|
||||||
func (m *Mailbox) Store(env *Envelope) error {
|
func (m *Mailbox) Store(env *Envelope) error {
|
||||||
if len(env.Ciphertext) > MailboxMaxEnvelopeSize {
|
if len(env.Ciphertext) > MailboxMaxEnvelopeSize {
|
||||||
return ErrEnvelopeTooLarge
|
return ErrEnvelopeTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.0.1 — canonicalise id & timestamp. Any client-supplied values are
|
||||||
|
// replaced with server-computed truth. This is the simplest way to
|
||||||
|
// prevent:
|
||||||
|
// - replay-via-rebroadcast (same ciphertext under different IDs),
|
||||||
|
// - timestamp spoofing (bypass TTL / fake chronology).
|
||||||
|
env.ID = envelopeID(env.Nonce, env.Ciphertext)
|
||||||
|
env.SentAt = time.Now().Unix()
|
||||||
|
|
||||||
key := mailboxKey(env.RecipientPub, env.SentAt, env.ID)
|
key := mailboxKey(env.RecipientPub, env.SentAt, env.ID)
|
||||||
val, err := json.Marshal(env)
|
val, err := json.Marshal(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user