From 2b652fe2a96e1d7d9ee1f58dc0107ede656d20d6 Mon Sep 17 00:00:00 2001
From: Jonathan Klabunde Tomer <125505367+jkt-signal@users.noreply.github.com>
Date: Wed, 10 Apr 2024 16:51:09 -0700
Subject: [PATCH] accept group send endorsements for multi-recipient sends
---
pom.xml | 2 +-
service/config/sample-secrets-bundle.yml | 2 +-
service/config/sample.yml | 4 +-
.../textsecuregcm/WhisperServerService.java | 2 +-
.../auth/GroupSendCredentialHeader.java | 26 ---
.../auth/GroupSendTokenHeader.java | 26 +++
.../controllers/MessageController.java | 58 +++---
.../textsecuregcm/util/HeaderUtils.java | 2 +-
.../controllers/MessageControllerTest.java | 187 ++++++++++--------
spam-filter | 2 +-
10 files changed, 164 insertions(+), 147 deletions(-)
delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/auth/GroupSendCredentialHeader.java
create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/auth/GroupSendTokenHeader.java
diff --git a/pom.xml b/pom.xml
index 5313f2d67..6b11fed7a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -278,7 +278,7 @@
org.signal
libsignal-server
- 0.39.0
+ 0.44.0
org.signal.forks
diff --git a/service/config/sample-secrets-bundle.yml b/service/config/sample-secrets-bundle.yml
index b33000665..a3c9addda 100644
--- a/service/config/sample-secrets-bundle.yml
+++ b/service/config/sample-secrets-bundle.yml
@@ -74,7 +74,7 @@ hCaptcha.apiKey: unset
storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
-zkConfig-libsignal-0.37.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
+zkConfig-libsignal-0.42.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdef
genericZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
callingZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
diff --git a/service/config/sample.yml b/service/config/sample.yml
index 8eab1952b..0df5f4571 100644
--- a/service/config/sample.yml
+++ b/service/config/sample.yml
@@ -319,8 +319,8 @@ storageService:
-----END CERTIFICATE-----
zkConfig:
- serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
- serverSecret: secret://zkConfig-libsignal-0.37.serverSecret
+ serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAB==
+ serverSecret: secret://zkConfig-libsignal-0.42.serverSecret
callingZkConfig:
serverSecret: secret://callingZkConfig.serverSecret
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index 02515bd0c..fe1503b2f 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -959,7 +959,7 @@ public class WhisperServerService extends Application dynamicConfigurationManager;
private final ServerSecretParams serverSecretParams;
private final SpamChecker spamChecker;
+ private final Clock clock;
private static final int MAX_FETCH_ACCOUNT_CONCURRENCY = 8;
@@ -211,7 +215,8 @@ public class MessageController {
final ClientReleaseManager clientReleaseManager,
final DynamicConfigurationManager dynamicConfigurationManager,
final ServerSecretParams serverSecretParams,
- final SpamChecker spamChecker) {
+ final SpamChecker spamChecker,
+ final Clock clock) {
this.rateLimiters = rateLimiters;
this.messageByteLimitEstimator = messageByteLimitEstimator;
this.messageSender = messageSender;
@@ -227,6 +232,7 @@ public class MessageController {
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.serverSecretParams = serverSecretParams;
this.spamChecker = spamChecker;
+ this.clock = clock;
}
@Timed
@@ -442,7 +448,7 @@ public class MessageController {
@ApiResponse(responseCode="400", description="The envelope specified delivery to the same recipient device multiple times")
@ApiResponse(
responseCode="401",
- description="The message is not a story and the unauthorized access key or group send credential is missing or incorrect")
+ description="The message is not a story and the unauthorized access key or group send endorsement token is missing or incorrect")
@ApiResponse(
responseCode="404",
description="The message is not a story and some of the recipient service IDs do not correspond to registered Signal users")
@@ -454,12 +460,12 @@ public class MessageController {
content = @Content(schema = @Schema(implementation = AccountStaleDevices[].class)))
public Response sendMultiRecipientMessage(
@Deprecated
- @Parameter(description="The bitwise xor of the unidentified access keys for every recipient of the message. Will be replaced with group send credentials")
+ @Parameter(description="The bitwise xor of the unidentified access keys for every recipient of the message. Will be replaced with group send endorsements")
@HeaderParam(HeaderUtils.UNIDENTIFIED_ACCESS_KEY) @Nullable CombinedUnidentifiedSenderAccessKeys accessKeys,
- @Parameter(description="A group send credential covering all (included and excluded) recipients of the message. Must not be combined with `Unidentified-Access-Key` or set on a story message.")
- @HeaderParam(HeaderUtils.GROUP_SEND_CREDENTIAL)
- @Nullable GroupSendCredentialHeader groupSendCredential,
+ @Parameter(description="A group send endorsement token covering recipients of this message. Must not be combined with `Unidentified-Access-Key` or set on a story message.")
+ @HeaderParam(HeaderUtils.GROUP_SEND_TOKEN)
+ @Nullable GroupSendTokenHeader groupSendToken,
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
@@ -484,29 +490,29 @@ public class MessageController {
return spamCheck.get();
}
- if (groupSendCredential == null && accessKeys == null && !isStory) {
- throw new NotAuthorizedException("A group send credential or unidentified access key is required for non-story messages");
+ if (groupSendToken == null && accessKeys == null && !isStory) {
+ throw new NotAuthorizedException("A group send endorsement token or unidentified access key is required for non-story messages");
}
- if (groupSendCredential != null) {
+ if (groupSendToken != null) {
if (accessKeys != null) {
- throw new BadRequestException("Only one of group send credential and unidentified access key may be provided");
+ throw new BadRequestException("Only one of group send endorsement token and unidentified access key may be provided");
} else if (isStory) {
- throw new BadRequestException("Stories should not provide a group send credential");
+ throw new BadRequestException("Stories should not provide a group send endorsement token");
}
}
- if (groupSendCredential != null) {
- // Group send credentials are checked before we even attempt to resolve any accounts, since
+ if (groupSendToken != null) {
+ // Group send endorsements are checked before we even attempt to resolve any accounts, since
// the lists of service IDs in the envelope are all that we need to check against
- checkGroupSendCredential(
- multiRecipientMessage.getRecipients().keySet(), multiRecipientMessage.getExcludedRecipients(), groupSendCredential);
+ checkGroupSendToken(
+ multiRecipientMessage.getRecipients().keySet(), groupSendToken);
}
final Map recipients = buildRecipientMap(multiRecipientMessage, isStory);
// Access keys are checked against the UAK in the resolved accounts, so we have to check after resolving accounts above.
- // Group send credentials are checked earlier; for stories, we don't check permissions at all because only clients check them
- if (groupSendCredential == null && !isStory) {
+ // Group send endorsements are checked earlier; for stories, we don't check permissions at all because only clients check them
+ if (groupSendToken == null && !isStory) {
checkAccessKeys(accessKeys, recipients.values());
}
// We might filter out all the recipients of a story (if none exist).
@@ -623,20 +629,12 @@ public class MessageController {
return Response.ok(new SendMultiRecipientMessageResponse(uuids404)).build();
}
- private void checkGroupSendCredential(
+ private void checkGroupSendToken(
final Collection recipients,
- final Collection excludedRecipients,
- final @NotNull GroupSendCredentialHeader groupSendCredential) {
+ final @NotNull GroupSendTokenHeader groupSendToken) {
try {
- // A group send credential covers *every* group member except the sender. However, clients
- // don't always want to actually send to every recipient in the same multi-send (most
- // commonly because a new member needs an SKDM first, but also could be because the sender
- // has blocked someone). So we check the group send credential against the combination of
- // the actual recipients and the supplied list of "excluded" recipients, accounts the
- // sender knows are part of the credential but doesn't want to send to right now.
- groupSendCredential.presentation().verify(
- Lists.newArrayList(Iterables.concat(recipients, excludedRecipients)),
- serverSecretParams);
+ final GroupSendFullToken token = groupSendToken.token();
+ token.verify(recipients, clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
} catch (VerificationFailedException e) {
throw new NotAuthorizedException(e);
}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/HeaderUtils.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/HeaderUtils.java
index c4aa3f622..72a905360 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/util/HeaderUtils.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/HeaderUtils.java
@@ -24,7 +24,7 @@ public final class HeaderUtils {
public static final String UNIDENTIFIED_ACCESS_KEY = "Unidentified-Access-Key";
- public static final String GROUP_SEND_CREDENTIAL = "Group-Send-Credential";
+ public static final String GROUP_SEND_TOKEN = "Group-Send-Token";
private HeaderUtils() {
// utility class
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java
index 1ed2e29ea..9c086aebf 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java
@@ -43,6 +43,7 @@ import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
@@ -80,6 +81,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.junitpioneer.jupiter.cartesian.ArgumentSets;
import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.mockito.ArgumentCaptor;
+import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.zkgroup.ServerPublicParams;
@@ -88,9 +90,11 @@ import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
-import org.signal.libsignal.zkgroup.groupsend.GroupSendCredential;
-import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialPresentation;
-import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialResponse;
+import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
+import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
+import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
+import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse.ReceivedEndorsements;
+import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
@@ -134,6 +138,7 @@ import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.SystemMapper;
+import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.websocket.Stories;
import reactor.core.publisher.Mono;
@@ -193,6 +198,8 @@ class MessageControllerTest {
private static final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
+ private static final TestClock clock = TestClock.now();
+
private static final ResourceExtension resources = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
.addProvider(AuthHelper.getAuthFilter())
@@ -204,7 +211,7 @@ class MessageControllerTest {
new MessageController(rateLimiters, cardinalityEstimator, messageSender, receiptSender, accountsManager,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, ReportSpamTokenProvider.noop(), mock(ClientReleaseManager.class), dynamicConfigurationManager,
- serverSecretParams, SpamChecker.noop()))
+ serverSecretParams, SpamChecker.noop(), clock))
.build();
@BeforeEach
@@ -258,6 +265,8 @@ class MessageControllerTest {
when(rateLimiters.getInboundMessageBytes()).thenReturn(rateLimiter);
when(rateLimiter.validateAsync(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null));
+
+ clock.unpin();
}
private static Device generateTestDevice(final byte id, final int registrationId, final int pniRegistrationId,
@@ -980,25 +989,11 @@ class MessageControllerTest {
bb.put(r.perRecipientKeyMaterial()); // key material (48 bytes)
}
- private static void writeMultiPayloadExcludedRecipient(final ByteBuffer bb, final ServiceIdentifier id, final boolean useExplicitIdentifier) {
- if (useExplicitIdentifier) {
- bb.put(id.toFixedWidthByteArray());
- } else {
- bb.put(UUIDUtil.toBytes(id.uuid()));
- }
-
- bb.put((byte) 0);
- }
-
private static InputStream initializeMultiPayload(final List recipients, final byte[] buffer, final boolean explicitIdentifiers) {
- return initializeMultiPayload(recipients, List.of(), buffer, explicitIdentifiers, 39);
+ return initializeMultiPayload(recipients, buffer, explicitIdentifiers, 39);
}
- private static InputStream initializeMultiPayload(final List recipients, final List excludedRecipients, final byte[] buffer, final boolean explicitIdentifiers) {
- return initializeMultiPayload(recipients, excludedRecipients, buffer, explicitIdentifiers, 39);
- }
-
- private static InputStream initializeMultiPayload(final List recipients, final List excludedRecipients, final byte[] buffer, final boolean explicitIdentifiers, final int payloadSize) {
+ private static InputStream initializeMultiPayload(final List recipients, final byte[] buffer, final boolean explicitIdentifiers, final int payloadSize) {
// initialize a binary payload according to our wire format
ByteBuffer bb = ByteBuffer.wrap(buffer);
bb.order(ByteOrder.BIG_ENDIAN);
@@ -1007,10 +1002,9 @@ class MessageControllerTest {
bb.put(explicitIdentifiers ? (byte) 0x23 : (byte) 0x22); // version byte
// count varint
- writeVarint(bb, recipients.size() + excludedRecipients.size());
+ writeVarint(bb, recipients.size());
recipients.forEach(recipient -> writeMultiPayloadRecipient(bb, recipient, explicitIdentifiers));
- excludedRecipients.forEach(recipient -> writeMultiPayloadExcludedRecipient(bb, recipient, explicitIdentifiers));
// now write the actual message body (empty for now)
assert(payloadSize >= 32);
@@ -1269,24 +1263,20 @@ class MessageControllerTest {
.argumentsForNextParameter(false, true); // urgent
}
- @ParameterizedTest
- @MethodSource
- void testMultiRecipientMessageWithGroupSendCredential(
- List includedRecipients,
- List excludedRecipients,
- int expectedStatus,
- int expectedMessagesSent) throws Exception {
- final List recipients = new ArrayList<>();
- includedRecipients.forEach(
- serviceIdentifier -> multiRecipientTargetMap().get(serviceIdentifier).forEach(
- (deviceId, registrationId) ->
- recipients.add(new Recipient(serviceIdentifier, deviceId, registrationId, new byte[48]))));
+ @Test
+ void testMultiRecipientMessageWithGroupSendEndorsements() throws Exception {
+ final List recipients = List.of(
+ new Recipient(SINGLE_DEVICE_ACI_ID, SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, new byte[48]),
+ new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]),
+ new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]));
// initialize our binary payload and create an input stream
byte[] buffer = new byte[2048];
- InputStream stream = initializeMultiPayload(recipients, excludedRecipients, buffer, true);
+ InputStream stream = initializeMultiPayload(recipients, buffer, true);
final AciServiceIdentifier senderId = new AciServiceIdentifier(UUID.randomUUID());
+ clock.pin(Instant.parse("2024-04-09T12:00:00.00Z"));
+
Response response = resources
.getJerseyTest()
.target("/v1/messages/multi_recipient")
@@ -1296,77 +1286,106 @@ class MessageControllerTest {
.queryParam("urgent", false)
.request()
.header(HttpHeaders.USER_AGENT, "FIXME")
- .header(HeaderUtils.GROUP_SEND_CREDENTIAL, validGroupSendCredentialHeader(
- senderId,
- List.of(senderId, SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID)))
+ .header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
+ senderId, List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
- assertThat("Unexpected response", response.getStatus(), is(equalTo(expectedStatus)));
+ assertThat("Unexpected response", response.getStatus(), is(equalTo(200)));
verify(messageSender,
- exactly(expectedMessagesSent))
+ exactly(3))
.sendMessage(
any(),
any(),
argThat(env -> !env.hasSourceUuid() && !env.hasSourceDevice()),
eq(true));
- if (expectedStatus == 200) {
- SendMultiRecipientMessageResponse smrmr = response.readEntity(SendMultiRecipientMessageResponse.class);
- assertThat(smrmr.uuids404(), is(empty()));
- }
+ SendMultiRecipientMessageResponse smrmr = response.readEntity(SendMultiRecipientMessageResponse.class);
+ assertThat(smrmr.uuids404(), is(empty()));
}
- private static Stream testMultiRecipientMessageWithGroupSendCredential() {
- return Stream.of(
- // All members present in included or excluded recipients: success, deliver to included recipients only
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), List.of(), 200, 3),
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID), List.of(MULTI_DEVICE_ACI_ID), 200, 1),
- Arguments.of(List.of(MULTI_DEVICE_ACI_ID), List.of(SINGLE_DEVICE_ACI_ID), 200, 2),
+ @Test
+ void testMultiRecipientMessageWithInvalidGroupSendEndorsements() throws Exception {
+ final List recipients = List.of(
+ new Recipient(NONEXISTENT_ACI_ID, SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, new byte[48]),
+ new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]),
+ new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]));
- // No included recipients: request is bad
- Arguments.of(List.of(), List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), 400, 0),
+ // initialize our binary payload and create an input stream
+ byte[] buffer = new byte[2048];
+ InputStream stream = initializeMultiPayload(recipients, buffer, true);
+ final AciServiceIdentifier senderId = new AciServiceIdentifier(UUID.randomUUID());
- // Some recipients both included and excluded: request is bad
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), List.of(SINGLE_DEVICE_ACI_ID), 400, 0),
+ clock.pin(Instant.parse("2024-04-09T12:00:00.00Z"));
- // Included recipient not covered by credential: forbid
- Arguments.of(List.of(NONEXISTENT_ACI_ID), List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), 401, 0),
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, NONEXISTENT_ACI_ID), List.of(MULTI_DEVICE_ACI_ID), 401, 0),
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID, NONEXISTENT_ACI_ID), List.of(), 401, 0),
+ Response response = resources
+ .getJerseyTest()
+ .target("/v1/messages/multi_recipient")
+ .queryParam("online", true)
+ .queryParam("ts", 1663798405641L)
+ .queryParam("story", false)
+ .queryParam("urgent", false)
+ .request()
+ .header(HttpHeaders.USER_AGENT, "FIXME")
+ .header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
+ senderId, List.of(MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
+ .put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
- // Excluded recipient not covered by credential: forbid
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), List.of(NONEXISTENT_ACI_ID), 401, 0),
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID), List.of(NONEXISTENT_ACI_ID, MULTI_DEVICE_ACI_ID), 401, 0),
- Arguments.of(List.of(MULTI_DEVICE_ACI_ID), List.of(NONEXISTENT_ACI_ID, SINGLE_DEVICE_ACI_ID), 401, 0),
-
- // Some recipients not in included or excluded list: forbid
- Arguments.of(List.of(SINGLE_DEVICE_ACI_ID), List.of(), 401, 0),
- Arguments.of(List.of(MULTI_DEVICE_ACI_ID), List.of(), 401, 0),
-
- // Substituting a PNI for an ACI is not allowed
- Arguments.of(List.of(SINGLE_DEVICE_PNI_ID, MULTI_DEVICE_ACI_ID), List.of(), 401, 0));
+ assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
+ verifyNoMoreInteractions(messageSender);
}
- private String validGroupSendCredentialHeader(AciServiceIdentifier sender, List allGroupMembers) throws Exception {
+ @Test
+ void testMultiRecipientMessageWithExpiredGroupSendEndorsements() throws Exception {
+ final List recipients = List.of(
+ new Recipient(SINGLE_DEVICE_ACI_ID, SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, new byte[48]),
+ new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]),
+ new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]));
+
+ // initialize our binary payload and create an input stream
+ byte[] buffer = new byte[2048];
+ InputStream stream = initializeMultiPayload(recipients, buffer, true);
+ final AciServiceIdentifier senderId = new AciServiceIdentifier(UUID.randomUUID());
+
+ clock.pin(Instant.parse("2024-04-10T12:00:00.00Z"));
+
+ Response response = resources
+ .getJerseyTest()
+ .target("/v1/messages/multi_recipient")
+ .queryParam("online", true)
+ .queryParam("ts", 1663798405641L)
+ .queryParam("story", false)
+ .queryParam("urgent", false)
+ .request()
+ .header(HttpHeaders.USER_AGENT, "FIXME")
+ .header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
+ senderId, List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
+ .put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
+
+ assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
+ verifyNoMoreInteractions(messageSender);
+ }
+
+ private String validGroupSendTokenHeader(AciServiceIdentifier sender, List recipients, Instant expiration) throws Exception {
final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
final GroupMasterKey groupMasterKey = new GroupMasterKey(new byte[32]);
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
- UuidCiphertext senderCiphertext = clientZkGroupCipher.encrypt(sender.toLibsignal());
- List groupCiphertexts = allGroupMembers.stream()
- .map(ServiceIdentifier::toLibsignal)
+ List groupPlaintexts = Stream.concat(Stream.of(sender), recipients.stream()).map(ServiceIdentifier::toLibsignal).toList();
+ List groupCiphertexts = groupPlaintexts.stream()
.map(clientZkGroupCipher::encrypt)
- .collect(Collectors.toList());
- GroupSendCredentialResponse credentialResponse =
- GroupSendCredentialResponse.issueCredential(groupCiphertexts, senderCiphertext, serverSecretParams);
- GroupSendCredential credential =
- credentialResponse.receive(
- allGroupMembers.stream().map(ServiceIdentifier::toLibsignal).collect(Collectors.toList()),
+ .toList();
+ GroupSendDerivedKeyPair keyPair = GroupSendDerivedKeyPair.forExpiration(expiration, serverSecretParams);
+ GroupSendEndorsementsResponse endorsementsResponse =
+ GroupSendEndorsementsResponse.issue(groupCiphertexts, keyPair);
+ ReceivedEndorsements endorsements =
+ endorsementsResponse.receive(
+ groupPlaintexts,
sender.toLibsignal(),
- serverPublicParams,
- groupSecretParams);
- GroupSendCredentialPresentation presentation = credential.present(serverPublicParams);
- return Base64.getEncoder().encodeToString(presentation.serialize());
+ expiration.minus(Duration.ofDays(1)),
+ groupSecretParams,
+ serverPublicParams);
+ GroupSendFullToken token = endorsements.combinedEndorsement().toFullToken(groupSecretParams, expiration);
+ return Base64.getEncoder().encodeToString(token.serialize());
}
@ParameterizedTest
@@ -1408,7 +1427,7 @@ class MessageControllerTest {
.request()
.header(HttpHeaders.USER_AGENT, "cluck cluck, i'm a parrot")
.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES))
- .put(Entity.entity(initializeMultiPayload(recipients, List.of(), new byte[257<<10], true, 256<<10), MultiRecipientMessageProvider.MEDIA_TYPE));
+ .put(Entity.entity(initializeMultiPayload(recipients, new byte[257<<10], true, 256<<10), MultiRecipientMessageProvider.MEDIA_TYPE));
checkBadMultiRecipientResponse(response, 400);
}
diff --git a/spam-filter b/spam-filter
index 89e44cd17..fa14e55cd 160000
--- a/spam-filter
+++ b/spam-filter
@@ -1 +1 @@
-Subproject commit 89e44cd170366326a400d95c596be1c5eb439c46
+Subproject commit fa14e55cdb88a7794c6d13bc459f8e2c646a4316