From 2d75f59d33067dd35c3ec82f22a33c35d8f79275 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Wed, 19 Aug 2020 11:24:23 -0400 Subject: [PATCH] Add support for UUID-only delivery certificates. (SERVER-132) --- .../auth/CertificateGenerator.java | 11 ++++- .../controllers/CertificateController.java | 10 ++++- .../auth/CertificateGeneratorTest.java | 41 +++++++++++++++++++ .../CertificateControllerTest.java | 41 +++++++++++++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/auth/CertificateGeneratorTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/CertificateGenerator.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/CertificateGenerator.java index 9e24671a9..fea266d2a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/CertificateGenerator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/CertificateGenerator.java @@ -28,14 +28,21 @@ public class CertificateGenerator { this.serverCertificate = ServerCertificate.parseFrom(serverCertificate); } - public byte[] createFor(Account account, Device device, boolean includeUuid) throws IOException, InvalidKeyException { + public byte[] createFor(Account account, Device device, boolean includeE164, boolean includeUuid) throws IOException, InvalidKeyException { SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder() - .setSender(account.getNumber()) .setSenderDevice(Math.toIntExact(device.getId())) .setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays)) .setIdentityKey(ByteString.copyFrom(Base64.decode(account.getIdentityKey()))) .setSigner(serverCertificate); + if (!includeE164 && !includeUuid) { + throw new IllegalArgumentException("Certificates must include one of a sender phone number or UUID"); + } + + if (includeE164) { + builder.setSender(account.getNumber()); + } + if (includeUuid) { builder.setSenderUuid(account.getUuid().toString()); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java index 5bdb37e7e..15d8dfb4d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java @@ -47,6 +47,7 @@ public class CertificateController { @Produces(MediaType.APPLICATION_JSON) @Path("/delivery") public DeliveryCertificate getDeliveryCertificate(@Auth Account account, + @QueryParam("includeE164") Optional includeE164, @QueryParam("includeUuid") Optional includeUuid) throws IOException, InvalidKeyException { @@ -56,7 +57,14 @@ public class CertificateController { throw new WebApplicationException(Response.Status.BAD_REQUEST); } - return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), includeUuid.orElse(false))); + final boolean effectiveIncludeE164 = includeE164.orElse(true); + final boolean effectiveIncludeUuid = includeUuid.orElse(false); + + if (!effectiveIncludeE164 && !effectiveIncludeUuid) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), effectiveIncludeE164, effectiveIncludeUuid)); } @Timed diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/auth/CertificateGeneratorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/auth/CertificateGeneratorTest.java new file mode 100644 index 000000000..06217885f --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/auth/CertificateGeneratorTest.java @@ -0,0 +1,41 @@ +package org.whispersystems.textsecuregcm.auth; + +import org.junit.Test; +import org.whispersystems.textsecuregcm.crypto.Curve; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Base64; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.util.UUID; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CertificateGeneratorTest { + + private static final String SIGNING_CERTIFICATE = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG"; + private static final String SIGNING_KEY = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4="; + private static final String IDENTITY_KEY = "BcxxDU9FGMda70E7+Uvm7pnQcEdXQ64aJCpPUeRSfcFo"; + + @Test + public void testCreateFor() throws IOException, InvalidKeyException { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final CertificateGenerator certificateGenerator = new CertificateGenerator(Base64.decode(SIGNING_CERTIFICATE), Curve.decodePrivatePoint(Base64.decode(SIGNING_KEY)), 1); + + when(account.getIdentityKey()).thenReturn(IDENTITY_KEY); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + when(account.getNumber()).thenReturn("+18005551234"); + when(device.getId()).thenReturn(4L); + + assertTrue(certificateGenerator.createFor(account, device, true, true).length > 0); + assertTrue(certificateGenerator.createFor(account, device, false, true).length > 0); + assertTrue(certificateGenerator.createFor(account, device, true, false).length > 0); + + assertThrows(IllegalArgumentException.class, () -> certificateGenerator.createFor(account, device, false, false)); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java index 4fcb79cd5..3c611e95e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java @@ -1,6 +1,7 @@ package org.whispersystems.textsecuregcm.tests.controllers; import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.ClassRule; import org.junit.Test; @@ -36,6 +37,7 @@ import static junit.framework.TestCase.assertTrue; import static org.assertj.core.api.Java6Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; public class CertificateControllerTest { @@ -117,6 +119,45 @@ public class CertificateControllerTest { assertTrue(Arrays.equals(certificate.getIdentityKey().toByteArray(), Base64.decode(AuthHelper.VALID_IDENTITY))); } + @Test + public void testValidCertificateWithUuidNoE164() throws Exception { + DeliveryCertificate certificateObject = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .queryParam("includeUuid", "true") + .queryParam("includeE164", "false") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(DeliveryCertificate.class); + + + SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate()); + SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom(certificateHolder.getCertificate()); + + ServerCertificate serverCertificateHolder = certificate.getSigner(); + ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom(serverCertificateHolder.getCertificate()); + + assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0), certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray())); + assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.decode(caPublicKey), 0), serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray())); + + assertTrue(StringUtils.isBlank(certificate.getSender())); + assertEquals(certificate.getSenderDevice(), 1L); + assertEquals(certificate.getSenderUuid(), AuthHelper.VALID_UUID.toString()); + assertTrue(Arrays.equals(certificate.getIdentityKey().toByteArray(), Base64.decode(AuthHelper.VALID_IDENTITY))); + } + + @Test + public void testValidCertificateWithNoUuidNoE164() throws Exception { + Response response = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .queryParam("includeUuid", "false") + .queryParam("includeE164", "false") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(); + + assertEquals(response.getStatus(), 400); + } + @Test public void testBadAuthentication() throws Exception { Response response = resources.getJerseyTest()