mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 03:28:05 +01:00
accept group send endorsements for multi-recipient sends
This commit is contained in:
committed by
GitHub
parent
cdd2082b07
commit
2b652fe2a9
@@ -959,7 +959,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
|
||||
accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
|
||||
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,
|
||||
dynamicConfigurationManager, zkSecretParams, spamChecker),
|
||||
dynamicConfigurationManager, zkSecretParams, spamChecker, Clock.systemUTC()),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
|
||||
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
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.zkgroup.ServerSecretParams;
|
||||
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.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
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.UnidentifiedAccessUtil;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
@@ -164,6 +167,7 @@ public class MessageController {
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> 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<DynamicConfiguration> 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<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.
|
||||
// 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<ServiceId> recipients,
|
||||
final Collection<ServiceId> 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user