accept group send endorsements for multi-recipient sends

This commit is contained in:
Jonathan Klabunde Tomer 2024-04-10 16:51:09 -07:00 committed by GitHub
parent cdd2082b07
commit 2b652fe2a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 164 additions and 147 deletions

View File

@ -278,7 +278,7 @@
<dependency> <dependency>
<groupId>org.signal</groupId> <groupId>org.signal</groupId>
<artifactId>libsignal-server</artifactId> <artifactId>libsignal-server</artifactId>
<version>0.39.0</version> <version>0.44.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.signal.forks</groupId> <groupId>org.signal.forks</groupId>

View File

@ -74,7 +74,7 @@ hCaptcha.apiKey: unset
storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= 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== 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== 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==

View File

@ -319,8 +319,8 @@ storageService:
-----END CERTIFICATE----- -----END CERTIFICATE-----
zkConfig: zkConfig:
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz 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.37.serverSecret serverSecret: secret://zkConfig-libsignal-0.42.serverSecret
callingZkConfig: callingZkConfig:
serverSecret: secret://callingZkConfig.serverSecret serverSecret: secret://callingZkConfig.serverSecret

View File

@ -959,7 +959,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender, new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
accountsManager, messagesManager, pushNotificationManager, reportMessageManager, accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager, multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,
dynamicConfigurationManager, zkSecretParams, spamChecker), dynamicConfigurationManager, zkSecretParams, spamChecker, Clock.systemUTC()),
new PaymentsController(currencyManager, paymentsCredentialsGenerator), new PaymentsController(currencyManager, paymentsCredentialsGenerator),
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager, new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,

View File

