Allow callers to get an expiring profile key credential
This commit is contained in:
		
							parent
							
								
									e38e5fa17d
								
							
						
					
					
						commit
						38e30c7513
					
				|  | @ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.controllers; | |||
| import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; | ||||
| 
 | ||||
| import com.codahale.metrics.annotation.Timed; | ||||
| import com.google.common.annotations.VisibleForTesting; | ||||
| import com.google.common.base.Preconditions; | ||||
| import io.dropwizard.auth.Auth; | ||||
| import io.micrometer.core.instrument.Counter; | ||||
|  | @ -19,8 +20,10 @@ import java.security.NoSuchAlgorithmException; | |||
| import java.security.SecureRandom; | ||||
| import java.time.Clock; | ||||
| import java.time.Duration; | ||||
| import java.time.Instant; | ||||
| import java.time.ZoneOffset; | ||||
| import java.time.ZonedDateTime; | ||||
| import java.time.temporal.ChronoUnit; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Base64; | ||||
|  | @ -66,6 +69,7 @@ import org.apache.commons.codec.binary.Hex; | |||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.signal.libsignal.zkgroup.InvalidInputException; | ||||
| import org.signal.libsignal.zkgroup.VerificationFailedException; | ||||
| import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; | ||||
| import org.signal.libsignal.zkgroup.profiles.PniCredentialResponse; | ||||
| import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; | ||||
| import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; | ||||
|  | @ -86,6 +90,7 @@ import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckRequest; | |||
| import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckResponse; | ||||
| import org.whispersystems.textsecuregcm.entities.CreateProfileRequest; | ||||
| import org.whispersystems.textsecuregcm.entities.CredentialProfileResponse; | ||||
| import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse; | ||||
| import org.whispersystems.textsecuregcm.entities.PniCredentialProfileResponse; | ||||
| import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes; | ||||
| import org.whispersystems.textsecuregcm.entities.ProfileKeyCredentialProfileResponse; | ||||
|  | @ -129,8 +134,12 @@ public class ProfileController { | |||
| 
 | ||||
|   private final Executor batchIdentityCheckExecutor; | ||||
| 
 | ||||
|   @VisibleForTesting | ||||
|   static final Duration EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION = Duration.ofDays(7); | ||||
| 
 | ||||
|   private static final String PROFILE_KEY_CREDENTIAL_TYPE = "profileKey"; | ||||
|   private static final String PNI_CREDENTIAL_TYPE = "pni"; | ||||
|   private static final String EXPIRING_PROFILE_KEY_CREDENTIAL_TYPE = "expiringProfileKey"; | ||||
| 
 | ||||
|   private static final Counter VERSION_NOT_FOUND_COUNTER = Metrics.counter(name(ProfileController.class, "versionNotFound")); | ||||
|   private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(ProfileController.class, "invalidAcceptLanguage"); | ||||
|  | @ -285,6 +294,15 @@ public class ProfileController { | |||
|             containerRequestContext); | ||||
|       } | ||||
| 
 | ||||
|       case EXPIRING_PROFILE_KEY_CREDENTIAL_TYPE -> { | ||||
|         return buildExpiringProfileKeyCredentialProfileResponse(targetAccount, | ||||
|             version, | ||||
|             credentialRequest, | ||||
|             isSelf, | ||||
|             Instant.now().plus(EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION).truncatedTo(ChronoUnit.DAYS), | ||||
|             containerRequestContext); | ||||
|       } | ||||
| 
 | ||||
|       default -> throw new BadRequestException(); | ||||
|     } | ||||
|   } | ||||
|  | @ -415,6 +433,23 @@ public class ProfileController { | |||
|         pniCredentialResponse); | ||||
|   } | ||||
| 
 | ||||
