Skip to content

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 the PreKeyStore (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)
    • we then include this pair as the 2nd argument to SignalServiceMessageSender#sendMessage (which is currently always null)
  • 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 calls SignalMessageSender#sendMessage with an Optional.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
  • the key is used used as follows:
    • in SignalServiceMessageSender#getEncryptedMessages to identify target
    • in socket#send -> PushServiceSocket#buildServiceRequest to add a Unidentified-Access-Key header to the request
  • 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)
  • 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
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information