mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 10:18:04 +01:00
Group Send Endorsement support for pre-key fetch endpoint
This commit is contained in:
committed by
GitHub
parent
ab64828661
commit
b8f64fe3d4
@@ -29,13 +29,18 @@ import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.Invocation;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
@@ -52,6 +57,7 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.CheckKeysRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||
@@ -64,6 +70,7 @@ import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||
@@ -79,6 +86,7 @@ import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class KeysControllerTest {
|
||||
@@ -86,8 +94,13 @@ class KeysControllerTest {
|
||||
private static final String EXISTS_NUMBER = "+14152222222";
|
||||
private static final UUID EXISTS_UUID = UUID.randomUUID();
|
||||
private static final UUID EXISTS_PNI = UUID.randomUUID();
|
||||
private static final AciServiceIdentifier EXISTS_ACI = new AciServiceIdentifier(EXISTS_UUID);
|
||||
|
||||
private static final UUID OTHER_UUID = UUID.randomUUID();
|
||||
private static final AciServiceIdentifier OTHER_ACI = new AciServiceIdentifier(OTHER_UUID);
|
||||
|
||||
private static final UUID NOT_EXISTS_UUID = UUID.randomUUID();
|
||||
private static final AciServiceIdentifier NOT_EXISTS_ACI = new AciServiceIdentifier(NOT_EXISTS_UUID);
|
||||
|
||||
private static final byte SAMPLE_DEVICE_ID = 1;
|
||||
private static final byte SAMPLE_DEVICE_ID2 = 2;
|
||||
@@ -136,6 +149,10 @@ class KeysControllerTest {
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static final RateLimiter rateLimiter = mock(RateLimiter.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())
|
||||
@@ -143,7 +160,7 @@ class KeysControllerTest {
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new ServerRejectedExceptionMapper())
|
||||
.addResource(new KeysController(rateLimiters, KEYS, accounts))
|
||||
.addResource(new KeysController(rateLimiters, KEYS, accounts, serverSecretParams, clock))
|
||||
.addResource(new RateLimitExceededExceptionMapper())
|
||||
.build();
|
||||
|
||||
@@ -183,6 +200,8 @@ class KeysControllerTest {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
clock.unpin();
|
||||
|
||||
sampleDevice = mock(Device.class);
|
||||
final Device sampleDevice2 = mock(Device.class);
|
||||
final Device sampleDevice3 = mock(Device.class);
|
||||
@@ -529,6 +548,68 @@ class KeysControllerTest {
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testGetKeysWithGroupSendEndorsement(
|
||||
ServiceIdentifier target, ServiceIdentifier authorizedTarget, Duration timeLeft, boolean includeUak, int expectedResponse) throws Exception {
|
||||
|
||||
final Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS);
|
||||
clock.pin(expiration.minus(timeLeft));
|
||||
|
||||
Invocation.Builder builder = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", target.toServiceIdentifierString()))
|
||||
.queryParam("pq", "true")
|
||||
.request()
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(serverSecretParams, List.of(authorizedTarget), expiration));
|
||||
|
||||
if (includeUak) {
|
||||
builder = builder.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes()));
|
||||
}
|
||||
|
||||
Response response = builder.get();
|
||||
assertThat(response.getStatus()).isEqualTo(expectedResponse);
|
||||
|
||||
if (expectedResponse == 200) {
|
||||
PreKeyResponse result = response.readEntity(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI));
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY, result.getDevice(SAMPLE_DEVICE_ID).getPreKey());
|
||||
assertEquals(SAMPLE_PQ_KEY, result.getDevice(SAMPLE_DEVICE_ID).getPqPreKey());
|
||||
assertEquals(SAMPLE_SIGNED_KEY, result.getDevice(SAMPLE_DEVICE_ID).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID);
|
||||
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID);
|
||||
}
|
||||
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> testGetKeysWithGroupSendEndorsement() {
|
||||
return Stream.of(
|
||||
// valid endorsement
|
||||
Arguments.of(EXISTS_ACI, EXISTS_ACI, Duration.ofHours(1), false, 200),
|
||||
|
||||
// expired endorsement, not authorized
|
||||
Arguments.of(EXISTS_ACI, EXISTS_ACI, Duration.ofHours(-1), false, 401),
|
||||
|
||||
// endorsement for the wrong recipient, not authorized
|
||||
Arguments.of(EXISTS_ACI, OTHER_ACI, Duration.ofHours(1), false, 401),
|
||||
|
||||
// expired endorsement for the wrong recipient, not authorized
|
||||
Arguments.of(EXISTS_ACI, OTHER_ACI, Duration.ofHours(-1), false, 401),
|
||||
|
||||
// valid endorsement for the right recipient but they aren't registered, not found
|
||||
Arguments.of(NOT_EXISTS_ACI, NOT_EXISTS_ACI, Duration.ofHours(1), false, 404),
|
||||
|
||||
// expired endorsement for the right recipient but they aren't registered, not authorized (NOT not found)
|
||||
Arguments.of(NOT_EXISTS_ACI, NOT_EXISTS_ACI, Duration.ofHours(-1), false, 401),
|
||||
|
||||
// valid endorsement but also a UAK, bad request
|
||||
Arguments.of(EXISTS_ACI, EXISTS_ACI, Duration.ofHours(1), true, 400));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoDevices() {
|
||||
|
||||
|
||||
@@ -433,7 +433,7 @@ class MessageControllerTest {
|
||||
.queryParam("story", story)
|
||||
.request()
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN,
|
||||
validGroupSendTokenHeader(List.of(authorizedRecipient), expiration));
|
||||
AuthHelper.validGroupSendTokenHeader(serverSecretParams, List.of(authorizedRecipient), expiration));
|
||||
|
||||
if (includeUak) {
|
||||
builder = builder.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES));
|
||||
@@ -1351,8 +1351,8 @@ class MessageControllerTest {
|
||||
.queryParam("urgent", false)
|
||||
.request()
|
||||
.header(HttpHeaders.USER_AGENT, "FIXME")
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
|
||||
List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
|
||||
serverSecretParams, 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(200)));
|
||||
@@ -1389,8 +1389,8 @@ class MessageControllerTest {
|
||||
.queryParam("urgent", false)
|
||||
.request()
|
||||
.header(HttpHeaders.USER_AGENT, "FIXME")
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
|
||||
List.of(MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
|
||||
serverSecretParams, List.of(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)));
|
||||
@@ -1419,39 +1419,14 @@ class MessageControllerTest {
|
||||
.queryParam("urgent", false)
|
||||
.request()
|
||||
.header(HttpHeaders.USER_AGENT, "FIXME")
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
|
||||
List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
|
||||
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
|
||||
serverSecretParams, 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(List<ServiceIdentifier> 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);
|
||||
|
||||
final ServiceId.Aci sender = new ServiceId.Aci(UUID.randomUUID());
|
||||
List<ServiceId> groupPlaintexts = Stream.concat(Stream.of(sender), recipients.stream().map(ServiceIdentifier::toLibsignal)).toList();
|
||||
List<UuidCiphertext> groupCiphertexts = groupPlaintexts.stream()
|
||||
.map(clientZkGroupCipher::encrypt)
|
||||
.toList();
|
||||
GroupSendDerivedKeyPair keyPair = GroupSendDerivedKeyPair.forExpiration(expiration, serverSecretParams);
|
||||
GroupSendEndorsementsResponse endorsementsResponse =
|
||||
GroupSendEndorsementsResponse.issue(groupCiphertexts, keyPair);
|
||||
ReceivedEndorsements endorsements =
|
||||
endorsementsResponse.receive(
|
||||
groupPlaintexts,
|
||||
sender,
|
||||
expiration.minus(Duration.ofDays(1)),
|
||||
groupSecretParams,
|
||||
serverPublicParams);
|
||||
GroupSendFullToken token = endorsements.combinedEndorsement().toFullToken(groupSecretParams, expiration);
|
||||
return Base64.getEncoder().encodeToString(token.serialize());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void testMultiRecipientRedisBombProtection(final boolean useExplicitIdentifier) throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user