|   private ExpiringProfileKeyCredentialProfileResponse buildExpiringProfileKeyCredentialProfileResponse( | ||||
|       final Account account, | ||||
|       final String version, | ||||
|       final String encodedCredentialRequest, | ||||
|       final boolean isSelf, | ||||
|       final Instant expiration, | ||||
|       final ContainerRequestContext containerRequestContext) { | ||||
| 
 | ||||
|     final ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse = profilesManager.get(account.getUuid(), version) | ||||
|         .map(profile -> getExpiringProfileKeyCredentialResponse(encodedCredentialRequest, profile, account.getUuid(), expiration)) | ||||
|         .orElse(null); | ||||
| 
 | ||||
|     return new ExpiringProfileKeyCredentialProfileResponse( | ||||
|         buildVersionedProfileResponse(account, version, isSelf, containerRequestContext), | ||||
|         expiringProfileKeyCredentialResponse); | ||||
|   } | ||||
| 
 | ||||
|   private VersionedProfileResponse buildVersionedProfileResponse(final Account account, | ||||
|       final String version, | ||||
|       final boolean isSelf, | ||||
|  | @ -514,6 +549,22 @@ public class ProfileController { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse( | ||||
|       final String encodedCredentialRequest, | ||||
|       final VersionedProfile profile, | ||||
|       final UUID accountIdentifier, | ||||
|       final Instant expiration) { | ||||
| 
 | ||||
|     try { | ||||
|       final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment()); | ||||
|       final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedCredentialRequest)); | ||||
| 
 | ||||
|       return zkProfileOperations.issueExpiringProfileKeyCredential(request, accountIdentifier, commitment, expiration); | ||||
|     } catch (DecoderException | VerificationFailedException | InvalidInputException e) { | ||||
|       throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build()); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private ProfileAvatarUploadAttributes generateAvatarUploadForm(String objectName) { | ||||
|     ZonedDateTime        now            = ZonedDateTime.now(ZoneOffset.UTC); | ||||
|     Pair<String, String> policy         = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024); | ||||
|  |  | |||
|  | @ -0,0 +1,36 @@ | |||
| /* | ||||
|  * Copyright 2013-2022 Signal Messenger, LLC | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| package org.whispersystems.textsecuregcm.entities; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||||
| import com.fasterxml.jackson.databind.annotation.JsonSerialize; | ||||
| import javax.annotation.Nullable; | ||||
| import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; | ||||
| 
 | ||||
| public class ExpiringProfileKeyCredentialProfileResponse extends CredentialProfileResponse { | ||||
| 
 | ||||
|   @JsonProperty | ||||
|   @JsonSerialize(using = ExpiringProfileKeyCredentialResponseAdapter.Serializing.class) | ||||
|   @JsonDeserialize(using = ExpiringProfileKeyCredentialResponseAdapter.Deserializing.class) | ||||
|   @Nullable | ||||
|   private ExpiringProfileKeyCredentialResponse credential; | ||||
| 
 | ||||
|   public ExpiringProfileKeyCredentialProfileResponse() { | ||||
|   } | ||||
| 
 | ||||
|   public ExpiringProfileKeyCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse, | ||||
|       @Nullable final ExpiringProfileKeyCredentialResponse credential) { | ||||
| 
 | ||||
|     super(versionedProfileResponse); | ||||
|     this.credential = credential; | ||||
|   } | ||||
| 
 | ||||
|   @Nullable | ||||
|   public ExpiringProfileKeyCredentialResponse getCredential() { | ||||
|     return credential; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,41 @@ | |||
| /* | ||||
|  * Copyright 2013-2022 Signal Messenger, LLC | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| package org.whispersystems.textsecuregcm.entities; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.JsonGenerator; | ||||
| import com.fasterxml.jackson.core.JsonParser; | ||||
| import com.fasterxml.jackson.databind.DeserializationContext; | ||||
| import com.fasterxml.jackson.databind.JsonDeserializer; | ||||
| import com.fasterxml.jackson.databind.JsonSerializer; | ||||
| import com.fasterxml.jackson.databind.SerializerProvider; | ||||
| import java.io.IOException; | ||||
| import java.util.Base64; | ||||
| import org.signal.libsignal.zkgroup.InvalidInputException; | ||||
| import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; | ||||
| 
 | ||||
| public class ExpiringProfileKeyCredentialResponseAdapter { | ||||
| 
 | ||||
|   public static class Serializing extends JsonSerializer<ExpiringProfileKeyCredentialResponse> { | ||||
|     @Override | ||||
|     public void serialize(ExpiringProfileKeyCredentialResponse response, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) | ||||
|         throws IOException { | ||||
|       if (response == null) jsonGenerator.writeNull(); | ||||
|       else                  jsonGenerator.writeString(Base64.getEncoder().encodeToString(response.serialize())); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public static class Deserializing extends JsonDeserializer<ExpiringProfileKeyCredentialResponse> { | ||||
|     @Override | ||||
|     public ExpiringProfileKeyCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) | ||||
|         throws IOException { | ||||
|       try { | ||||
|         return new ExpiringProfileKeyCredentialResponse(Base64.getDecoder().decode(jsonParser.getValueAsString())); | ||||
|       } catch (InvalidInputException e) { | ||||
|         throw new IOException(e); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -1,9 +1,9 @@ | |||
| /* | ||||
|  * Copyright 2013-2021 Signal Messenger, LLC | ||||
|  * Copyright 2013-2022 Signal Messenger, LLC | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| package org.whispersystems.textsecuregcm.tests.controllers; | ||||
| package org.whispersystems.textsecuregcm.controllers; | ||||
| 
 | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.assertj.core.api.Assertions.assertThatNoException; | ||||
|  | @ -31,6 +31,7 @@ import java.security.SecureRandom; | |||
| import java.time.Clock; | ||||
| import java.time.Duration; | ||||
| import java.time.Instant; | ||||
| import java.time.temporal.ChronoUnit; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Base64; | ||||
|  | @ -64,6 +65,7 @@ import org.signal.libsignal.zkgroup.ServerPublicParams; | |||
| import org.signal.libsignal.zkgroup.ServerSecretParams; | ||||
| import org.signal.libsignal.zkgroup.VerificationFailedException; | ||||
| import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; | ||||
| import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; | ||||
| import org.signal.libsignal.zkgroup.profiles.PniCredentialResponse; | ||||
| import org.signal.libsignal.zkgroup.profiles.ProfileKey; | ||||
| import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; | ||||
|  | @ -86,6 +88,7 @@ import org.whispersystems.textsecuregcm.entities.BaseProfileResponse; | |||
| import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckRequest; | ||||
| import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckResponse; | ||||
| import org.whispersystems.textsecuregcm.entities.CreateProfileRequest; | ||||
| import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse; | ||||
| import org.whispersystems.textsecuregcm.entities.PniCredentialProfileResponse; | ||||
| import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes; | ||||
| import org.whispersystems.textsecuregcm.entities.ProfileKeyCredentialProfileResponse; | ||||
|  | @ -1131,6 +1134,76 @@ class ProfileControllerTest { | |||
|     verify(zkProfileOperations, never()).issuePniCredential(any(), any(), any(), any()); | ||||
|   } | ||||
| 
 | ||||
|   @ParameterizedTest | ||||
|   @MethodSource | ||||
|   void testGetProfileWithExpiringProfileKeyCredential(final MultivaluedMap<String, Object> authHeaders) | ||||
|       throws VerificationFailedException, InvalidInputException { | ||||
|     final String version = "version"; | ||||
|     final byte[] unidentifiedAccessKey = "test-uak".getBytes(StandardCharsets.UTF_8); | ||||
| 
 | ||||
|     final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); | ||||
|     final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); | ||||
| 
 | ||||
|     final ServerZkProfileOperations serverZkProfile = new ServerZkProfileOperations(serverSecretParams); | ||||
|     final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams); | ||||
| 
 | ||||
|     final byte[] profileKeyBytes = new byte[32]; | ||||
|     new SecureRandom().nextBytes(profileKeyBytes); | ||||
| 
 | ||||
|     final ProfileKey profileKey = new ProfileKey(profileKeyBytes); | ||||
|     final ProfileKeyCommitment profileKeyCommitment = profileKey.getCommitment(AuthHelper.VALID_UUID); | ||||
| 
 | ||||
|     final VersionedProfile versionedProfile = mock(VersionedProfile.class); | ||||
|     when(versionedProfile.getCommitment()).thenReturn(profileKeyCommitment.serialize()); | ||||
| 
 | ||||
|     final ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = | ||||
|         clientZkProfile.createProfileKeyCredentialRequestContext(AuthHelper.VALID_UUID, profileKey); | ||||
| 
 | ||||
|     final ProfileKeyCredentialRequest credentialRequest = profileKeyCredentialRequestContext.getRequest(); | ||||
| 
 | ||||
|     final Account account = mock(Account.class); | ||||
|     when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID); | ||||
|     when(account.getCurrentProfileVersion()).thenReturn(Optional.of(version)); | ||||
|     when(account.isEnabled()).thenReturn(true); | ||||
|     when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); | ||||
| 
 | ||||
|     final Instant expiration = Instant.now().plus(ProfileController.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION) | ||||
|         .truncatedTo(ChronoUnit.DAYS); | ||||
| 
 | ||||
|     final ExpiringProfileKeyCredentialResponse credentialResponse = | ||||
|         serverZkProfile.issueExpiringProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment, expiration); | ||||
| 
 | ||||
|     when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); | ||||
|     when(profilesManager.get(AuthHelper.VALID_UUID, version)).thenReturn(Optional.of(versionedProfile)); | ||||
|     when(zkProfileOperations.issueExpiringProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment, expiration)) | ||||
|         .thenReturn(credentialResponse); | ||||
| 
 | ||||
|     final ExpiringProfileKeyCredentialProfileResponse profile = resources.getJerseyTest() | ||||
|         .target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, version, Hex.encodeHexString(credentialRequest.serialize()))) | ||||
|         .queryParam("credentialType", "expiringProfileKey") | ||||
|         .request() | ||||
|         .headers(authHeaders) | ||||
|         .get(ExpiringProfileKeyCredentialProfileResponse.class); | ||||
| 
 | ||||
|     assertThat(profile.getVersionedProfileResponse().getBaseProfileResponse().getUuid()).isEqualTo(AuthHelper.VALID_UUID); | ||||
|     assertThat(profile.getCredential()).isEqualTo(credentialResponse); | ||||
| 
 | ||||
|     verify(zkProfileOperations).issueExpiringProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment, expiration); | ||||
|     verify(zkProfileOperations, never()).issuePniCredential(any(), any(), any(), any()); | ||||
| 
 | ||||
|     final ClientZkProfileOperations clientZkProfileCipher = new ClientZkProfileOperations(serverPublicParams); | ||||
|     assertThatNoException().isThrownBy(() -> | ||||
|         clientZkProfileCipher.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, profile.getCredential())); | ||||
|   } | ||||
| 
 | ||||
|   private static Stream<Arguments> testGetProfileWithExpiringProfileKeyCredential() { | ||||
|     return Stream.of( | ||||
|         Arguments.of(new MultivaluedHashMap<>(Map.of(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_KEY)))), | ||||
|         Arguments.of(new MultivaluedHashMap<>(Map.of("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)))), | ||||
|         Arguments.of(new MultivaluedHashMap<>(Map.of("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)))) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @Test | ||||
|   void testGetProfileWithPniCredentialVersionNotFound() throws VerificationFailedException { | ||||
|     final Account account = mock(Account.class); | ||||
		Loading…
	
		Reference in New Issue
	
	 Jon Chambers
						Jon Chambers