@ -1,26 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.util.Base64;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialPresentation;
public record GroupSendCredentialHeader(GroupSendCredentialPresentation presentation) {
public static GroupSendCredentialHeader valueOf(String header) {
try {
return new GroupSendCredentialHeader(new GroupSendCredentialPresentation(Base64.getDecoder().decode(header)));
} catch (InvalidInputException | IllegalArgumentException e) {
// Base64 throws IllegalArgumentException; GroupSendCredentialPresentation ctor throws InvalidInputException
throw new WebApplicationException(e, Status.UNAUTHORIZED);
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.util.Base64;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
public record GroupSendTokenHeader(GroupSendFullToken token) {
public static GroupSendTokenHeader valueOf(String header) {
try {
return new GroupSendTokenHeader(new GroupSendFullToken(Base64.getDecoder().decode(header)));
} catch (InvalidInputException | IllegalArgumentException e) {
// Base64 throws IllegalArgumentException; GroupSendFullToken ctor throws InvalidInputException
throw new WebApplicationException(e, Status.UNAUTHORIZED);
}
}
}

View File

@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
@ -78,12 +79,14 @@ import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage.Recipient
import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.protocol.util.Pair;
import org.signal.libsignal.zkgroup.ServerSecretParams; import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.Anonymous; import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.CombinedUnidentifiedSenderAccessKeys; import org.whispersystems.textsecuregcm.auth.CombinedUnidentifiedSenderAccessKeys;
import org.whispersystems.textsecuregcm.auth.GroupSendCredentialHeader; import org.whispersystems.textsecuregcm.auth.GroupSendTokenHeader;
import org.whispersystems.textsecuregcm.auth.OptionalAccess; import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
@ -164,6 +167,7 @@ public class MessageController {
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager; private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final ServerSecretParams serverSecretParams; private final ServerSecretParams serverSecretParams;
private final SpamChecker spamChecker; private final SpamChecker spamChecker;
private final Clock clock;
private static final int MAX_FETCH_ACCOUNT_CONCURRENCY = 8; private static final int MAX_FETCH_ACCOUNT_CONCURRENCY = 8;
@ -211,7 +215,8 @@ public class MessageController {
final ClientReleaseManager clientReleaseManager, final ClientReleaseManager clientReleaseManager,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager, final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final ServerSecretParams serverSecretParams, final ServerSecretParams serverSecretParams,
final SpamChecker spamChecker) { final SpamChecker spamChecker,
final Clock clock) {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.messageByteLimitEstimator = messageByteLimitEstimator; this.messageByteLimitEstimator = messageByteLimitEstimator;
this.messageSender = messageSender; this.messageSender = messageSender;
@ -227,6 +232,7 @@ public class MessageController {
this.dynamicConfigurationManager = dynamicConfigurationManager; this.dynamicConfigurationManager = dynamicConfigurationManager;
this.serverSecretParams = serverSecretParams; this.serverSecretParams = serverSecretParams;
this.spamChecker = spamChecker; this.spamChecker = spamChecker;
this.clock = clock;
} }
@Timed @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="400", description="The envelope specified delivery to the same recipient device multiple times")
@ApiResponse( @ApiResponse(
responseCode="401", 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( @ApiResponse(
responseCode="404", responseCode="404",
description="The message is not a story and some of the recipient service IDs do not correspond to registered Signal users") 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))) content = @Content(schema = @Schema(implementation = AccountStaleDevices[].class)))
public Response sendMultiRecipientMessage( public Response sendMultiRecipientMessage(
@Deprecated @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, @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.") @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_CREDENTIAL) @HeaderParam(HeaderUtils.GROUP_SEND_TOKEN)
@Nullable GroupSendCredentialHeader groupSendCredential, @Nullable GroupSendTokenHeader groupSendToken,
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent, @HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
@ -484,29 +490,29 @@ public class MessageController {
return spamCheck.get(); return spamCheck.get();
} }
if (groupSendCredential == null && accessKeys == null && !isStory) { if (groupSendToken == null && accessKeys == null && !isStory) {
throw new NotAuthorizedException("A group send credential or unidentified access key is required for non-story messages"); 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) { 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) { } 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) { if (groupSendToken != null) {
// Group send credentials are checked before we even attempt to resolve any accounts, since // 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 // the lists of service IDs in the envelope are all that we need to check against
checkGroupSendCredential( checkGroupSendToken(
multiRecipientMessage.getRecipients().keySet(), multiRecipientMessage.getExcludedRecipients(), groupSendCredential); multiRecipientMessage.getRecipients().keySet(), groupSendToken);
} }
final Map<ServiceIdentifier, MultiRecipientDeliveryData> recipients = buildRecipientMap(multiRecipientMessage, isStory); final Map<ServiceIdentifier, MultiRecipientDeliveryData> recipients = buildRecipientMap(multiRecipientMessage, isStory);
// Access keys are checked against the UAK in the resolved accounts, so we have to check after resolving accounts above. // 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 // Group send endorsements are checked earlier; for stories, we don't check permissions at all because only clients check them
if (groupSendCredential == null && !isStory) { if (groupSendToken == null && !isStory) {
checkAccessKeys(accessKeys, recipients.values()); checkAccessKeys(accessKeys, recipients.values());
} }
// We might filter out all the recipients of a story (if none exist). // 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(); return Response.ok(new SendMultiRecipientMessageResponse(uuids404)).build();
} }
private void checkGroupSendCredential( private void checkGroupSendToken(
final Collection<ServiceId> recipients, final Collection<ServiceId> recipients,
final Collection<ServiceId> excludedRecipients, final @NotNull GroupSendTokenHeader groupSendToken) {
final @NotNull GroupSendCredentialHeader groupSendCredential) {
try { try {
// A group send credential covers *every* group member except the sender. However, clients final GroupSendFullToken token = groupSendToken.token();
// don't always want to actually send to every recipient in the same multi-send (most token.verify(recipients, clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
// 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);
} catch (VerificationFailedException e) { } catch (VerificationFailedException e) {
throw new NotAuthorizedException(e); throw new NotAuthorizedException(e);
} }

View File

@ -24,7 +24,7 @@ public final class HeaderUtils {
public static final String UNIDENTIFIED_ACCESS_KEY = "Unidentified-Access-Key"; 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() { private HeaderUtils() {
// utility class // utility class

View File

@ -43,6 +43,7 @@ import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; 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.ArgumentSets;
import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.zkgroup.ServerPublicParams; 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.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext; import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredential; import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialPresentation; import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialResponse; 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.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; 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.HeaderUtils;
import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.SystemMapper; import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.websocket.Stories; import org.whispersystems.websocket.Stories;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -193,6 +198,8 @@ class MessageControllerTest {
private static final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(DynamicConfigurationManager.class); private static final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
private static final TestClock clock = TestClock.now();
private static final ResourceExtension resources = ResourceExtension.builder() private static final ResourceExtension resources = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
.addProvider(AuthHelper.getAuthFilter()) .addProvider(AuthHelper.getAuthFilter())
@ -204,7 +211,7 @@ class MessageControllerTest {
new MessageController(rateLimiters, cardinalityEstimator, messageSender, receiptSender, accountsManager, new MessageController(rateLimiters, cardinalityEstimator, messageSender, receiptSender, accountsManager,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor, messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, ReportSpamTokenProvider.noop(), mock(ClientReleaseManager.class), dynamicConfigurationManager, messageDeliveryScheduler, ReportSpamTokenProvider.noop(), mock(ClientReleaseManager.class), dynamicConfigurationManager,
serverSecretParams, SpamChecker.noop())) serverSecretParams, SpamChecker.noop(), clock))
.build(); .build();
@BeforeEach @BeforeEach
@ -258,6 +265,8 @@ class MessageControllerTest {
when(rateLimiters.getInboundMessageBytes()).thenReturn(rateLimiter); when(rateLimiters.getInboundMessageBytes()).thenReturn(rateLimiter);
when(rateLimiter.validateAsync(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null)); 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, 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) 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<Recipient> recipients, final byte[] buffer, final boolean explicitIdentifiers) { private static InputStream initializeMultiPayload(final List<Recipient> 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<Recipient> recipients, final List<ServiceIdentifier> excludedRecipients, final byte[] buffer, final boolean explicitIdentifiers) { private static InputStream initializeMultiPayload(final List<Recipient> recipients, final byte[] buffer, final boolean explicitIdentifiers, final int payloadSize) {
return initializeMultiPayload(recipients, excludedRecipients, buffer, explicitIdentifiers, 39);
}
private static InputStream initializeMultiPayload(final List<Recipient> recipients, final List<ServiceIdentifier> excludedRecipients, final byte[] buffer, final boolean explicitIdentifiers, final int payloadSize) {
// initialize a binary payload according to our wire format // initialize a binary payload according to our wire format
ByteBuffer bb = ByteBuffer.wrap(buffer); ByteBuffer bb = ByteBuffer.wrap(buffer);
bb.order(ByteOrder.BIG_ENDIAN); bb.order(ByteOrder.BIG_ENDIAN);
@ -1007,10 +1002,9 @@ class MessageControllerTest {
bb.put(explicitIdentifiers ? (byte) 0x23 : (byte) 0x22); // version byte bb.put(explicitIdentifiers ? (byte) 0x23 : (byte) 0x22); // version byte
// count varint // count varint
writeVarint(bb, recipients.size() + excludedRecipients.size()); writeVarint(bb, recipients.size());
recipients.forEach(recipient -> writeMultiPayloadRecipient(bb, recipient, explicitIdentifiers)); recipients.forEach(recipient -> writeMultiPayloadRecipient(bb, recipient, explicitIdentifiers));
excludedRecipients.forEach(recipient -> writeMultiPayloadExcludedRecipient(bb, recipient, explicitIdentifiers));
// now write the actual message body (empty for now) // now write the actual message body (empty for now)
assert(payloadSize >= 32); assert(payloadSize >= 32);
@ -1269,24 +1263,20 @@ class MessageControllerTest {
.argumentsForNextParameter(false, true); // urgent .argumentsForNextParameter(false, true); // urgent
} }
@ParameterizedTest @Test
@MethodSource void testMultiRecipientMessageWithGroupSendEndorsements() throws Exception {
void testMultiRecipientMessageWithGroupSendCredential( final List<Recipient> recipients = List.of(
List<ServiceIdentifier> includedRecipients, new Recipient(SINGLE_DEVICE_ACI_ID, SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, new byte[48]),
List<ServiceIdentifier> excludedRecipients, new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]),
int expectedStatus, new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]));
int expectedMessagesSent) throws Exception {
final List<Recipient> recipients = new ArrayList<>();
includedRecipients.forEach(
serviceIdentifier -> multiRecipientTargetMap().get(serviceIdentifier).forEach(
(deviceId, registrationId) ->
recipients.add(new Recipient(serviceIdentifier, deviceId, registrationId, new byte[48]))));
// initialize our binary payload and create an input stream // initialize our binary payload and create an input stream
byte[] buffer = new byte[2048]; 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()); final AciServiceIdentifier senderId = new AciServiceIdentifier(UUID.randomUUID());
clock.pin(Instant.parse("2024-04-09T12:00:00.00Z"));
Response response = resources Response response = resources
.getJerseyTest() .getJerseyTest()
.target("/v1/messages/multi_recipient") .target("/v1/messages/multi_recipient")
@ -1296,77 +1286,106 @@ class MessageControllerTest {
.queryParam("urgent", false) .queryParam("urgent", false)
.request() .request()
.header(HttpHeaders.USER_AGENT, "FIXME") .header(HttpHeaders.USER_AGENT, "FIXME")
.header(HeaderUtils.GROUP_SEND_CREDENTIAL, validGroupSendCredentialHeader( .header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
senderId, senderId, List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
List.of(senderId, SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID)))
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE)); .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, verify(messageSender,
exactly(expectedMessagesSent)) exactly(3))
.sendMessage( .sendMessage(
any(), any(),
any(), any(),
argThat(env -> !env.hasSourceUuid() && !env.hasSourceDevice()), argThat(env -> !env.hasSourceUuid() && !env.hasSourceDevice()),
eq(true)); eq(true));
if (expectedStatus == 200) { SendMultiRecipientMessageResponse smrmr = response.readEntity(SendMultiRecipientMessageResponse.class);
SendMultiRecipientMessageResponse smrmr = response.readEntity(SendMultiRecipientMessageResponse.class); assertThat(smrmr.uuids404(), is(empty()));
assertThat(smrmr.uuids404(), is(empty()));
}
} }
private static Stream<Arguments> testMultiRecipientMessageWithGroupSendCredential() { @Test
return Stream.of( void testMultiRecipientMessageWithInvalidGroupSendEndorsements() throws Exception {
// All members present in included or excluded recipients: success, deliver to included recipients only final List<Recipient> recipients = List.of(
Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), List.of(), 200, 3), new Recipient(NONEXISTENT_ACI_ID, SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, new byte[48]),
Arguments.of(List.of(SINGLE_DEVICE_ACI_ID), List.of(MULTI_DEVICE_ACI_ID), 200, 1), new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]),
Arguments.of(List.of(MULTI_DEVICE_ACI_ID), List.of(SINGLE_DEVICE_ACI_ID), 200, 2), new Recipient(MULTI_DEVICE_ACI_ID, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]));
// No included recipients: request is bad // initialize our binary payload and create an input stream
Arguments.of(List.of(), List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), 400, 0), 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 clock.pin(Instant.parse("2024-04-09T12:00:00.00Z"));
Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), List.of(SINGLE_DEVICE_ACI_ID), 400, 0),
// Included recipient not covered by credential: forbid Response response = resources
Arguments.of(List.of(NONEXISTENT_ACI_ID), List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), 401, 0), .getJerseyTest()
Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, NONEXISTENT_ACI_ID), List.of(MULTI_DEVICE_ACI_ID), 401, 0), .target("/v1/messages/multi_recipient")
Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID, NONEXISTENT_ACI_ID), List.of(), 401, 0), .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 assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
Arguments.of(List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), List.of(NONEXISTENT_ACI_ID), 401, 0), verifyNoMoreInteractions(messageSender);
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));
} }
private String validGroupSendCredentialHeader(AciServiceIdentifier sender, List<ServiceIdentifier> allGroupMembers) throws Exception { @Test
void testMultiRecipientMessageWithExpiredGroupSendEndorsements() throws Exception {
final List<Recipient> 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<ServiceIdentifier> recipients, Instant expiration) throws Exception {
final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
final GroupMasterKey groupMasterKey = new GroupMasterKey(new byte[32]); final GroupMasterKey groupMasterKey = new GroupMasterKey(new byte[32]);
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams); final ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
UuidCiphertext senderCiphertext = clientZkGroupCipher.encrypt(sender.toLibsignal()); List<ServiceId> groupPlaintexts = Stream.concat(Stream.of(sender), recipients.stream()).map(ServiceIdentifier::toLibsignal).toList();
List<UuidCiphertext> groupCiphertexts = allGroupMembers.stream() List<UuidCiphertext> groupCiphertexts = groupPlaintexts.stream()
.map(ServiceIdentifier::toLibsignal)
.map(clientZkGroupCipher::encrypt) .map(clientZkGroupCipher::encrypt)
.collect(Collectors.toList()); .toList();
GroupSendCredentialResponse credentialResponse = GroupSendDerivedKeyPair keyPair = GroupSendDerivedKeyPair.forExpiration(expiration, serverSecretParams);
GroupSendCredentialResponse.issueCredential(groupCiphertexts, senderCiphertext, serverSecretParams); GroupSendEndorsementsResponse endorsementsResponse =
GroupSendCredential credential = GroupSendEndorsementsResponse.issue(groupCiphertexts, keyPair);
credentialResponse.receive( ReceivedEndorsements endorsements =
allGroupMembers.stream().map(ServiceIdentifier::toLibsignal).collect(Collectors.toList()), endorsementsResponse.receive(
groupPlaintexts,
sender.toLibsignal(), sender.toLibsignal(),
serverPublicParams, expiration.minus(Duration.ofDays(1)),
groupSecretParams); groupSecretParams,
GroupSendCredentialPresentation presentation = credential.present(serverPublicParams); serverPublicParams);
return Base64.getEncoder().encodeToString(presentation.serialize()); GroupSendFullToken token = endorsements.combinedEndorsement().toFullToken(groupSecretParams, expiration);
return Base64.getEncoder().encodeToString(token.serialize());
} }
@ParameterizedTest @ParameterizedTest
@ -1408,7 +1427,7 @@ class MessageControllerTest {
.request() .request()
.header(HttpHeaders.USER_AGENT, "cluck cluck, i'm a parrot") .header(HttpHeaders.USER_AGENT, "cluck cluck, i'm a parrot")
.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)) .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); checkBadMultiRecipientResponse(response, 400);
} }

@ -1 +1 @@
Subproject commit 89e44cd170366326a400d95c596be1c5eb439c46 Subproject commit fa14e55cdb88a7794c6d13bc459f8e2c646a4316