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