sc: support sealed sender messages (to avoid spam blocking)
context
- Signal is experiencing a massive uptick in spammers abusing the open protocol to send strangers ads and harassing messages. (boo! we hate that!)
- To combat this problem, Signal has shipped a spate of updates to its server code that implement stringent rate limits on messages sent in unsealed sender mode (see: https://github.com/signalapp/Signal-Server/commits/master)
- Why is this a good way to combat abuse?
- short version: the token/cert handshake that accompanies the exchange of sealed sender messages provides a form of "proof of consent" that the recipient of sealed sender messages wanted to receive them (either because they had the sender in their contact store, or because they had already sent the sender a message previously)
- long version, which gets at "what is sealed sender anyway?" https://signal.org/blog/sealed-sender/
- since singalboost (and under the hood, signald or signalc) do not currently support sealed sender messages, all of our messages are counted toward the rate limit, which currently is getting more stringent, and appears to have recently resulted in the blocking of our IP (which will likely get blocked again quickly if we move to another server)
- this MR provides a fix to allow signalc to support sealed sender messages and thus prove to the signal server that our bots are not sending spam, and thus avoid rate limiting and blocking. (since it requires being implemented in signalc -- our custom signal client -- it will not take effect until signalc ships)
implementation sketch
- handle incoming profile keys on every message (in
SignalReceiver#dispatch
) and upsert them into thePreKeyStore
(do this on every message b/c we want to notice new profile keys when they are rotated -- which happens after a user blocks someone) - when we want to send a message, derive a
Pair<UnidentifiedAccessKey>
from stored profile keys (one member of the pair derrives from the channel's key, the other from- here, an
UnidentifiedAccessKey
is a "delivery token" + "sender certificate" tuple that allows the sealed sender authentication to work - the "pair" denotes that we will derrive one such tuple for ourselves and one for users to whom we want to send messages
- we derrive members of the pair by (respecively) calling
UnidentifiedAccess.deriveAccessKeyFrom(profileKey)
on:- (1) the channel's profile key (stored in the
signalc.store.AccountStore
) - (2) the profile key we have stored for the subscriber in a (newly created)
signalc.store.ContactStore
)
- (1) the channel's profile key (stored in the
- we then include this pair as the 2nd argument to
SignalServiceMessageSender#sendMessage
(which is currently always null)
- here, an
- add our profile key to all outgoing messages to enable people to send us sealed sender messages (via
SignalDataMessage.Builder.withProfileKey
)
investigation notes
inventory
- in code: UnidentifiedAccess(key, cert)
- referred to in blog as: delivery token, sender certificate
spam prevention design
- blog: "To prevent abuse, clients derive a 96-bit delivery token from their profile key and register it with the service. The service requires clients to prove knowledge of the delivery token for a user in order to transmit “sealed sender” messages to that user."
signal-android code dive
-
signal-android
callsSignalMessageSender#sendMessage
with anOptional.of(UniditentifedAccess)
, which has a key and a cert- note: in libsignal, the outer call to sendMessage expects a pair of
UnidentifiedAccess
with both a target and a "source" -> they extract the target and pass it to inner calls of#sendMessage
- note: in libsignal, the outer call to sendMessage expects a pair of
- the key is used used as follows:
- in
SignalServiceMessageSender#getEncryptedMessages
to identify target - in
socket#send
->PushServiceSocket#buildServiceRequest
to add aUnidentified-Access-Key
header to the request
- in
- blog: "To prevent spoofing, clients periodically retrieve a short-lived sender certificate from the service attesting to their identity."
- blog: "receiving clients can easily check its validity." (we think we don't have to implement this check b/c it is implicitly handled in underlying
libsignal-client
rust implementation)
facts:
- to send sealed sender msg to subscriber, signalc needs a delivery token. to derive delivery token, it needs a profile key. to get a profile key, it has to ask for it from server.
- "blocking a user who has access to a profile key will trigger a profile key rotation." -> we probably need to detect this state and fetch new profile keys (but we do this on every received message, and advertise to receivers on every sent message, so: check!)
- signal-android includes its own profileKey in messages it sends
questions:
- what makes a good
UNIDENTIFIED_SENDER_TRUST_ROOT
?- A: likely whatever
Signal-Android
uses, which we should check for expiry / key rotation (as we do for base cert in trust store)
- A: likely whatever
- where does "registration" of a signalc channel's delivery token happen? (as per quote in "spam prevention design" above)
- A: perhaps it already happens w/ account manager construction?
Edited by feed back