diff --git a/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java b/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java index 8421dd943..2a313332c 100644 --- a/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java +++ b/java/client/src/test/java/org/signal/libsignal/zkgroup/integrationtests/ZkGroupTest.java @@ -6,7 +6,6 @@ package org.signal.libsignal.zkgroup.integrationtests; import java.io.UnsupportedEncodingException; -import java.util.concurrent.TimeUnit; import org.junit.Test; import org.signal.libsignal.protocol.util.Hex; import org.signal.libsignal.zkgroup.InvalidInputException; @@ -19,6 +18,8 @@ import org.signal.libsignal.zkgroup.InvalidRedemptionTimeException; import org.signal.libsignal.zkgroup.auth.AuthCredential; import org.signal.libsignal.zkgroup.auth.AuthCredentialPresentation; import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPni; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations; import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations; import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher; @@ -51,6 +52,7 @@ import java.util.UUID; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public final class ZkGroupTest extends SecureRandomTest { @@ -127,20 +129,28 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss byte[] temp = new byte[32]; // wrong length new ServerSecretParams(temp); throw new AssertionError("Failed to catch invalid ServerSecretParams deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new ServerSecretParams(makeBadArray(serverSecretParams.serialize())); throw new AssertionError("Failed to catch invalid ServerSecretParams deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { byte[] temp = new byte[32]; // wrong length new ServerPublicParams(temp); throw new AssertionError("Failed to catch invalid ServerPublicParams deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new ServerPublicParams(makeBadArray(serverPublicParams.serialize())); throw new AssertionError("Failed to catch invalid ServerPublicParams deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } } ServerZkAuthOperations serverZkAuth = new ServerZkAuthOperations(serverSecretParams); @@ -161,25 +171,35 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss byte[] temp = new byte[10]; // wrong length new GroupMasterKey(temp); throw new AssertionError("Failed to catch invalid GroupMasterKey deserialize"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { byte[] temp = new byte[10]; // wrong length new GroupSecretParams(temp); throw new AssertionError("Failed to catch invalid GroupSecretParams deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new GroupSecretParams(makeBadArray(groupSecretParams.serialize())); throw new AssertionError("Failed to catch invalid GroupSecretParams deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { byte[] temp = new byte[10]; // wrong length new GroupPublicParams(temp); throw new AssertionError("Failed to catch invalid GroupPublicParams deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new GroupPublicParams(makeBadArray(groupPublicParams.serialize())); throw new AssertionError("Failed to catch invalid GroupPublicParams deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } } // SERVER @@ -199,11 +219,15 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss byte[] temp = new byte[10]; new AuthCredentialResponse(temp); throw new AssertionError("Failed to catch invalid AuthCredentialResponse deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new AuthCredentialResponse(makeBadArray(authCredentialResponse.serialize())); throw new AssertionError("Failed to catch invalid AuthCredentialResponse deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } } // CLIENT - verify test @@ -212,7 +236,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { clientZkAuthCipher.receiveAuthCredential(badUuid, redemptionTime, authCredentialResponse); throw new AssertionError("Failed to catch invalid AuthCredential 1"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } byte[] temp = authCredentialResponse.serialize(); temp[1]++; @@ -220,7 +246,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { clientZkAuthCipher.receiveAuthCredential(uuid, redemptionTime, badResponse); throw new AssertionError("Failed to catch invalid AuthCredential 2"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } } // Create and decrypt user entry @@ -235,12 +263,16 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss byte[] temp = new byte[10]; new UuidCiphertext(temp); throw new AssertionError("Failed to catch invalid UuidCiphertext deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new UuidCiphertext(makeBadArray(uuidCiphertext.serialize())); throw new AssertionError("Failed to catch invalid UuidCiphertext deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } } // CLIENT - verify test @@ -250,7 +282,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { clientZkGroupCipher.decryptUuid(new UuidCiphertext(temp)); throw new AssertionError("Failed to catch invalid UuidCiphertext decrypt"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } } // CLIENT - Create presentation @@ -266,32 +300,38 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { new AuthCredentialPresentation(temp); throw new AssertionError("Failed to catch invalid AuthCredentialPresentation deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new AuthCredentialPresentation(makeBadArray(presentation.serialize())); throw new AssertionError("Failed to catch invalid AuthCredentialPresentation deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } } // SERVER - Verify presentation, using times at the edge of the acceptable window + Instant redemptionInstant = Instant.ofEpochSecond(86400L * redemptionTime); UuidCiphertext uuidCiphertextRecv = presentation.getUuidCiphertext(); assertArrayEquals(uuidCiphertext.serialize(), uuidCiphertextRecv.serialize()); - assertEquals(presentation.getRedemptionTime(), redemptionTime); + assertNull(presentation.getPniCiphertext()); + assertEquals(presentation.getRedemptionTime(), redemptionInstant); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(123455L, TimeUnit.DAYS)); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(123458L, TimeUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.minus(1, ChronoUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.plus(2, ChronoUnit.DAYS)); try { - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(123455L, TimeUnit.DAYS) - 1L); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.minus(1, ChronoUnit.DAYS).minus(1, ChronoUnit.SECONDS)); throw new AssertionError("verifyAuthCredentialPresentation should fail #1!"); - } catch(InvalidRedemptionTimeException e) { + } catch (InvalidRedemptionTimeException e) { // good } try { - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(123458L, TimeUnit.DAYS) + 1L); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.plus(2, ChronoUnit.DAYS).plus(1, ChronoUnit.SECONDS)); throw new AssertionError("verifyAuthCredentialPresentation should fail #2!"); - } catch(InvalidRedemptionTimeException e) { + } catch (InvalidRedemptionTimeException e) { // good } @@ -299,32 +339,38 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss byte[] temp = presentation.serialize(); temp[3]++; // We need a bad presentation that passes deserialization, this seems to work AuthCredentialPresentation presentationTemp = new AuthCredentialPresentation(temp); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, TimeUnit.MILLISECONDS.convert(123455L, TimeUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, redemptionInstant); throw new AssertionError("verifyAuthCredentialPresentation should fail #3!"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); temp[0] = 0; // This interprets a V2 as V1, so should fail AuthCredentialPresentation presentationTemp = new AuthCredentialPresentation(temp); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, TimeUnit.MILLISECONDS.convert(123455L, TimeUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, redemptionInstant); throw new AssertionError("verifyAuthCredentialPresentation should fail #4"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); - temp[0] = 2; // This interprets a V2 as a non-existent version, so should fail + temp[0] = 40; // This interprets a V2 as a non-existent version, so should fail AuthCredentialPresentation presentationTemp = new AuthCredentialPresentation(temp); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, TimeUnit.MILLISECONDS.convert(123455L, TimeUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, redemptionInstant); throw new AssertionError("verifyAuthCredentialPresentation should fail #5"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } { // Test that V1 presentation verifies successfully AuthCredentialPresentation presentationTemp = new AuthCredentialPresentation(authPresentationResultV1); assertEquals(presentationTemp.serialize()[0], 0); // Check V1 assertEquals(presentationTemp.getVersion(), AuthCredentialPresentation.Version.V1); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, TimeUnit.MILLISECONDS.convert(123455L, TimeUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, redemptionInstant); assertArrayEquals(presentationTemp.serialize(), authPresentationResultV1); } @@ -338,7 +384,7 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss // redemption date to compare against test vectors, it uses the current time UUID uuid = TEST_UUID; - int redemptionTime = (int)TimeUnit.DAYS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + int redemptionTime = (int)(Instant.now().truncatedTo(ChronoUnit.DAYS).getEpochSecond() / 86400); // Generate keys (client's are per-group, server's are not) // --- @@ -375,33 +421,190 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss AuthCredentialPresentation presentation = clientZkAuthCipher.createAuthCredentialPresentation(createSecureRandom(TEST_ARRAY_32_5), groupSecretParams, authCredential); // Verify presentation, using times at the edge of the acceptable window + Instant redemptionInstant = Instant.ofEpochSecond(86400L * redemptionTime); UuidCiphertext uuidCiphertextRecv = presentation.getUuidCiphertext(); assertArrayEquals(uuidCiphertext.serialize(), uuidCiphertextRecv.serialize()); - assertEquals(presentation.getRedemptionTime(), redemptionTime); + assertEquals(presentation.getRedemptionTime(), redemptionInstant); // By default the library uses the current time serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(redemptionTime - 1L, TimeUnit.DAYS)); - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(redemptionTime + 2L, TimeUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.minus(1, ChronoUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.plus(2, ChronoUnit.DAYS)); try { - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(redemptionTime - 1L, TimeUnit.DAYS) - 1L); - throw new AssertionError("verifyAuthCredentialPresentation (current time) should fail #1!"); - } catch(InvalidRedemptionTimeException e) { + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.minus(1, ChronoUnit.DAYS).minus(1, ChronoUnit.SECONDS)); + throw new AssertionError("verifyAuthCredentialPresentation should fail #1!"); + } catch (InvalidRedemptionTimeException e) { // good } try { - serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, TimeUnit.MILLISECONDS.convert(redemptionTime + 2L, TimeUnit.DAYS) + 1L); - throw new AssertionError("verifyAuthCredentialPresentation (current time) should fail #2!"); - } catch(InvalidRedemptionTimeException e) { + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionInstant.plus(2, ChronoUnit.DAYS).plus(1, ChronoUnit.SECONDS)); + throw new AssertionError("verifyAuthCredentialPresentation should fail #2!"); + } catch (InvalidRedemptionTimeException e) { // good } + } + + @Test + public void testAuthWithPniIntegration() throws VerificationFailedException, InvalidInputException, InvalidRedemptionTimeException { + + UUID aci = TEST_UUID; + UUID pni = TEST_UUID_1; + Instant redemptionTime = Instant.now().truncatedTo(ChronoUnit.DAYS); + + // Generate keys (client's are per-group, server's are not) + // --- + + // SERVER + ServerSecretParams serverSecretParams = ServerSecretParams.generate(createSecureRandom(TEST_ARRAY_32)); + ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); + + ServerZkAuthOperations serverZkAuth = new ServerZkAuthOperations(serverSecretParams); + + // CLIENT + GroupMasterKey masterKey = new GroupMasterKey(TEST_ARRAY_32_1); + GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey); + + assertArrayEquals(groupSecretParams.getMasterKey().serialize(), masterKey.serialize()); + + GroupPublicParams groupPublicParams = groupSecretParams.getPublicParams(); + + // SERVER + // Issue credential + AuthCredentialWithPniResponse authCredentialResponse = serverZkAuth.issueAuthCredentialWithPni(createSecureRandom(TEST_ARRAY_32_2), aci, pni, redemptionTime); + + // CLIENT + // Receive credential + ClientZkAuthOperations clientZkAuthCipher = new ClientZkAuthOperations(serverPublicParams); + ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher (groupSecretParams ); + AuthCredentialWithPni authCredential = clientZkAuthCipher.receiveAuthCredentialWithPni(aci, pni, redemptionTime.getEpochSecond(), authCredentialResponse); + + // CLIENT - deserialize test + { + new AuthCredentialWithPniResponse(authCredentialResponse.serialize()); + try { + byte[] temp = new byte[10]; + new AuthCredentialWithPniResponse(temp); + throw new AssertionError("Failed to catch invalid AuthCredentialWithPniResponse deserialize 1"); + } catch (InvalidInputException e) { + // expected + } + try { + new AuthCredentialWithPniResponse(makeBadArray(authCredentialResponse.serialize())); + throw new AssertionError("Failed to catch invalid AuthCredentialWithPniResponse deserialize 2"); + } catch (InvalidInputException e) { + // expected + } + } + + // CLIENT - verify test + { + try { + // Switch ACI and PNI + clientZkAuthCipher.receiveAuthCredentialWithPni(pni, aci, redemptionTime.getEpochSecond(), authCredentialResponse); + throw new AssertionError("Failed to catch invalid AuthCredentialWithPni 1"); + } catch (VerificationFailedException e) { + // expected + } + + byte[] temp = authCredentialResponse.serialize(); + temp[1]++; + AuthCredentialWithPniResponse badResponse = new AuthCredentialWithPniResponse(temp); + try { + clientZkAuthCipher.receiveAuthCredentialWithPni(aci, pni, redemptionTime.getEpochSecond(), badResponse); + throw new AssertionError("Failed to catch invalid AuthCredentialWithPni 2"); + } catch (VerificationFailedException e) { + // expected + } + } + + // Create and decrypt user entry + UuidCiphertext aciCiphertext = clientZkGroupCipher.encryptUuid(aci); + UUID aciPlaintext = clientZkGroupCipher.decryptUuid(aciCiphertext); + assertEquals(aci, aciPlaintext); + UuidCiphertext pniCiphertext = clientZkGroupCipher.encryptUuid(pni); + UUID pniPlaintext = clientZkGroupCipher.decryptUuid(pniCiphertext); + assertEquals(pni, pniPlaintext); + + // CLIENT - Create presentation + AuthCredentialPresentation presentation = clientZkAuthCipher.createAuthCredentialPresentation(createSecureRandom(TEST_ARRAY_32_5), groupSecretParams, authCredential); + assertEquals(presentation.serialize()[0], 2); // Check V3 + assertEquals(presentation.getVersion(), AuthCredentialPresentation.Version.V3); + + // CLIENT - deserialize test + { + new AuthCredentialPresentation(presentation.serialize()); + byte[] temp = new byte[10]; + try { + new AuthCredentialPresentation(temp); + throw new AssertionError("Failed to catch invalid AuthCredentialPresentation deserialize 1"); + } catch (InvalidInputException e) { + // expected + } + try { + new AuthCredentialPresentation(makeBadArray(presentation.serialize())); + throw new AssertionError("Failed to catch invalid AuthCredentialPresentation deserialize 2"); + } catch (InvalidInputException e) { + // expected + } + } + + // SERVER - Verify presentation, using times at the edge of the acceptable window + assertArrayEquals(aciCiphertext.serialize(), presentation.getUuidCiphertext().serialize()); + assertArrayEquals(pniCiphertext.serialize(), presentation.getPniCiphertext().serialize()); + assertEquals(presentation.getRedemptionTime(), redemptionTime); + + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionTime.minus(1, ChronoUnit.DAYS)); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionTime.plus(2, ChronoUnit.DAYS)); + + try { + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionTime.minus(1, ChronoUnit.DAYS).minus(1, ChronoUnit.SECONDS)); + throw new AssertionError("verifyAuthCredentialPresentation should fail #1!"); + } catch (InvalidRedemptionTimeException e) { + // good + } + + try { + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentation, redemptionTime.plus(2, ChronoUnit.DAYS).plus(1, ChronoUnit.SECONDS)); + throw new AssertionError("verifyAuthCredentialPresentation should fail #2!"); + } catch (InvalidRedemptionTimeException e) { + // good + } + + try { + byte[] temp = presentation.serialize(); + temp[3] += 5; // We need a bad presentation that passes deserialization, this seems to work + AuthCredentialPresentation presentationTemp = new AuthCredentialPresentation(temp); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, redemptionTime); + throw new AssertionError("verifyAuthCredentialPresentation should fail #3!"); + } catch (VerificationFailedException e) { + // expected + } + + try { + byte[] temp = presentation.serialize(); + temp[0] = 0; // This interprets a V3 as V1, so should fail + AuthCredentialPresentation presentationTemp = new AuthCredentialPresentation(temp); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, redemptionTime); + throw new AssertionError("verifyAuthCredentialPresentation should fail #4"); + } catch (InvalidInputException e) { + // expected + } + + try { + byte[] temp = presentation.serialize(); + temp[0] = 40; // This interprets a V3 as a non-existent version, so should fail + AuthCredentialPresentation presentationTemp = new AuthCredentialPresentation(temp); + serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams, presentationTemp, redemptionTime); + throw new AssertionError("verifyAuthCredentialPresentation should fail #5"); + } catch (InvalidInputException e) { + // expected + } } - @Test public void testProfileKeyIntegration() throws VerificationFailedException, InvalidInputException, UnsupportedEncodingException { @@ -438,11 +641,15 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss byte[] temp = new byte[10]; new ProfileKeyCredentialRequestContext(temp); throw new AssertionError("Failed to catch invalid ProfileKeyCredentialResponse deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new ProfileKeyCredentialRequestContext(makeBadArray(context.serialize())); throw new AssertionError("Failed to catch invalid ProfileKeyCredentialRequestContext deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } } // SERVER @@ -456,7 +663,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { serverZkProfile.issueProfileKeyCredential(createSecureRandom(TEST_ARRAY_32_4), badRequest, uuid, profileKeyCommitment); throw new AssertionError("Failed to catch invalid ProfileKeyCredentialRequest"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } } // CLIENT @@ -480,12 +689,16 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss byte[] temp = new byte[10]; new ProfileKeyCiphertext(temp); throw new AssertionError("Failed to catch invalid ProfileKeyCiphertext deserialize 1"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } try { new ProfileKeyCiphertext(makeBadArray(profileKeyCiphertext.serialize())); throw new AssertionError("Failed to catch invalid ProfileKeyCiphertext deserialize 2"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } } // CLIENT - verify test @@ -495,7 +708,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { clientZkGroupCipher.decryptProfileKey(new ProfileKeyCiphertext(temp), uuid); throw new AssertionError("Failed to catch invalid ProfileKeyCiphertext decrypt"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } } ProfileKeyCredentialPresentation presentation = clientZkProfileCipher.createProfileKeyCredentialPresentation(createSecureRandom(TEST_ARRAY_32_5), groupSecretParams, profileKeyCredential); @@ -519,7 +734,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss ProfileKeyCredentialPresentation presentationTemp = new ProfileKeyCredentialPresentation(temp); serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyProfileKeyCredentialPresentation should fail 1"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); @@ -527,7 +744,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss ProfileKeyCredentialPresentation presentationTemp = new ProfileKeyCredentialPresentation(temp); serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyProfileKeyCredentialPresentation should fail 2"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); @@ -535,7 +754,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss ProfileKeyCredentialPresentation presentationTemp = new ProfileKeyCredentialPresentation(temp); serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyProfileKeyCredentialPresentation should fail 3"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } { // Test that V1 presentation verifies successfully @@ -589,7 +810,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { serverZkProfile.issueExpiringProfileKeyCredential(createSecureRandom(TEST_ARRAY_32_4), badRequest, uuid, profileKeyCommitment, expiration); throw new AssertionError("Failed to catch invalid ProfileKeyCredentialRequest"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } } // CLIENT @@ -621,12 +844,16 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss try { serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentation, expiration); throw new AssertionError("credential expired 1"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentation, expiration.plusSeconds(5)); throw new AssertionError("credential expired 2"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); @@ -634,7 +861,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss ProfileKeyCredentialPresentation presentationTemp = new ProfileKeyCredentialPresentation(temp); serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyProfileKeyCredentialPresentation should fail 1"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); @@ -642,7 +871,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss ProfileKeyCredentialPresentation presentationTemp = new ProfileKeyCredentialPresentation(temp); serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyProfileKeyCredentialPresentation should fail 2"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); @@ -650,7 +881,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss ProfileKeyCredentialPresentation presentationTemp = new ProfileKeyCredentialPresentation(temp); serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyProfileKeyCredentialPresentation should fail 3"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } // Test that we can encode as a V1 presentation, even though it won't verify. ProfileKeyCredentialPresentation v1Presentation = new ProfileKeyCredentialPresentation(presentation.getStructurallyValidV1PresentationBytes()); @@ -658,7 +891,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss assertEquals(v1Presentation.getProfileKeyCiphertext(), presentation.getProfileKeyCiphertext()); try { serverZkProfile.verifyProfileKeyCredentialPresentation(groupPublicParams, v1Presentation); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } } @Test @@ -718,7 +953,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss PniCredentialPresentation presentationTemp = new PniCredentialPresentation(temp); serverZkProfile.verifyPniCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyPniCredentialPresentation should fail 1"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); @@ -726,7 +963,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss PniCredentialPresentation presentationTemp = new PniCredentialPresentation(temp); serverZkProfile.verifyPniCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyPniCredentialPresentation should fail 2"); - } catch (VerificationFailedException e) {} + } catch (VerificationFailedException e) { + // expected + } try { byte[] temp = presentation.serialize(); @@ -734,7 +973,9 @@ private static final byte[] pniPresentationResultV2 = Hex.fromStringCondensedAss PniCredentialPresentation presentationTemp = new PniCredentialPresentation(temp); serverZkProfile.verifyPniCredentialPresentation(groupPublicParams, presentationTemp); throw new AssertionError("verifyPniCredentialPresentation should fail 3"); - } catch (InvalidInputException e) {} + } catch (InvalidInputException e) { + // expected + } { // Test that V1 presentation verifies successfully diff --git a/java/shared/java/org/signal/libsignal/internal/Native.java b/java/shared/java/org/signal/libsignal/internal/Native.java index e8cf6e9e4..0a7702e74 100644 --- a/java/shared/java/org/signal/libsignal/internal/Native.java +++ b/java/shared/java/org/signal/libsignal/internal/Native.java @@ -105,11 +105,16 @@ public final class Native { public static native long Aes256GcmSiv_New(byte[] key); public static native void AuthCredentialPresentation_CheckValidContents(byte[] presentationBytes); - public static native int AuthCredentialPresentation_GetRedemptionTime(byte[] presentationBytes); + public static native byte[] AuthCredentialPresentation_GetPniCiphertext(byte[] presentationBytes); + public static native long AuthCredentialPresentation_GetRedemptionTime(byte[] presentationBytes); public static native byte[] AuthCredentialPresentation_GetUuidCiphertext(byte[] presentationBytes); public static native void AuthCredentialResponse_CheckValidContents(byte[] buffer); + public static native void AuthCredentialWithPniResponse_CheckValidContents(byte[] buffer); + + public static native void AuthCredentialWithPni_CheckValidContents(byte[] buffer); + public static native void AuthCredential_CheckValidContents(byte[] buffer); public static native void Cds2ClientState_CompleteHandshake(long cli, byte[] handshakeReceived); @@ -358,6 +363,7 @@ public final class Native { public static native void ServerPublicParams_CheckValidContents(byte[] buffer); public static native byte[] ServerPublicParams_CreateAuthCredentialPresentationDeterministic(byte[] serverPublicParams, byte[] randomness, byte[] groupSecretParams, byte[] authCredential); + public static native byte[] ServerPublicParams_CreateAuthCredentialWithPniPresentationDeterministic(byte[] serverPublicParams, byte[] randomness, byte[] groupSecretParams, byte[] authCredential); public static native byte[] ServerPublicParams_CreateExpiringProfileKeyCredentialPresentationDeterministic(byte[] serverPublicParams, byte[] randomness, byte[] groupSecretParams, byte[] profileKeyCredential); public static native byte[] ServerPublicParams_CreatePniCredentialPresentationDeterministic(byte[] serverPublicParams, byte[] randomness, byte[] groupSecretParams, byte[] pniCredential); public static native byte[] ServerPublicParams_CreatePniCredentialRequestContextDeterministic(byte[] serverPublicParams, byte[] randomness, UUID aci, UUID pni, byte[] profileKey); @@ -366,6 +372,7 @@ public final class Native { public static native byte[] ServerPublicParams_CreateReceiptCredentialPresentationDeterministic(byte[] serverPublicParams, byte[] randomness, byte[] receiptCredential); public static native byte[] ServerPublicParams_CreateReceiptCredentialRequestContextDeterministic(byte[] serverPublicParams, byte[] randomness, byte[] receiptSerial); public static native byte[] ServerPublicParams_ReceiveAuthCredential(byte[] params, UUID uuid, int redemptionTime, byte[] response); + public static native byte[] ServerPublicParams_ReceiveAuthCredentialWithPni(byte[] params, UUID aci, UUID pni, long redemptionTime, byte[] response); public static native byte[] ServerPublicParams_ReceiveExpiringProfileKeyCredential(byte[] serverPublicParams, byte[] requestContext, byte[] response, long currentTimeInSeconds); public static native byte[] ServerPublicParams_ReceivePniCredential(byte[] serverPublicParams, byte[] requestContext, byte[] response); public static native byte[] ServerPublicParams_ReceiveProfileKeyCredential(byte[] serverPublicParams, byte[] requestContext, byte[] response); @@ -376,6 +383,7 @@ public final class Native { public static native byte[] ServerSecretParams_GenerateDeterministic(byte[] randomness); public static native byte[] ServerSecretParams_GetPublicParams(byte[] params); public static native byte[] ServerSecretParams_IssueAuthCredentialDeterministic(byte[] serverSecretParams, byte[] randomness, UUID uuid, int redemptionTime); + public static native byte[] ServerSecretParams_IssueAuthCredentialWithPniDeterministic(byte[] serverSecretParams, byte[] randomness, UUID aci, UUID pni, long redemptionTime); public static native byte[] ServerSecretParams_IssueExpiringProfileKeyCredentialDeterministic(byte[] serverSecretParams, byte[] randomness, byte[] request, UUID uuid, byte[] commitment, long expirationInSeconds); public static native byte[] ServerSecretParams_IssuePniCredentialDeterministic(byte[] serverSecretParams, byte[] randomness, byte[] request, UUID aci, UUID pni, byte[] commitment); public static native byte[] ServerSecretParams_IssueProfileKeyCredentialDeterministic(byte[] serverSecretParams, byte[] randomness, byte[] request, UUID uuid, byte[] commitment); diff --git a/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialPresentation.java b/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialPresentation.java index 669dd04d7..bfc0fbc0d 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialPresentation.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialPresentation.java @@ -1,11 +1,12 @@ // -// Copyright 2020-2021 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // package org.signal.libsignal.zkgroup.auth; -import java.nio.ByteBuffer; +import java.time.Instant; + import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; import org.signal.libsignal.zkgroup.internal.ByteArray; @@ -13,7 +14,7 @@ import org.signal.libsignal.internal.Native; public final class AuthCredentialPresentation extends ByteArray { - public enum Version {V1, V2, UNKNOWN}; + public enum Version {V1, V2, V3, UNKNOWN}; public AuthCredentialPresentation(byte[] contents) throws InvalidInputException { super(contents); @@ -30,18 +31,33 @@ public final class AuthCredentialPresentation extends ByteArray { } } - public int getRedemptionTime() { - return Native.AuthCredentialPresentation_GetRedemptionTime(contents); + /** + * Returns the PNI ciphertext for this credential. May be {@code null}. + */ + public UuidCiphertext getPniCiphertext() { + byte[] newContents = Native.AuthCredentialPresentation_GetPniCiphertext(contents); + if (newContents == null) { + return null; + } + + try { + return new UuidCiphertext(newContents); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + } + + public Instant getRedemptionTime() { + return Instant.ofEpochSecond(Native.AuthCredentialPresentation_GetRedemptionTime(contents)); } public Version getVersion() { - if (this.contents[0] == 0) { - return Version.V1; - } else if (this.contents[0] == 1) { - return Version.V2; - } else { - return Version.UNKNOWN; - } + switch (this.contents[0]) { + case 0: return Version.V1; + case 1: return Version.V2; + case 2: return Version.V3; + default: return Version.UNKNOWN; + } } } diff --git a/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialWithPni.java b/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialWithPni.java new file mode 100644 index 000000000..6b5a50598 --- /dev/null +++ b/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialWithPni.java @@ -0,0 +1,17 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +package org.signal.libsignal.zkgroup.auth; + +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.internal.ByteArray; +import org.signal.libsignal.internal.Native; + +public final class AuthCredentialWithPni extends ByteArray { + public AuthCredentialWithPni(byte[] contents) throws InvalidInputException { + super(contents); + Native.AuthCredentialWithPni_CheckValidContents(contents); + } +} diff --git a/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialWithPniResponse.java b/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialWithPniResponse.java new file mode 100644 index 000000000..c5b1f59c3 --- /dev/null +++ b/java/shared/java/org/signal/libsignal/zkgroup/auth/AuthCredentialWithPniResponse.java @@ -0,0 +1,17 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +package org.signal.libsignal.zkgroup.auth; + +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.internal.ByteArray; +import org.signal.libsignal.internal.Native; + +public final class AuthCredentialWithPniResponse extends ByteArray { + public AuthCredentialWithPniResponse(byte[] contents) throws InvalidInputException { + super(contents); + Native.AuthCredentialWithPniResponse_CheckValidContents(contents); + } +} diff --git a/java/shared/java/org/signal/libsignal/zkgroup/auth/ClientZkAuthOperations.java b/java/shared/java/org/signal/libsignal/zkgroup/auth/ClientZkAuthOperations.java index 8c6f51ab9..3669d6e42 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/auth/ClientZkAuthOperations.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/auth/ClientZkAuthOperations.java @@ -1,5 +1,5 @@ // -// Copyright 2020-2021 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // @@ -33,6 +33,21 @@ public class ClientZkAuthOperations { } } + /** + * Produces the AuthCredentialWithPni from a server-generated AuthCredentialWithPniResponse. + * + * @param redemptionTime This is provided by the server as an integer, and should be passed through directly. + */ + public AuthCredentialWithPni receiveAuthCredentialWithPni(UUID aci, UUID pni, long redemptionTime, AuthCredentialWithPniResponse authCredentialResponse) throws VerificationFailedException { + byte[] newContents = Native.ServerPublicParams_ReceiveAuthCredentialWithPni(serverPublicParams.getInternalContentsForJNI(), aci, pni, redemptionTime, authCredentialResponse.getInternalContentsForJNI()); + + try { + return new AuthCredentialWithPni(newContents); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + } + public AuthCredentialPresentation createAuthCredentialPresentation(GroupSecretParams groupSecretParams, AuthCredential authCredential) { return createAuthCredentialPresentation(new SecureRandom(), groupSecretParams, authCredential); } @@ -50,4 +65,20 @@ public class ClientZkAuthOperations { } } + public AuthCredentialPresentation createAuthCredentialPresentation(GroupSecretParams groupSecretParams, AuthCredentialWithPni authCredential) { + return createAuthCredentialPresentation(new SecureRandom(), groupSecretParams, authCredential); + } + + public AuthCredentialPresentation createAuthCredentialPresentation(SecureRandom secureRandom, GroupSecretParams groupSecretParams, AuthCredentialWithPni authCredential) { + byte[] random = new byte[RANDOM_LENGTH]; + secureRandom.nextBytes(random); + + byte[] newContents = Native.ServerPublicParams_CreateAuthCredentialWithPniPresentationDeterministic(serverPublicParams.getInternalContentsForJNI(), random, groupSecretParams.getInternalContentsForJNI(), authCredential.getInternalContentsForJNI()); + + try { + return new AuthCredentialPresentation(newContents); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + } } diff --git a/java/shared/java/org/signal/libsignal/zkgroup/auth/ServerZkAuthOperations.java b/java/shared/java/org/signal/libsignal/zkgroup/auth/ServerZkAuthOperations.java index f9c05a10f..efdbfbc13 100644 --- a/java/shared/java/org/signal/libsignal/zkgroup/auth/ServerZkAuthOperations.java +++ b/java/shared/java/org/signal/libsignal/zkgroup/auth/ServerZkAuthOperations.java @@ -1,13 +1,14 @@ // -// Copyright 2020-2021 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // package org.signal.libsignal.zkgroup.auth; import java.security.SecureRandom; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.UUID; -import java.util.concurrent.TimeUnit; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.ServerSecretParams; import org.signal.libsignal.zkgroup.VerificationFailedException; @@ -43,15 +44,34 @@ public class ServerZkAuthOperations { } } + public AuthCredentialWithPniResponse issueAuthCredentialWithPni(UUID aci, UUID pni, Instant redemptionTime) { + return issueAuthCredentialWithPni(new SecureRandom(), aci, pni, redemptionTime); + } + + public AuthCredentialWithPniResponse issueAuthCredentialWithPni(SecureRandom secureRandom, UUID aci, UUID pni, Instant redemptionTime) { + byte[] random = new byte[RANDOM_LENGTH]; + + secureRandom.nextBytes(random); + + byte[] newContents = Native.ServerSecretParams_IssueAuthCredentialWithPniDeterministic(serverSecretParams.getInternalContentsForJNI(), random, aci, pni, redemptionTime.getEpochSecond()); + + try { + return new AuthCredentialWithPniResponse(newContents); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + } + public void verifyAuthCredentialPresentation(GroupPublicParams groupPublicParams, AuthCredentialPresentation authCredentialPresentation) throws VerificationFailedException, InvalidRedemptionTimeException { - verifyAuthCredentialPresentation(groupPublicParams, authCredentialPresentation, System.currentTimeMillis()); + verifyAuthCredentialPresentation(groupPublicParams, authCredentialPresentation, Instant.now()); } - public void verifyAuthCredentialPresentation(GroupPublicParams groupPublicParams, AuthCredentialPresentation authCredentialPresentation, long currentTimeMillis) throws VerificationFailedException, InvalidRedemptionTimeException { - long acceptableStartTime = TimeUnit.MILLISECONDS.convert(authCredentialPresentation.getRedemptionTime()-1, TimeUnit.DAYS); - long acceptableEndTime = TimeUnit.MILLISECONDS.convert(authCredentialPresentation.getRedemptionTime()+2, TimeUnit.DAYS); + public void verifyAuthCredentialPresentation(GroupPublicParams groupPublicParams, AuthCredentialPresentation authCredentialPresentation, Instant currentTime) throws VerificationFailedException, InvalidRedemptionTimeException { + // TODO: Move this check down to Rust. + Instant acceptableStartTime = authCredentialPresentation.getRedemptionTime().minus(1, ChronoUnit.DAYS); + Instant acceptableEndTime = acceptableStartTime.plus(3, ChronoUnit.DAYS); - if (currentTimeMillis < acceptableStartTime || currentTimeMillis > acceptableEndTime) { + if (currentTime.isBefore(acceptableStartTime) || currentTime.isAfter(acceptableEndTime)) { throw new InvalidRedemptionTimeException(); } diff --git a/node/Native.d.ts b/node/Native.d.ts index cb181fc57..921f320ff 100644 --- a/node/Native.d.ts +++ b/node/Native.d.ts @@ -54,9 +54,12 @@ export function Aes256GcmSiv_Decrypt(aesGcmSiv: Wrapper, ctext: Bu export function Aes256GcmSiv_Encrypt(aesGcmSivObj: Wrapper, ptext: Buffer, nonce: Buffer, associatedData: Buffer): Buffer; export function Aes256GcmSiv_New(key: Buffer): Aes256GcmSiv; export function AuthCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void; -export function AuthCredentialPresentation_GetRedemptionTime(presentationBytes: Buffer): number; +export function AuthCredentialPresentation_GetPniCiphertext(presentationBytes: Buffer): Buffer; +export function AuthCredentialPresentation_GetRedemptionTime(presentationBytes: Buffer): Timestamp; export function AuthCredentialPresentation_GetUuidCiphertext(presentationBytes: Buffer): Serialized; export function AuthCredentialResponse_CheckValidContents(buffer: Buffer): void; +export function AuthCredentialWithPniResponse_CheckValidContents(buffer: Buffer): void; +export function AuthCredentialWithPni_CheckValidContents(buffer: Buffer): void; export function AuthCredential_CheckValidContents(buffer: Buffer): void; export function Cds2ClientState_CompleteHandshake(cli: Wrapper, handshakeReceived: Buffer): void; export function Cds2ClientState_EstablishedRecv(cli: Wrapper, receivedCiphertext: Buffer): Buffer; @@ -226,6 +229,7 @@ export function ServerCertificate_GetSignature(obj: Wrapper): export function ServerCertificate_New(keyId: number, serverKey: Wrapper, trustRoot: Wrapper): ServerCertificate; export function ServerPublicParams_CheckValidContents(buffer: Buffer): void; export function ServerPublicParams_CreateAuthCredentialPresentationDeterministic(serverPublicParams: Serialized, randomness: Buffer, groupSecretParams: Serialized, authCredential: Serialized): Buffer; +export function ServerPublicParams_CreateAuthCredentialWithPniPresentationDeterministic(serverPublicParams: Serialized, randomness: Buffer, groupSecretParams: Serialized, authCredential: Serialized): Buffer; export function ServerPublicParams_CreateExpiringProfileKeyCredentialPresentationDeterministic(serverPublicParams: Serialized, randomness: Buffer, groupSecretParams: Serialized, profileKeyCredential: Serialized): Buffer; export function ServerPublicParams_CreatePniCredentialPresentationDeterministic(serverPublicParams: Serialized, randomness: Buffer, groupSecretParams: Serialized, pniCredential: Serialized): Buffer; export function ServerPublicParams_CreatePniCredentialRequestContextDeterministic(serverPublicParams: Serialized, randomness: Buffer, aci: Uuid, pni: Uuid, profileKey: Serialized): Serialized; @@ -234,6 +238,7 @@ export function ServerPublicParams_CreateProfileKeyCredentialRequestContextDeter export function ServerPublicParams_CreateReceiptCredentialPresentationDeterministic(serverPublicParams: Serialized, randomness: Buffer, receiptCredential: Serialized): Serialized; export function ServerPublicParams_CreateReceiptCredentialRequestContextDeterministic(serverPublicParams: Serialized, randomness: Buffer, receiptSerial: Buffer): Serialized; export function ServerPublicParams_ReceiveAuthCredential(params: Serialized, uuid: Uuid, redemptionTime: number, response: Serialized): Serialized; +export function ServerPublicParams_ReceiveAuthCredentialWithPni(params: Serialized, aci: Uuid, pni: Uuid, redemptionTime: Timestamp, response: Serialized): Serialized; export function ServerPublicParams_ReceiveExpiringProfileKeyCredential(serverPublicParams: Serialized, requestContext: Serialized, response: Serialized, currentTimeInSeconds: Timestamp): Serialized; export function ServerPublicParams_ReceivePniCredential(serverPublicParams: Serialized, requestContext: Serialized, response: Serialized): Serialized; export function ServerPublicParams_ReceiveProfileKeyCredential(serverPublicParams: Serialized, requestContext: Serialized, response: Serialized): Serialized; @@ -243,6 +248,7 @@ export function ServerSecretParams_CheckValidContents(buffer: Buffer): void; export function ServerSecretParams_GenerateDeterministic(randomness: Buffer): Serialized; export function ServerSecretParams_GetPublicParams(params: Serialized): Serialized; export function ServerSecretParams_IssueAuthCredentialDeterministic(serverSecretParams: Serialized, randomness: Buffer, uuid: Uuid, redemptionTime: number): Serialized; +export function ServerSecretParams_IssueAuthCredentialWithPniDeterministic(serverSecretParams: Serialized, randomness: Buffer, aci: Uuid, pni: Uuid, redemptionTime: Timestamp): Serialized; export function ServerSecretParams_IssueExpiringProfileKeyCredentialDeterministic(serverSecretParams: Serialized, randomness: Buffer, request: Serialized, uuid: Uuid, commitment: Serialized, expirationInSeconds: Timestamp): Serialized; export function ServerSecretParams_IssuePniCredentialDeterministic(serverSecretParams: Serialized, randomness: Buffer, request: Serialized, aci: Uuid, pni: Uuid, commitment: Serialized): Serialized; export function ServerSecretParams_IssueProfileKeyCredentialDeterministic(serverSecretParams: Serialized, randomness: Buffer, request: Serialized, uuid: Uuid, commitment: Serialized): Serialized; @@ -291,6 +297,8 @@ export function initLogger(maxLevel: LogLevel, callback: (level: LogLevel, targe interface Aes256GcmSiv { readonly __type: unique symbol; } interface AuthCredential { readonly __type: unique symbol; } interface AuthCredentialResponse { readonly __type: unique symbol; } +interface AuthCredentialWithPni { readonly __type: unique symbol; } +interface AuthCredentialWithPniResponse { readonly __type: unique symbol; } interface Cds2ClientState { readonly __type: unique symbol; } interface CiphertextMessage { readonly __type: unique symbol; } interface DecryptionErrorMessage { readonly __type: unique symbol; } diff --git a/node/package.json b/node/package.json index bfdbfdaef..44cd79bd4 100644 --- a/node/package.json +++ b/node/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@types/bindings": "^1.3.0", - "@types/chai": "^4.2.15", + "@types/chai": "^4.3.1", "@types/chai-as-promised": "^7.1.3", "@types/mocha": "^5.2.7", "@types/node": "*", diff --git a/node/ts/test/PublicAPITest.ts b/node/ts/test/PublicAPITest.ts index 1ae6cd4fb..67ae01b29 100644 --- a/node/ts/test/PublicAPITest.ts +++ b/node/ts/test/PublicAPITest.ts @@ -700,28 +700,25 @@ describe('SignalClient', () => { assert.deepEqual(aDPlaintext, bMessage); const session = await bSess.getSession(aAddress); + assert(session !== null); - if (session != null) { - assert(session.serialize().length > 0); - assert.deepEqual(session.localRegistrationId(), 5); - assert.deepEqual(session.remoteRegistrationId(), 5); - assert(session.hasCurrentState()); - assert( - !session.currentRatchetKeyMatches( - SignalClient.PrivateKey.generate().getPublicKey() - ) - ); + assert(session.serialize().length > 0); + assert.deepEqual(session.localRegistrationId(), 5); + assert.deepEqual(session.remoteRegistrationId(), 5); + assert(session.hasCurrentState()); + assert( + !session.currentRatchetKeyMatches( + SignalClient.PrivateKey.generate().getPublicKey() + ) + ); - session.archiveCurrentState(); - assert(!session.hasCurrentState()); - assert( - !session.currentRatchetKeyMatches( - SignalClient.PrivateKey.generate().getPublicKey() - ) - ); - } else { - assert.fail('no session found'); - } + session.archiveCurrentState(); + assert(!session.hasCurrentState()); + assert( + !session.currentRatchetKeyMatches( + SignalClient.PrivateKey.generate().getPublicKey() + ) + ); }); it('handles duplicated messages', async () => { const aKeys = new InMemoryIdentityKeyStore(); @@ -985,13 +982,10 @@ describe('SignalClient', () => { ); assert(bPlaintext != null); - - if (bPlaintext != null) { - assert.deepEqual(bPlaintext.message(), aPlaintext); - assert.deepEqual(bPlaintext.senderE164(), aE164); - assert.deepEqual(bPlaintext.senderUuid(), aUuid); - assert.deepEqual(bPlaintext.deviceId(), aDeviceId); - } + assert.deepEqual(bPlaintext.message(), aPlaintext); + assert.deepEqual(bPlaintext.senderE164(), aE164); + assert.deepEqual(bPlaintext.senderUuid(), aUuid); + assert.deepEqual(bPlaintext.deviceId(), aDeviceId); const innerMessage = await SignalClient.signalEncrypt( aPlaintext, diff --git a/node/ts/test/ZKGroup-test.ts b/node/ts/test/ZKGroup-test.ts index 9e9ee442a..e5869e9ca 100644 --- a/node/ts/test/ZKGroup-test.ts +++ b/node/ts/test/ZKGroup-test.ts @@ -197,7 +197,11 @@ describe('ZKGroup', () => { uuidCiphertext.serialize(), uuidCiphertextRecv.serialize() ); - assert.strictEqual(presentation.getRedemptionTime(), redemptionTime); + assert.isNull(presentation.getPniCiphertext()); + assert.deepEqual( + presentation.getRedemptionTime(), + new Date(redemptionTime * 86400 * 1000) + ); serverZkAuth.verifyAuthCredentialPresentation( groupPublicParams, presentation @@ -206,6 +210,89 @@ describe('ZKGroup', () => { assertArrayEquals(presentation.serialize(), authPresentationResult); }); + it('testAuthWithPniIntegration', () => { + const aci = toUUID(TEST_ARRAY_16); + const pni = toUUID(TEST_ARRAY_16_1); + const redemptionTime = 123456 * 86400; + + // Generate keys (client's are per-group, server's are not) + // --- + + // SERVER + const serverSecretParams = ServerSecretParams.generateWithRandom( + TEST_ARRAY_32 + ); + const serverPublicParams = serverSecretParams.getPublicParams(); + const serverZkAuth = new ServerZkAuthOperations(serverSecretParams); + + // CLIENT + const masterKey = new GroupMasterKey(TEST_ARRAY_32_1); + const groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey); + + assertArrayEquals( + groupSecretParams.getMasterKey().serialize(), + masterKey.serialize() + ); + + const groupPublicParams = groupSecretParams.getPublicParams(); + + // SERVER + // Issue credential + const authCredentialResponse = serverZkAuth.issueAuthCredentialWithPniWithRandom( + TEST_ARRAY_32_2, + aci, + pni, + redemptionTime + ); + + // CLIENT + // Receive credential + const clientZkAuthCipher = new ClientZkAuthOperations(serverPublicParams); + const clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams); + const authCredential = clientZkAuthCipher.receiveAuthCredentialWithPni( + aci, + pni, + redemptionTime, + authCredentialResponse + ); + + // Create and decrypt user entry + const aciCiphertext = clientZkGroupCipher.encryptUuid(aci); + const aciPlaintext = clientZkGroupCipher.decryptUuid(aciCiphertext); + assert.strictEqual(aci, aciPlaintext); + const pniCiphertext = clientZkGroupCipher.encryptUuid(pni); + const pniPlaintext = clientZkGroupCipher.decryptUuid(pniCiphertext); + assert.strictEqual(pni, pniPlaintext); + + // Create presentation + const presentation = clientZkAuthCipher.createAuthCredentialWithPniPresentationWithRandom( + TEST_ARRAY_32_5, + groupSecretParams, + authCredential + ); + + // Verify presentation + assertArrayEquals( + aciCiphertext.serialize(), + presentation.getUuidCiphertext().serialize() + ); + const presentationPniCiphertext = presentation.getPniCiphertext(); + // Use a generic assertion instead of assert.isNotNull because TypeScript understands it. + assert(presentationPniCiphertext !== null); + assertArrayEquals( + pniCiphertext.serialize(), + presentationPniCiphertext.serialize() + ); + assert.deepEqual( + presentation.getRedemptionTime(), + new Date(1000 * redemptionTime) + ); + serverZkAuth.verifyAuthCredentialPresentation( + groupPublicParams, + presentation + ); + }); + it('testProfileKeyIntegration', () => { const uuid = toUUID(TEST_ARRAY_16); diff --git a/node/ts/zkgroup/auth/AuthCredentialPresentation.ts b/node/ts/zkgroup/auth/AuthCredentialPresentation.ts index 3ff13a4c3..dfe59fecd 100644 --- a/node/ts/zkgroup/auth/AuthCredentialPresentation.ts +++ b/node/ts/zkgroup/auth/AuthCredentialPresentation.ts @@ -1,5 +1,5 @@ // -// Copyright 2020-2021 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // @@ -20,7 +20,19 @@ export default class AuthCredentialPresentation extends ByteArray { ); } - getRedemptionTime(): number { - return Native.AuthCredentialPresentation_GetRedemptionTime(this.contents); + getPniCiphertext(): UuidCiphertext | null { + const ciphertextBytes = Native.AuthCredentialPresentation_GetPniCiphertext( + this.contents + ); + if (ciphertextBytes === null) { + return null; + } + return new UuidCiphertext(ciphertextBytes); + } + + getRedemptionTime(): Date { + return new Date( + 1000 * Native.AuthCredentialPresentation_GetRedemptionTime(this.contents) + ); } } diff --git a/node/ts/zkgroup/auth/AuthCredentialWithPni.ts b/node/ts/zkgroup/auth/AuthCredentialWithPni.ts new file mode 100644 index 000000000..bb6b89ffb --- /dev/null +++ b/node/ts/zkgroup/auth/AuthCredentialWithPni.ts @@ -0,0 +1,15 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +import ByteArray from '../internal/ByteArray'; +import * as Native from '../../../Native'; + +export default class AuthCredentialWithPni extends ByteArray { + private readonly __type?: never; + + constructor(contents: Buffer) { + super(contents, Native.AuthCredentialWithPni_CheckValidContents); + } +} diff --git a/node/ts/zkgroup/auth/AuthCredentialWithPniResponse.ts b/node/ts/zkgroup/auth/AuthCredentialWithPniResponse.ts new file mode 100644 index 000000000..57297151e --- /dev/null +++ b/node/ts/zkgroup/auth/AuthCredentialWithPniResponse.ts @@ -0,0 +1,15 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +import ByteArray from '../internal/ByteArray'; +import * as Native from '../../../Native'; + +export default class AuthCredentialWithPniResponse extends ByteArray { + private readonly __type?: never; + + constructor(contents: Buffer) { + super(contents, Native.AuthCredentialWithPniResponse_CheckValidContents); + } +} diff --git a/node/ts/zkgroup/auth/ClientZkAuthOperations.ts b/node/ts/zkgroup/auth/ClientZkAuthOperations.ts index 23cc1e9a3..f8f8ff07d 100644 --- a/node/ts/zkgroup/auth/ClientZkAuthOperations.ts +++ b/node/ts/zkgroup/auth/ClientZkAuthOperations.ts @@ -1,5 +1,5 @@ // -// Copyright 2020-2021 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // @@ -12,6 +12,8 @@ import ServerPublicParams from '../ServerPublicParams'; import AuthCredential from './AuthCredential'; import AuthCredentialPresentation from './AuthCredentialPresentation'; import AuthCredentialResponse from './AuthCredentialResponse'; +import AuthCredentialWithPni from './AuthCredentialWithPni'; +import AuthCredentialWithPniResponse from './AuthCredentialWithPniResponse'; import GroupSecretParams from '../groups/GroupSecretParams'; import { UUIDType, fromUUID } from '../internal/UUIDUtil'; @@ -37,6 +39,28 @@ export default class ClientZkAuthOperations { ); } + /** + * Produces the AuthCredentialWithPni from a server-generated AuthCredentialWithPniResponse. + * + * @param redemptionTime - This is provided by the server as an integer, and should be passed through directly. + */ + receiveAuthCredentialWithPni( + aci: UUIDType, + pni: UUIDType, + redemptionTime: number, + authCredentialResponse: AuthCredentialWithPniResponse + ): AuthCredentialWithPni { + return new AuthCredentialWithPni( + Native.ServerPublicParams_ReceiveAuthCredentialWithPni( + this.serverPublicParams.getContents(), + fromUUID(aci), + fromUUID(pni), + redemptionTime, + authCredentialResponse.getContents() + ) + ); + } + createAuthCredentialPresentation( groupSecretParams: GroupSecretParams, authCredential: AuthCredential @@ -64,4 +88,32 @@ export default class ClientZkAuthOperations { ) ); } + + createAuthCredentialWithPniPresentation( + groupSecretParams: GroupSecretParams, + authCredential: AuthCredentialWithPni + ): AuthCredentialPresentation { + const random = randomBytes(RANDOM_LENGTH); + + return this.createAuthCredentialWithPniPresentationWithRandom( + random, + groupSecretParams, + authCredential + ); + } + + createAuthCredentialWithPniPresentationWithRandom( + random: Buffer, + groupSecretParams: GroupSecretParams, + authCredential: AuthCredentialWithPni + ): AuthCredentialPresentation { + return new AuthCredentialPresentation( + Native.ServerPublicParams_CreateAuthCredentialWithPniPresentationDeterministic( + this.serverPublicParams.getContents(), + random, + groupSecretParams.getContents(), + authCredential.getContents() + ) + ); + } } diff --git a/node/ts/zkgroup/auth/ServerZkAuthOperations.ts b/node/ts/zkgroup/auth/ServerZkAuthOperations.ts index 98a2432b3..25476dc2a 100644 --- a/node/ts/zkgroup/auth/ServerZkAuthOperations.ts +++ b/node/ts/zkgroup/auth/ServerZkAuthOperations.ts @@ -1,5 +1,5 @@ // -// Copyright 2020-2021 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // @@ -10,6 +10,7 @@ import * as Native from '../../../Native'; import ServerSecretParams from '../ServerSecretParams'; import AuthCredentialResponse from './AuthCredentialResponse'; import AuthCredentialPresentation from './AuthCredentialPresentation'; +import AuthCredentialWithPniResponse from './AuthCredentialWithPniResponse'; import GroupPublicParams from '../groups/GroupPublicParams'; import { UUIDType, fromUUID } from '../internal/UUIDUtil'; @@ -44,6 +45,38 @@ export default class ServerZkAuthOperations { ); } + issueAuthCredentialWithPni( + aci: UUIDType, + pni: UUIDType, + redemptionTime: number + ): AuthCredentialWithPniResponse { + const random = randomBytes(RANDOM_LENGTH); + + return this.issueAuthCredentialWithPniWithRandom( + random, + aci, + pni, + redemptionTime + ); + } + + issueAuthCredentialWithPniWithRandom( + random: Buffer, + aci: UUIDType, + pni: UUIDType, + redemptionTime: number + ): AuthCredentialWithPniResponse { + return new AuthCredentialWithPniResponse( + Native.ServerSecretParams_IssueAuthCredentialWithPniDeterministic( + this.serverSecretParams.getContents(), + random, + fromUUID(aci), + fromUUID(pni), + redemptionTime + ) + ); + } + verifyAuthCredentialPresentation( groupPublicParams: GroupPublicParams, authCredentialPresentation: AuthCredentialPresentation diff --git a/node/yarn.lock b/node/yarn.lock index adf84bc7f..890305d04 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -113,10 +113,10 @@ dependencies: "@types/chai" "*" -"@types/chai@*", "@types/chai@^4.2.15": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" - integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw== +"@types/chai@*", "@types/chai@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04" + integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ== "@types/json-schema@^7.0.7": version "7.0.10" diff --git a/rust/bridge/shared/src/zkgroup.rs b/rust/bridge/shared/src/zkgroup.rs index a100367a3..9b39912f1 100644 --- a/rust/bridge/shared/src/zkgroup.rs +++ b/rust/bridge/shared/src/zkgroup.rs @@ -50,6 +50,8 @@ macro_rules! fixed_length_serializable { fixed_length_serializable!(AuthCredential); fixed_length_serializable!(AuthCredentialResponse); +fixed_length_serializable!(AuthCredentialWithPni); +fixed_length_serializable!(AuthCredentialWithPniResponse); fixed_length_serializable!(ExpiringProfileKeyCredential); fixed_length_serializable!(ExpiringProfileKeyCredentialResponse); fixed_length_serializable!(GroupMasterKey); @@ -233,6 +235,24 @@ fn ServerPublicParams_ReceiveAuthCredential( .into()) } +#[bridge_fn] +fn ServerPublicParams_ReceiveAuthCredentialWithPni( + params: Serialized, + aci: Uuid, + pni: Uuid, + redemption_time: Timestamp, + response: Serialized, +) -> Result, ZkGroupVerificationFailure> { + Ok(params + .receive_auth_credential_with_pni( + *aci.as_bytes(), + *pni.as_bytes(), + redemption_time.as_seconds(), + &response, + )? + .into()) +} + #[bridge_fn_buffer] fn ServerPublicParams_CreateAuthCredentialPresentationDeterministic( server_public_params: Serialized, @@ -248,6 +268,23 @@ fn ServerPublicParams_CreateAuthCredentialPresentationDeterministic( .expect("can serialize") } +#[bridge_fn_buffer] +fn ServerPublicParams_CreateAuthCredentialWithPniPresentationDeterministic( + server_public_params: Serialized, + randomness: &[u8; RANDOMNESS_LEN], + group_secret_params: Serialized, + auth_credential: Serialized, +) -> Vec { + bincode::serialize( + &server_public_params.create_auth_credential_with_pni_presentation( + *randomness, + group_secret_params.into_inner(), + auth_credential.into_inner(), + ), + ) + .expect("can serialize") +} + #[bridge_fn] fn ServerPublicParams_CreateProfileKeyCredentialRequestContextDeterministic( server_public_params: Serialized, @@ -414,6 +451,24 @@ fn ServerSecretParams_IssueAuthCredentialDeterministic( .into() } +#[bridge_fn] +fn ServerSecretParams_IssueAuthCredentialWithPniDeterministic( + server_secret_params: Serialized, + randomness: &[u8; RANDOMNESS_LEN], + aci: Uuid, + pni: Uuid, + redemption_time: Timestamp, +) -> Serialized { + server_secret_params + .issue_auth_credential_with_pni( + *randomness, + *aci.as_bytes(), + *pni.as_bytes(), + redemption_time.as_seconds(), + ) + .into() +} + #[bridge_fn_void] fn ServerSecretParams_VerifyAuthCredentialPresentation( server_secret_params: Serialized, @@ -572,11 +627,20 @@ fn AuthCredentialPresentation_GetUuidCiphertext( presentation.get_uuid_ciphertext().into() } -#[bridge_fn] -fn AuthCredentialPresentation_GetRedemptionTime(presentation_bytes: &[u8]) -> u32 { +#[bridge_fn_buffer] +fn AuthCredentialPresentation_GetPniCiphertext(presentation_bytes: &[u8]) -> Option> { let presentation = AnyAuthCredentialPresentation::new(presentation_bytes) .expect("should have been parsed previously"); - presentation.get_redemption_time() + presentation + .get_pni_ciphertext() + .map(|ciphertext| bincode::serialize(&ciphertext).expect("can serialize")) +} + +#[bridge_fn] +fn AuthCredentialPresentation_GetRedemptionTime(presentation_bytes: &[u8]) -> Timestamp { + let presentation = AnyAuthCredentialPresentation::new(presentation_bytes) + .expect("should have been parsed previously"); + Timestamp::from_seconds(presentation.get_redemption_time()) } // FIXME: bridge_get diff --git a/rust/zkgroup/src/api/auth.rs b/rust/zkgroup/src/api/auth.rs index 3bbe9a3bf..e4fc956bf 100644 --- a/rust/zkgroup/src/api/auth.rs +++ b/rust/zkgroup/src/api/auth.rs @@ -1,14 +1,19 @@ // -// Copyright 2020 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // pub mod auth_credential; pub mod auth_credential_presentation; pub mod auth_credential_response; +pub mod auth_credential_with_pni; +pub mod auth_credential_with_pni_response; pub use auth_credential::AuthCredential; pub use auth_credential_presentation::AnyAuthCredentialPresentation; pub use auth_credential_presentation::AuthCredentialPresentationV1; pub use auth_credential_presentation::AuthCredentialPresentationV2; +pub use auth_credential_presentation::AuthCredentialWithPniPresentation; pub use auth_credential_response::AuthCredentialResponse; +pub use auth_credential_with_pni::AuthCredentialWithPni; +pub use auth_credential_with_pni_response::AuthCredentialWithPniResponse; diff --git a/rust/zkgroup/src/api/auth/auth_credential_presentation.rs b/rust/zkgroup/src/api/auth/auth_credential_presentation.rs index 1c7743b9c..e85327575 100644 --- a/rust/zkgroup/src/api/auth/auth_credential_presentation.rs +++ b/rust/zkgroup/src/api/auth/auth_credential_presentation.rs @@ -1,5 +1,5 @@ // -// Copyright 2020 Signal Messenger, LLC. +// Copyright 2020-2022 Signal Messenger, LLC. // SPDX-License-Identifier: AGPL-3.0-only // @@ -32,6 +32,7 @@ impl AuthCredentialPresentationV1 { } } +/// Like [`AuthCredentialPresentationV1`], but with an optimized proof. #[derive(Serialize, Deserialize)] pub struct AuthCredentialPresentationV2 { pub(crate) version: ReservedBytes, @@ -53,9 +54,40 @@ impl AuthCredentialPresentationV2 { } } +#[derive(Serialize, Deserialize)] +pub struct AuthCredentialWithPniPresentation { + pub(crate) version: ReservedBytes, + pub(crate) proof: crypto::proofs::AuthCredentialWithPniPresentationProof, + pub(crate) aci_ciphertext: crypto::uid_encryption::Ciphertext, + pub(crate) pni_ciphertext: crypto::uid_encryption::Ciphertext, + pub(crate) redemption_time: Timestamp, +} + +impl AuthCredentialWithPniPresentation { + pub fn get_aci_ciphertext(&self) -> api::groups::UuidCiphertext { + api::groups::UuidCiphertext { + reserved: Default::default(), + ciphertext: self.aci_ciphertext, + } + } + + pub fn get_pni_ciphertext(&self) -> api::groups::UuidCiphertext { + api::groups::UuidCiphertext { + reserved: Default::default(), + ciphertext: self.pni_ciphertext, + } + } + + pub fn get_redemption_time(&self) -> Timestamp { + self.redemption_time + } +} + +#[allow(clippy::large_enum_variant)] pub enum AnyAuthCredentialPresentation { V1(AuthCredentialPresentationV1), V2(AuthCredentialPresentationV2), + V3(AuthCredentialWithPniPresentation), } impl AnyAuthCredentialPresentation { @@ -73,29 +105,44 @@ impl AnyAuthCredentialPresentation { Err(_) => Err(ZkGroupDeserializationFailure), } } + PRESENTATION_VERSION_3 => { + match bincode::deserialize::(presentation_bytes) + { + Ok(presentation) => Ok(AnyAuthCredentialPresentation::V3(presentation)), + Err(_) => Err(ZkGroupDeserializationFailure), + } + } _ => Err(ZkGroupDeserializationFailure), } } pub fn get_uuid_ciphertext(&self) -> api::groups::UuidCiphertext { match self { - AnyAuthCredentialPresentation::V1(presentation_v1) => { - presentation_v1.get_uuid_ciphertext() - } - AnyAuthCredentialPresentation::V2(presentation_v2) => { - presentation_v2.get_uuid_ciphertext() + AnyAuthCredentialPresentation::V1(presentation) => presentation.get_uuid_ciphertext(), + AnyAuthCredentialPresentation::V2(presentation) => presentation.get_uuid_ciphertext(), + AnyAuthCredentialPresentation::V3(presentation) => presentation.get_aci_ciphertext(), + } + } + + pub fn get_pni_ciphertext(&self) -> Option { + match self { + AnyAuthCredentialPresentation::V1(_presentation) => None, + AnyAuthCredentialPresentation::V2(_presentation) => None, + AnyAuthCredentialPresentation::V3(presentation) => { + Some(presentation.get_pni_ciphertext()) } } } - pub fn get_redemption_time(&self) -> CoarseRedemptionTime { + pub fn get_redemption_time(&self) -> Timestamp { match self { - AnyAuthCredentialPresentation::V1(presentation_v1) => { - presentation_v1.get_redemption_time() + AnyAuthCredentialPresentation::V1(presentation) => { + u64::from(presentation.get_redemption_time()) * SECONDS_PER_DAY } - AnyAuthCredentialPresentation::V2(presentation_v2) => { - presentation_v2.get_redemption_time() + AnyAuthCredentialPresentation::V2(presentation) => { + u64::from(presentation.get_redemption_time()) * SECONDS_PER_DAY } + AnyAuthCredentialPresentation::V3(presentation) => presentation.get_redemption_time(), } } } @@ -106,12 +153,25 @@ impl Serialize for AnyAuthCredentialPresentation { S: Serializer, { match self { - AnyAuthCredentialPresentation::V1(presentation_v1) => { - presentation_v1.serialize(serializer) - } - AnyAuthCredentialPresentation::V2(presentation_v2) => { - presentation_v2.serialize(serializer) - } + AnyAuthCredentialPresentation::V1(presentation) => presentation.serialize(serializer), + AnyAuthCredentialPresentation::V2(presentation) => presentation.serialize(serializer), + AnyAuthCredentialPresentation::V3(presentation) => presentation.serialize(serializer), } } } + +impl From for AnyAuthCredentialPresentation { + fn from(presentation: AuthCredentialPresentationV1) -> Self { + Self::V1(presentation) + } +} +impl From for AnyAuthCredentialPresentation { + fn from(presentation: AuthCredentialPresentationV2) -> Self { + Self::V2(presentation) + } +} +impl From for AnyAuthCredentialPresentation { + fn from(presentation: AuthCredentialWithPniPresentation) -> Self { + Self::V3(presentation) + } +} diff --git a/rust/zkgroup/src/api/auth/auth_credential_with_pni.rs b/rust/zkgroup/src/api/auth/auth_credential_with_pni.rs new file mode 100644 index 000000000..c0fb2489f --- /dev/null +++ b/rust/zkgroup/src/api/auth/auth_credential_with_pni.rs @@ -0,0 +1,18 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +use serde::{Deserialize, Serialize}; + +use crate::common::simple_types::*; +use crate::crypto; + +#[derive(Copy, Clone, Serialize, Deserialize)] +pub struct AuthCredentialWithPni { + pub(crate) reserved: ReservedBytes, + pub(crate) credential: crypto::credentials::AuthCredentialWithPni, + pub(crate) aci: crypto::uid_struct::UidStruct, + pub(crate) pni: crypto::uid_struct::UidStruct, + pub(crate) redemption_time: Timestamp, +} diff --git a/rust/zkgroup/src/api/auth/auth_credential_with_pni_response.rs b/rust/zkgroup/src/api/auth/auth_credential_with_pni_response.rs new file mode 100644 index 000000000..780d24f39 --- /dev/null +++ b/rust/zkgroup/src/api/auth/auth_credential_with_pni_response.rs @@ -0,0 +1,15 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +use crate::common::simple_types::*; +use crate::crypto; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct AuthCredentialWithPniResponse { + pub(crate) reserved: ReservedBytes, + pub(crate) credential: crypto::credentials::AuthCredentialWithPni, + pub(crate) proof: crypto::proofs::AuthCredentialWithPniIssuanceProof, +} diff --git a/rust/zkgroup/src/api/server_params.rs b/rust/zkgroup/src/api/server_params.rs index 7ad42afd3..ecd66273d 100644 --- a/rust/zkgroup/src/api/server_params.rs +++ b/rust/zkgroup/src/api/server_params.rs @@ -25,6 +25,8 @@ pub struct ServerSecretParams { pni_credentials_key_pair: crypto::credentials::KeyPair, expiring_profile_key_credentials_key_pair: crypto::credentials::KeyPair, + auth_credentials_with_pni_key_pair: + crypto::credentials::KeyPair, } #[derive(Copy, Clone, Serialize, Deserialize)] @@ -36,6 +38,7 @@ pub struct ServerPublicParams { receipt_credentials_public_key: crypto::credentials::PublicKey, pni_credentials_public_key: crypto::credentials::PublicKey, expiring_profile_key_credentials_public_key: crypto::credentials::PublicKey, + auth_credentials_with_pni_public_key: crypto::credentials::PublicKey, } impl ServerSecretParams { @@ -52,6 +55,7 @@ impl ServerSecretParams { let pni_credentials_key_pair = crypto::credentials::KeyPair::generate(&mut sho); let expiring_profile_key_credentials_key_pair = crypto::credentials::KeyPair::generate(&mut sho); + let auth_credentials_with_pni_key_pair = crypto::credentials::KeyPair::generate(&mut sho); Self { reserved: Default::default(), @@ -61,6 +65,7 @@ impl ServerSecretParams { receipt_credentials_key_pair, pni_credentials_key_pair, expiring_profile_key_credentials_key_pair, + auth_credentials_with_pni_key_pair, } } @@ -77,6 +82,9 @@ impl ServerSecretParams { expiring_profile_key_credentials_public_key: self .expiring_profile_key_credentials_key_pair .get_public_key(), + auth_credentials_with_pni_public_key: self + .auth_credentials_with_pni_key_pair + .get_public_key(), } } @@ -117,27 +125,69 @@ impl ServerSecretParams { } } + pub fn issue_auth_credential_with_pni( + &self, + randomness: RandomnessBytes, + aci_bytes: UidBytes, + pni_bytes: UidBytes, + redemption_time: Timestamp, + ) -> api::auth::AuthCredentialWithPniResponse { + let mut sho = Sho::new( + b"Signal_ZKGroup_20220617_Random_ServerSecretParams_IssueAuthCredentialWithPni", + &randomness, + ); + + let aci = crypto::uid_struct::UidStruct::new(aci_bytes); + let pni = crypto::uid_struct::UidStruct::new(pni_bytes); + let credential = self + .auth_credentials_with_pni_key_pair + .create_auth_credential_with_pni(aci, pni, redemption_time, &mut sho); + let proof = crypto::proofs::AuthCredentialWithPniIssuanceProof::new( + self.auth_credentials_with_pni_key_pair, + credential, + aci, + pni, + redemption_time, + &mut sho, + ); + api::auth::AuthCredentialWithPniResponse { + reserved: Default::default(), + credential, + proof, + } + } + pub fn verify_auth_credential_presentation( &self, group_public_params: api::groups::GroupPublicParams, presentation: &api::auth::AnyAuthCredentialPresentation, ) -> Result<(), ZkGroupVerificationFailure> { match presentation { - api::auth::AnyAuthCredentialPresentation::V1(presentation_v1) => { - presentation_v1.proof.verify( + api::auth::AnyAuthCredentialPresentation::V1(presentation) => { + presentation.proof.verify( self.auth_credentials_key_pair, group_public_params.uid_enc_public_key, - presentation_v1.ciphertext, - presentation_v1.redemption_time, + presentation.ciphertext, + presentation.redemption_time, ) } - api::auth::AnyAuthCredentialPresentation::V2(presentation_v2) => { - presentation_v2.proof.verify( + api::auth::AnyAuthCredentialPresentation::V2(presentation) => { + presentation.proof.verify( self.auth_credentials_key_pair, group_public_params.uid_enc_public_key, - presentation_v2.ciphertext, - presentation_v2.redemption_time, + presentation.ciphertext, + presentation.redemption_time, + ) + } + + api::auth::AnyAuthCredentialPresentation::V3(presentation) => { + presentation.proof.verify( + self.auth_credentials_with_pni_key_pair, + group_public_params.uid_enc_public_key, + presentation.aci_ciphertext, + presentation.pni_ciphertext, + presentation.redemption_time, ) } } @@ -169,6 +219,20 @@ impl ServerSecretParams { ) } + pub fn verify_auth_credential_with_pni_presentation( + &self, + group_public_params: api::groups::GroupPublicParams, + presentation: &api::auth::AuthCredentialWithPniPresentation, + ) -> Result<(), ZkGroupVerificationFailure> { + presentation.proof.verify( + self.auth_credentials_with_pni_key_pair, + group_public_params.uid_enc_public_key, + presentation.aci_ciphertext, + presentation.pni_ciphertext, + presentation.redemption_time, + ) + } + pub fn verify_profile_key_credential_presentation( &self, group_public_params: api::groups::GroupPublicParams, @@ -550,6 +614,32 @@ impl ServerPublicParams { }) } + pub fn receive_auth_credential_with_pni( + &self, + aci_bytes: UidBytes, + pni_bytes: UidBytes, + redemption_time: Timestamp, + response: &api::auth::AuthCredentialWithPniResponse, + ) -> Result { + let aci = crypto::uid_struct::UidStruct::new(aci_bytes); + let pni = crypto::uid_struct::UidStruct::new(pni_bytes); + response.proof.verify( + self.auth_credentials_with_pni_public_key, + response.credential, + aci, + pni, + redemption_time, + )?; + + Ok(api::auth::AuthCredentialWithPni { + reserved: Default::default(), + credential: response.credential, + aci, + pni, + redemption_time, + }) + } + pub fn create_auth_credential_presentation( &self, randomness: RandomnessBytes, @@ -626,6 +716,41 @@ impl ServerPublicParams { } } + pub fn create_auth_credential_with_pni_presentation( + &self, + randomness: RandomnessBytes, + group_secret_params: api::groups::GroupSecretParams, + auth_credential: api::auth::AuthCredentialWithPni, + ) -> api::auth::AuthCredentialWithPniPresentation { + let mut sho = Sho::new( + b"Signal_ZKGroup_20220617_Random_ServerPublicParams_CreateAuthCredentialWithPniPresentation", + &randomness, + ); + + let aci_ciphertext = group_secret_params.encrypt_uid_struct(auth_credential.aci); + let pni_ciphertext = group_secret_params.encrypt_uid_struct(auth_credential.pni); + + let proof = crypto::proofs::AuthCredentialWithPniPresentationProof::new( + self.auth_credentials_with_pni_public_key, + group_secret_params.uid_enc_key_pair, + auth_credential.credential, + auth_credential.aci, + aci_ciphertext.ciphertext, + auth_credential.pni, + pni_ciphertext.ciphertext, + auth_credential.redemption_time, + &mut sho, + ); + + api::auth::AuthCredentialWithPniPresentation { + version: [PRESENTATION_VERSION_3], + proof, + aci_ciphertext: aci_ciphertext.ciphertext, + pni_ciphertext: pni_ciphertext.ciphertext, + redemption_time: auth_credential.redemption_time, + } + } + pub fn create_profile_key_credential_request_context( &self, randomness: RandomnessBytes, @@ -728,13 +853,13 @@ impl ServerPublicParams { response.credential_expiration_time, )?; - if response.credential_expiration_time % 86400 != 0 { + if response.credential_expiration_time % SECONDS_PER_DAY != 0 { return Err(ZkGroupVerificationFailure); } let days_remaining = response .credential_expiration_time .saturating_sub(current_time) - / 86400; + / SECONDS_PER_DAY; if days_remaining == 0 || days_remaining > 7 { return Err(ZkGroupVerificationFailure); } diff --git a/rust/zkgroup/src/common/constants.rs b/rust/zkgroup/src/common/constants.rs index 35a4c7475..566aec013 100644 --- a/rust/zkgroup/src/common/constants.rs +++ b/rust/zkgroup/src/common/constants.rs @@ -22,6 +22,8 @@ pub const AUTH_CREDENTIAL_LEN: usize = 181; pub const AUTH_CREDENTIAL_PRESENTATION_V1_LEN: usize = 493; pub const AUTH_CREDENTIAL_PRESENTATION_V2_LEN: usize = 461; pub const AUTH_CREDENTIAL_RESPONSE_LEN: usize = 361; +pub const AUTH_CREDENTIAL_WITH_PNI_LEN: usize = 265; +pub const AUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN: usize = 425; pub const PNI_CREDENTIAL_LEN: usize = 161; pub const PNI_CREDENTIAL_PRESENTATION_V1_LEN: usize = 841; pub const PNI_CREDENTIAL_PRESENTATION_V2_LEN: usize = 841; @@ -47,13 +49,16 @@ pub const RECEIPT_CREDENTIAL_REQUEST_CONTEXT_LEN: usize = 177; pub const RECEIPT_CREDENTIAL_RESPONSE_LEN: usize = 409; pub const RECEIPT_SERIAL_LEN: usize = 16; pub const RESERVED_LEN: usize = 1; -pub const SERVER_SECRET_PARAMS_LEN: usize = 1921; -pub const SERVER_PUBLIC_PARAMS_LEN: usize = 353; +pub const SERVER_SECRET_PARAMS_LEN: usize = 2305; +pub const SERVER_PUBLIC_PARAMS_LEN: usize = 417; pub const UUID_CIPHERTEXT_LEN: usize = 65; pub const RANDOMNESS_LEN: usize = 32; pub const SIGNATURE_LEN: usize = 64; pub const UUID_LEN: usize = 16; +/// Seconds in a 24-hour cycle (ignoring leap seconds). +pub const SECONDS_PER_DAY: u64 = 86400; + pub const TEST_ARRAY_16: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; pub const TEST_ARRAY_16_1: [u8; 16] = [ diff --git a/rust/zkgroup/src/crypto/credentials.rs b/rust/zkgroup/src/crypto/credentials.rs index db4490da2..7bcbd6e14 100644 --- a/rust/zkgroup/src/crypto/credentials.rs +++ b/rust/zkgroup/src/crypto/credentials.rs @@ -66,6 +66,9 @@ impl AttrScalars for AuthCredential { type Storage = [Scalar; 4]; const NUM_ATTRS: usize = NUM_AUTH_CRED_ATTRIBUTES; } +impl AttrScalars for AuthCredentialWithPni { + type Storage = [Scalar; 5]; +} impl AttrScalars for ProfileKeyCredential { // Store four scalars for backwards compatibility. type Storage = [Scalar; 4]; @@ -134,6 +137,13 @@ pub struct AuthCredential { pub(crate) V: RistrettoPoint, } +#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)] +pub struct AuthCredentialWithPni { + pub(crate) t: Scalar, + pub(crate) U: RistrettoPoint, + pub(crate) V: RistrettoPoint, +} + #[derive(Copy, Clone, PartialEq, Serialize, Deserialize)] pub struct ProfileKeyCredential { pub(crate) t: Scalar, @@ -238,6 +248,22 @@ pub(crate) fn convert_to_points_uid_struct( vec![uid.M1, uid.M2, redemption_time_scalar * system.G_m3] } +pub(crate) fn convert_to_points_aci_pni_timestamp( + aci: uid_struct::UidStruct, + pni: uid_struct::UidStruct, + redemption_time: Timestamp, +) -> Vec { + let system = SystemParams::get_hardcoded(); + let redemption_time_scalar = TimestampStruct::calc_m_from(redemption_time); + vec![ + aci.M1, + aci.M2, + pni.M1, + pni.M2, + redemption_time_scalar * system.G_m5, + ] +} + pub(crate) fn convert_to_points_receipt_struct( receipt: receipt_struct::ReceiptStruct, ) -> Vec { @@ -431,6 +457,20 @@ impl KeyPair { } } +impl KeyPair { + pub fn create_auth_credential_with_pni( + &self, + aci: uid_struct::UidStruct, + pni: uid_struct::UidStruct, + redemption_time: Timestamp, + sho: &mut Sho, + ) -> AuthCredentialWithPni { + let M = convert_to_points_aci_pni_timestamp(aci, pni, redemption_time); + let (t, U, V) = self.credential_core(&M, sho); + AuthCredentialWithPni { t, U, V } + } +} + impl KeyPair { pub fn create_blinded_profile_key_credential( &self, diff --git a/rust/zkgroup/src/crypto/proofs.rs b/rust/zkgroup/src/crypto/proofs.rs index f16ffc51a..ef64870eb 100644 --- a/rust/zkgroup/src/crypto/proofs.rs +++ b/rust/zkgroup/src/crypto/proofs.rs @@ -32,6 +32,11 @@ pub struct AuthCredentialIssuanceProof { poksho_proof: Vec, } +#[derive(Serialize, Deserialize, Clone)] +pub struct AuthCredentialWithPniIssuanceProof { + poksho_proof: Vec, +} + #[derive(Serialize, Deserialize, Clone)] pub struct ProfileKeyCredentialRequestProof { poksho_proof: Vec, @@ -79,6 +84,19 @@ pub struct AuthCredentialPresentationProofV2 { poksho_proof: Vec, } +#[derive(Serialize, Deserialize, Clone)] +pub struct AuthCredentialWithPniPresentationProof { + C_x0: RistrettoPoint, + C_x1: RistrettoPoint, + C_y1: RistrettoPoint, + C_y2: RistrettoPoint, + C_y3: RistrettoPoint, + C_y4: RistrettoPoint, + C_y5: RistrettoPoint, + C_V: RistrettoPoint, + poksho_proof: Vec, +} + #[derive(Serialize, Deserialize, Clone)] pub struct ProfileKeyCredentialPresentationProofV1 { C_x0: RistrettoPoint, @@ -269,6 +287,141 @@ impl AuthCredentialIssuanceProof { } } +impl AuthCredentialWithPniIssuanceProof { + pub fn get_poksho_statement() -> poksho::Statement { + let mut st = poksho::Statement::new(); + st.add("C_W", &[("w", "G_w"), ("wprime", "G_wprime")]); + st.add( + "G_V-I", + &[ + ("x0", "G_x0"), + ("x1", "G_x1"), + ("y1", "G_y1"), + ("y2", "G_y2"), + ("y3", "G_y3"), + ("y4", "G_y4"), + ("y5", "G_y5"), + ], + ); + st.add( + "V", + &[ + ("w", "G_w"), + ("x0", "U"), + ("x1", "tU"), + ("y1", "M1"), + ("y2", "M2"), + ("y3", "M3"), + ("y4", "M4"), + ("y5", "M5"), + ], + ); + st + } + + pub fn new( + key_pair: credentials::KeyPair, + credential: credentials::AuthCredentialWithPni, + aci: uid_struct::UidStruct, + pni: uid_struct::UidStruct, + redemption_time: Timestamp, + sho: &mut Sho, + ) -> Self { + let system = credentials::SystemParams::get_hardcoded(); + + let M = OneBased(credentials::convert_to_points_aci_pni_timestamp( + aci, + pni, + redemption_time, + )); + + let mut scalar_args = poksho::ScalarArgs::new(); + scalar_args.add("w", key_pair.w); + scalar_args.add("wprime", key_pair.wprime); + scalar_args.add("x0", key_pair.x0); + scalar_args.add("x1", key_pair.x1); + scalar_args.add("y1", key_pair.y[1]); + scalar_args.add("y2", key_pair.y[2]); + scalar_args.add("y3", key_pair.y[3]); + scalar_args.add("y4", key_pair.y[4]); + scalar_args.add("y5", key_pair.y[5]); + + let mut point_args = poksho::PointArgs::new(); + point_args.add("C_W", key_pair.C_W); + point_args.add("G_w", system.G_w); + point_args.add("G_wprime", system.G_wprime); + point_args.add("G_V-I", system.G_V - key_pair.I); + point_args.add("G_x0", system.G_x0); + point_args.add("G_x1", system.G_x1); + point_args.add("G_y1", system.G_y[1]); + point_args.add("G_y2", system.G_y[2]); + point_args.add("G_y3", system.G_y[3]); + point_args.add("G_y4", system.G_y[4]); + point_args.add("G_y5", system.G_y[5]); + point_args.add("V", credential.V); + point_args.add("U", credential.U); + point_args.add("tU", credential.t * credential.U); + point_args.add("M1", M[1]); + point_args.add("M2", M[2]); + point_args.add("M3", M[3]); + point_args.add("M4", M[4]); + point_args.add("M5", M[5]); + + let poksho_proof = Self::get_poksho_statement() + .prove( + &scalar_args, + &point_args, + &[], + &sho.squeeze(RANDOMNESS_LEN)[..], + ) + .unwrap(); + Self { poksho_proof } + } + + pub fn verify( + &self, + public_key: credentials::PublicKey, + credential: credentials::AuthCredentialWithPni, + aci_struct: uid_struct::UidStruct, + pni_struct: uid_struct::UidStruct, + redemption_time: Timestamp, + ) -> Result<(), ZkGroupVerificationFailure> { + let system = credentials::SystemParams::get_hardcoded(); + + let M = OneBased(credentials::convert_to_points_aci_pni_timestamp( + aci_struct, + pni_struct, + redemption_time, + )); + + let mut point_args = poksho::PointArgs::new(); + point_args.add("C_W", public_key.C_W); + point_args.add("G_w", system.G_w); + point_args.add("G_wprime", system.G_wprime); + point_args.add("G_V-I", system.G_V - public_key.I); + point_args.add("G_x0", system.G_x0); + point_args.add("G_x1", system.G_x1); + point_args.add("G_y1", system.G_y[1]); + point_args.add("G_y2", system.G_y[2]); + point_args.add("G_y3", system.G_y[3]); + point_args.add("G_y4", system.G_y[4]); + point_args.add("G_y5", system.G_y[5]); + point_args.add("V", credential.V); + point_args.add("U", credential.U); + point_args.add("tU", credential.t * credential.U); + point_args.add("M1", M[1]); + point_args.add("M2", M[2]); + point_args.add("M3", M[3]); + point_args.add("M4", M[4]); + point_args.add("M5", M[5]); + + match Self::get_poksho_statement().verify_proof(&self.poksho_proof, &point_args, &[]) { + Err(_) => Err(ZkGroupVerificationFailure), + Ok(_) => Ok(()), + } + } +} + impl ProfileKeyCredentialRequestProof { pub fn get_poksho_statement() -> poksho::Statement { let mut st = poksho::Statement::new(); @@ -1179,6 +1332,200 @@ impl AuthCredentialPresentationProofV2 { } } +impl AuthCredentialWithPniPresentationProof { + pub fn get_poksho_statement() -> poksho::Statement { + let mut st = poksho::Statement::new(); + st.add("Z", &[("z", "I")]); + st.add("C_x1", &[("t", "C_x0"), ("z0", "G_x0"), ("z", "G_x1")]); + st.add("A", &[("a1", "G_a1"), ("a2", "G_a2")]); + st.add("C_y2-E_A2", &[("z", "G_y2"), ("a2", "-E_A1")]); + st.add("E_A1", &[("a1", "C_y1"), ("z1", "G_y1")]); + st.add("C_y4-E_B2", &[("z", "G_y4"), ("a2", "-E_B1")]); + st.add("E_B1", &[("a1", "C_y3"), ("z1", "G_y3")]); + st.add("0", &[("z1", "I"), ("a1", "Z")]); + st + } + + #[allow(clippy::too_many_arguments)] + pub fn new( + credentials_public_key: credentials::PublicKey, + uid_enc_key_pair: uid_encryption::KeyPair, + credential: credentials::AuthCredentialWithPni, + aci: uid_struct::UidStruct, + aci_ciphertext: uid_encryption::Ciphertext, + pni: uid_struct::UidStruct, + pni_ciphertext: uid_encryption::Ciphertext, + redemption_time: Timestamp, + sho: &mut Sho, + ) -> Self { + let credentials_system = credentials::SystemParams::get_hardcoded(); + let uid_system = uid_encryption::SystemParams::get_hardcoded(); + let M = OneBased(credentials::convert_to_points_aci_pni_timestamp( + aci, + pni, + redemption_time, + )); + + let z = sho.get_scalar(); + + let C_y1 = z * credentials_system.G_y[1] + M[1]; + let C_y2 = z * credentials_system.G_y[2] + M[2]; + let C_y3 = z * credentials_system.G_y[3] + M[3]; + let C_y4 = z * credentials_system.G_y[4] + M[4]; + let C_y5 = z * credentials_system.G_y[5]; + + let C_x0 = z * credentials_system.G_x0 + credential.U; + let C_V = z * credentials_system.G_V + credential.V; + let C_x1 = z * credentials_system.G_x1 + credential.t * credential.U; + + let z0 = -z * credential.t; + let z1 = -z * uid_enc_key_pair.a1; + + let I = credentials_public_key.I; + let Z = z * I; + + // Scalars listed in order of stmts for debugging + let mut scalar_args = poksho::ScalarArgs::new(); + scalar_args.add("z", z); + scalar_args.add("t", credential.t); + scalar_args.add("z0", z0); + scalar_args.add("a1", uid_enc_key_pair.a1); + scalar_args.add("a2", uid_enc_key_pair.a2); + scalar_args.add("z1", z1); + + // Points listed in order of stmts for debugging + let mut point_args = poksho::PointArgs::new(); + point_args.add("Z", Z); + point_args.add("I", I); + + point_args.add("C_x1", C_x1); + point_args.add("C_x0", C_x0); + point_args.add("G_x0", credentials_system.G_x0); + point_args.add("G_x1", credentials_system.G_x1); + + point_args.add("A", uid_enc_key_pair.A); + point_args.add("G_a1", uid_system.G_a1); + point_args.add("G_a2", uid_system.G_a2); + + point_args.add("C_y2-E_A2", C_y2 - aci_ciphertext.E_A2); + point_args.add("G_y2", credentials_system.G_y[2]); + point_args.add("-E_A1", -aci_ciphertext.E_A1); + point_args.add("E_A1", aci_ciphertext.E_A1); + point_args.add("C_y1", C_y1); + point_args.add("G_y1", credentials_system.G_y[1]); + + point_args.add("C_y4-E_B2", C_y4 - pni_ciphertext.E_A2); + point_args.add("G_y4", credentials_system.G_y[4]); + point_args.add("-E_B1", -pni_ciphertext.E_A1); + point_args.add("E_B1", pni_ciphertext.E_A1); + point_args.add("C_y3", C_y3); + point_args.add("G_y3", credentials_system.G_y[3]); + point_args.add("0", RistrettoPoint::identity()); + + let poksho_proof = Self::get_poksho_statement() + .prove( + &scalar_args, + &point_args, + &[], + &sho.squeeze(RANDOMNESS_LEN)[..], + ) + .unwrap(); + + Self { + C_x0, + C_x1, + C_y1, + C_y2, + C_y3, + C_y4, + C_y5, + C_V, + poksho_proof, + } + } + + pub fn verify( + &self, + credentials_key_pair: credentials::KeyPair, + uid_enc_public_key: uid_encryption::PublicKey, + aci_ciphertext: uid_encryption::Ciphertext, + pni_ciphertext: uid_encryption::Ciphertext, + redemption_time: Timestamp, + ) -> Result<(), ZkGroupVerificationFailure> { + let uid_enc_system = uid_encryption::SystemParams::get_hardcoded(); + let credentials_system = credentials::SystemParams::get_hardcoded(); + + let Self { + C_x0, + C_x1, + C_y1, + C_y2, + C_y3, + C_y4, + C_y5, + C_V, + poksho_proof, + } = self; + + let (C_x0, C_x1, C_y1, C_y2, C_y3, C_y4, C_y5, C_V) = + (*C_x0, *C_x1, *C_y1, *C_y2, *C_y3, *C_y4, *C_y5, *C_V); + + let credentials::KeyPair { + W, + x0, + x1, + y: OneBased([y1, y2, y3, y4, y5]), + I, + .. + } = credentials_key_pair; + + let m5 = TimestampStruct::calc_m_from(redemption_time); + let M5 = m5 * credentials_system.G_m5; + let Z = C_V + - W + - x0 * C_x0 + - x1 * C_x1 + - y1 * C_y1 + - y2 * C_y2 + - y3 * C_y3 + - y4 * C_y4 + - y5 * (C_y5 + M5); + + // Points listed in order of stmts for debugging + let mut point_args = poksho::PointArgs::new(); + point_args.add("Z", Z); + point_args.add("I", I); + point_args.add("C_x1", C_x1); + point_args.add("C_x0", C_x0); + point_args.add("G_x0", credentials_system.G_x0); + point_args.add("G_x1", credentials_system.G_x1); + + point_args.add("A", uid_enc_public_key.A); + point_args.add("G_a1", uid_enc_system.G_a1); + point_args.add("G_a2", uid_enc_system.G_a2); + + point_args.add("C_y2-E_A2", C_y2 - aci_ciphertext.E_A2); + point_args.add("G_y2", credentials_system.G_y[2]); + point_args.add("-E_A1", -aci_ciphertext.E_A1); + point_args.add("E_A1", aci_ciphertext.E_A1); + point_args.add("C_y1", C_y1); + point_args.add("G_y1", credentials_system.G_y[1]); + + point_args.add("C_y4-E_B2", C_y4 - pni_ciphertext.E_A2); + point_args.add("G_y4", credentials_system.G_y[4]); + point_args.add("-E_B1", -pni_ciphertext.E_A1); + point_args.add("E_B1", pni_ciphertext.E_A1); + point_args.add("C_y3", C_y3); + point_args.add("G_y3", credentials_system.G_y[3]); + point_args.add("0", RistrettoPoint::identity()); + + match Self::get_poksho_statement().verify_proof(poksho_proof, &point_args, &[]) { + Err(_) => Err(ZkGroupVerificationFailure), + Ok(_) => Ok(()), + } + } +} + impl ProfileKeyCredentialPresentationProofV1 { pub fn get_poksho_statement() -> poksho::Statement { let mut st = poksho::Statement::new(); @@ -1760,7 +2107,6 @@ impl ExpiringProfileKeyCredentialPresentationProof { .. } = credentials_key_pair; - // TEST let m5 = TimestampStruct::calc_m_from(credential_expiration_time); let M5 = m5 * credentials_system.G_m5; diff --git a/rust/zkgroup/tests/integration_tests.rs b/rust/zkgroup/tests/integration_tests.rs index efbd6bedd..073de9837 100644 --- a/rust/zkgroup/tests/integration_tests.rs +++ b/rust/zkgroup/tests/integration_tests.rs @@ -3,10 +3,9 @@ // SPDX-License-Identifier: AGPL-3.0-only // -extern crate zkgroup; - use curve25519_dalek::ristretto::RistrettoPoint; use sha2::Sha256; +use zkgroup::SECONDS_PER_DAY; #[test] fn test_lizard() { @@ -82,6 +81,49 @@ pub const AUTH_CREDENTIAL_PRESENTATION_V2_RESULT: [u8; 0x08, 0x1f, 0x77, 0xc7, 0x2c, 0x8f, 0x52, 0x54, 0x74, 0x40, 0xe2, 0x01, 0x00, ]; +pub const AUTH_CREDENTIAL_PRESENTATION_V3_RESULT: &[u8] = &[ + 0x02, 0xec, 0x23, 0x74, 0x62, 0x4e, 0xe8, 0xde, 0x07, 0x39, 0x3f, 0x4c, 0x4f, 0x62, 0x5a, 0xfe, + 0x17, 0x93, 0xa3, 0xfe, 0x0c, 0xfc, 0xf1, 0x9a, 0x44, 0x7e, 0xe9, 0x36, 0x67, 0xe5, 0x2d, 0xc7, + 0x76, 0x38, 0x00, 0x38, 0x2c, 0x6e, 0xe4, 0x1e, 0x49, 0xbb, 0x60, 0xc4, 0x0c, 0xbd, 0x76, 0x65, + 0x7e, 0x1f, 0x6c, 0x73, 0x7f, 0x50, 0x2d, 0x6f, 0x47, 0xab, 0xe1, 0x6b, 0xd4, 0xef, 0xab, 0x1f, + 0x71, 0x94, 0x8d, 0x76, 0x34, 0x77, 0x1c, 0xd0, 0x45, 0x73, 0xa7, 0x5f, 0x3c, 0x8e, 0x77, 0xe7, + 0x0c, 0x55, 0xf5, 0x55, 0x07, 0x53, 0xad, 0x07, 0x7c, 0xfe, 0x5b, 0xb3, 0xed, 0xee, 0x0b, 0x0e, + 0x2a, 0xb8, 0x08, 0x72, 0x85, 0x65, 0x3d, 0xf8, 0x41, 0x5b, 0x9f, 0xea, 0x2f, 0x54, 0x10, 0xc4, + 0x09, 0x40, 0x59, 0xa2, 0x21, 0x7e, 0x28, 0x08, 0x65, 0xbf, 0xeb, 0xa6, 0x60, 0x53, 0x8d, 0xa2, + 0x07, 0xf8, 0x87, 0x9e, 0x4a, 0xfc, 0x64, 0xbb, 0x83, 0x90, 0xb8, 0xdd, 0xc4, 0x67, 0x61, 0x86, + 0x87, 0x0e, 0x5a, 0x34, 0x5a, 0x85, 0x32, 0x5a, 0xb1, 0xdf, 0xd9, 0x8b, 0xe0, 0x19, 0x53, 0x1a, + 0x16, 0x10, 0x0a, 0x6a, 0x49, 0x17, 0x95, 0x9f, 0x0f, 0x87, 0xef, 0x75, 0xef, 0x35, 0x35, 0x0e, + 0x6d, 0xa9, 0xd7, 0xd6, 0x38, 0xcf, 0x8c, 0xf9, 0xf1, 0x06, 0xc9, 0x47, 0x34, 0xa3, 0x2b, 0x85, + 0x33, 0x74, 0x41, 0xd2, 0x2a, 0x99, 0xcf, 0x08, 0xc2, 0x4d, 0x11, 0xf4, 0xe7, 0xbe, 0xdd, 0xbf, + 0x7f, 0xc9, 0x1a, 0x10, 0x14, 0x52, 0x15, 0xb9, 0x50, 0xa2, 0xb7, 0x8e, 0x7f, 0xbb, 0xc7, 0x70, + 0x7f, 0x8e, 0xeb, 0xb4, 0x75, 0x78, 0xae, 0x1c, 0x9d, 0xa0, 0xaf, 0x2f, 0x74, 0xeb, 0x26, 0x75, + 0xc8, 0xae, 0xc0, 0x4c, 0x6d, 0xf8, 0x0b, 0x56, 0x6e, 0xd3, 0xce, 0xd4, 0x5c, 0xd9, 0x52, 0x04, + 0x53, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0xaf, 0xf5, 0x41, 0xb1, 0xef, 0x79, + 0xa2, 0x82, 0xca, 0x4e, 0x55, 0x3e, 0xd1, 0xe2, 0xcc, 0x3e, 0x1c, 0x95, 0x51, 0x0c, 0x6d, 0x3b, + 0x90, 0x37, 0xa5, 0xa6, 0x72, 0x41, 0x1a, 0x35, 0x0e, 0x82, 0x5c, 0xb0, 0xfd, 0xbf, 0x4c, 0xc5, + 0x17, 0xdd, 0xaa, 0x48, 0x2b, 0x4b, 0x24, 0x67, 0xec, 0x91, 0x36, 0x25, 0x44, 0x15, 0x3b, 0x83, + 0x19, 0x1a, 0xba, 0x0b, 0x41, 0x3c, 0xa0, 0xb0, 0x00, 0x34, 0xa1, 0xa9, 0x49, 0xd1, 0x07, 0xb3, + 0xe7, 0x41, 0x56, 0xd5, 0xd9, 0xb1, 0x96, 0x48, 0x0e, 0xe3, 0x7f, 0x0d, 0xf1, 0xb4, 0x2a, 0x13, + 0x6f, 0xfe, 0xa8, 0x96, 0x81, 0x39, 0xa0, 0xaa, 0x01, 0x59, 0x5f, 0xc8, 0xe2, 0x02, 0x31, 0xf3, + 0x58, 0x78, 0x06, 0xc9, 0xc4, 0x71, 0x3c, 0x87, 0xac, 0xc8, 0xfe, 0xf6, 0x0d, 0x3f, 0x1a, 0x84, + 0x9d, 0x18, 0x4d, 0xd4, 0x95, 0x58, 0x8b, 0xf3, 0x03, 0xb1, 0xfc, 0xe7, 0x5e, 0x4c, 0x7a, 0x17, + 0x7c, 0x64, 0x4b, 0x62, 0xd2, 0xad, 0xbc, 0x04, 0x2c, 0x77, 0x98, 0xcf, 0xd6, 0xc4, 0x39, 0x21, + 0x54, 0x16, 0x49, 0xd2, 0x3a, 0x90, 0xe8, 0x7d, 0x0f, 0x14, 0x30, 0x30, 0x50, 0xfc, 0xc1, 0x5d, + 0x97, 0x5a, 0xef, 0x8d, 0x56, 0xbf, 0xab, 0xcf, 0x55, 0xb0, 0x5e, 0x93, 0x44, 0x91, 0x6b, 0xb6, + 0xae, 0xb4, 0x35, 0x18, 0x98, 0xdf, 0x15, 0x89, 0x03, 0xbe, 0x4f, 0xef, 0xc9, 0x33, 0xc6, 0xa2, + 0x03, 0x2e, 0x34, 0x3a, 0x59, 0xf1, 0xce, 0x1e, 0x1f, 0x4d, 0xe8, 0xf3, 0x81, 0xa5, 0x9f, 0x35, + 0xf9, 0x8c, 0x3c, 0x22, 0x49, 0x36, 0xfe, 0x54, 0x04, 0x76, 0x5d, 0xe9, 0xdf, 0x4c, 0xfa, 0x54, + 0x87, 0xf3, 0x60, 0xe2, 0x9e, 0x99, 0x34, 0x3e, 0x91, 0x81, 0x1b, 0xae, 0xc3, 0x31, 0xc4, 0x68, + 0x09, 0x85, 0xe6, 0x08, 0xca, 0x5d, 0x40, 0x8e, 0x21, 0x72, 0x5c, 0x6a, 0xa1, 0xb6, 0x1d, 0x5a, + 0x8b, 0x48, 0xd7, 0x5f, 0x4a, 0xaa, 0x9a, 0x3c, 0xbe, 0x88, 0xd3, 0xe0, 0xf1, 0xa5, 0x43, 0x19, + 0x08, 0x1f, 0x77, 0xc7, 0x2c, 0x8f, 0x52, 0x54, 0x74, 0xfe, 0x74, 0x40, 0x90, 0x60, 0x61, 0x56, + 0x79, 0xfc, 0x11, 0x54, 0x73, 0x68, 0x3d, 0x63, 0xab, 0xd9, 0xce, 0xd4, 0x6c, 0x7f, 0x2a, 0xd7, + 0x36, 0x04, 0x6d, 0xe5, 0xa2, 0xc7, 0xd2, 0x52, 0x2f, 0x12, 0x28, 0x95, 0x59, 0x70, 0x49, 0xcf, + 0xd7, 0xcc, 0x5b, 0xeb, 0x6d, 0xc7, 0x2a, 0xa9, 0x90, 0xae, 0x9a, 0x62, 0xec, 0x8e, 0x25, 0x6a, + 0x1c, 0xbf, 0x5f, 0x3f, 0x28, 0x42, 0x33, 0xbb, 0x07, 0x00, 0x60, 0xc7, 0x7b, 0x02, 0x00, 0x00, + 0x00, +]; + pub const PROFILE_KEY_CREDENTIAL_PRESENTATION_V1_RESULT: [u8; zkgroup::PROFILE_KEY_CREDENTIAL_PRESENTATION_V1_LEN] = [ 0x00, 0xc4, 0xd1, 0x9b, 0xca, 0x1a, 0xe8, 0x44, 0x58, 0x51, 0x68, 0x86, 0x9d, 0xa4, 0x13, 0x3e, @@ -409,6 +451,9 @@ fn test_integration_auth() { let presentation_v2_parsed = zkgroup::auth::AnyAuthCredentialPresentation::new(presentation_v2_bytes).unwrap(); + assert!(presentation_v1_parsed.get_pni_ciphertext().is_none()); + assert!(presentation_v2_parsed.get_pni_ciphertext().is_none()); + server_secret_params .verify_auth_credential_presentation(group_public_params, &presentation_v1_parsed) .unwrap(); @@ -463,6 +508,91 @@ fn test_integration_auth() { randomness_bytes.copy_from_slice(&bincode::serialize(&randomness).unwrap()); } +#[test] +fn test_integration_auth_with_pni() { + let server_secret_params = zkgroup::ServerSecretParams::generate(zkgroup::TEST_ARRAY_32); + let server_public_params = server_secret_params.get_public_params(); + + let master_key = zkgroup::groups::GroupMasterKey::new(zkgroup::TEST_ARRAY_32_1); + let group_secret_params = + zkgroup::groups::GroupSecretParams::derive_from_master_key(master_key); + let group_public_params = group_secret_params.get_public_params(); + + // Random UID and issueTime + let aci = zkgroup::TEST_ARRAY_16; + let pni = zkgroup::TEST_ARRAY_16_1; + let redemption_time = 123456 * SECONDS_PER_DAY; + + // SERVER + // Issue credential + let randomness = zkgroup::TEST_ARRAY_32_2; + let auth_credential_response = + server_secret_params.issue_auth_credential_with_pni(randomness, aci, pni, redemption_time); + + // CLIENT + let auth_credential = server_public_params + .receive_auth_credential_with_pni(aci, pni, redemption_time, &auth_credential_response) + .unwrap(); + + // Create and receive presentation + let randomness = zkgroup::TEST_ARRAY_32_5; + + let presentation = server_public_params.create_auth_credential_with_pni_presentation( + randomness, + group_secret_params, + auth_credential, + ); + + let presentation_bytes = &bincode::serialize(&presentation).unwrap(); + + let presentation_any: zkgroup::auth::AnyAuthCredentialPresentation = presentation.into(); + + let presentation_any_bytes = &bincode::serialize(&presentation_any).unwrap(); + + // for b in presentation_bytes.iter() { + // print!("0x{:02x}, ", b); + // } + + assert!(AUTH_CREDENTIAL_PRESENTATION_V3_RESULT[..] == presentation_bytes[..]); + assert!(AUTH_CREDENTIAL_PRESENTATION_V3_RESULT[..] == presentation_any_bytes[..]); + + let presentation_parsed = bincode::deserialize::< + zkgroup::auth::AuthCredentialWithPniPresentation, + >(presentation_bytes) + .unwrap(); + + assert!(presentation_any.get_pni_ciphertext().is_some()); + + server_secret_params + .verify_auth_credential_with_pni_presentation(group_public_params, &presentation_parsed) + .unwrap(); + + server_secret_params + .verify_auth_credential_presentation(group_public_params, &presentation_any) + .unwrap(); + + // test encoding + // these tests will also discover if the serialized sizes change, + // necessitating an update to the LEN constants + let mut group_secret_params_bytes = [0u8; zkgroup::common::constants::GROUP_SECRET_PARAMS_LEN]; + let mut server_secret_params_bytes = + [0u8; zkgroup::common::constants::SERVER_SECRET_PARAMS_LEN]; + let mut group_public_params_bytes = [0u8; zkgroup::common::constants::GROUP_PUBLIC_PARAMS_LEN]; + let mut server_public_params_bytes = + [0u8; zkgroup::common::constants::SERVER_PUBLIC_PARAMS_LEN]; + let mut auth_credential_response_bytes = + [0u8; zkgroup::common::constants::AUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN]; + let mut auth_credential_bytes = [0u8; zkgroup::common::constants::AUTH_CREDENTIAL_WITH_PNI_LEN]; + + group_secret_params_bytes.copy_from_slice(&bincode::serialize(&group_secret_params).unwrap()); + server_secret_params_bytes.copy_from_slice(&bincode::serialize(&server_secret_params).unwrap()); + group_public_params_bytes.copy_from_slice(&bincode::serialize(&group_public_params).unwrap()); + server_public_params_bytes.copy_from_slice(&bincode::serialize(&server_public_params).unwrap()); + auth_credential_response_bytes + .copy_from_slice(&bincode::serialize(&auth_credential_response).unwrap()); + auth_credential_bytes.copy_from_slice(&bincode::serialize(&auth_credential).unwrap()); +} + #[test] fn test_integration_profile() { // SERVER diff --git a/swift/Sources/LibSignalClient/Utils.swift b/swift/Sources/LibSignalClient/Utils.swift index b1411db66..8a1106a2f 100644 --- a/swift/Sources/LibSignalClient/Utils.swift +++ b/swift/Sources/LibSignalClient/Utils.swift @@ -56,6 +56,11 @@ internal func invokeFnReturningVariableLengthSerialized(fn: ( return try Result(contents: output) } +internal func invokeFnReturningOptionalVariableLengthSerialized(fn: (UnsafeMutablePointer?>?, UnsafeMutablePointer?) -> SignalFfiErrorRef?) throws -> Result? { + let output = try invokeFnReturningOptionalArray(fn: fn) + return try output.map { try Result(contents: $0) } +} + internal func invokeFnReturningUuid(fn: (UnsafeMutablePointer?) -> SignalFfiErrorRef?) throws -> UUID { var output: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) try checkError(fn(&output)) diff --git a/swift/Sources/LibSignalClient/zkgroup/AuthCredentialPresentation.swift b/swift/Sources/LibSignalClient/zkgroup/AuthCredentialPresentation.swift index 33e31ad72..f02925b87 100644 --- a/swift/Sources/LibSignalClient/zkgroup/AuthCredentialPresentation.swift +++ b/swift/Sources/LibSignalClient/zkgroup/AuthCredentialPresentation.swift @@ -20,12 +20,21 @@ public class AuthCredentialPresentation: ByteArray { } } - public func getRedemptionTime() throws -> UInt32 { + public func getPniCiphertext() throws -> UuidCiphertext? { return try withUnsafeBorrowedBuffer { buffer in - try invokeFnReturningInteger { - signal_auth_credential_presentation_get_redemption_time($0, buffer) + try invokeFnReturningOptionalVariableLengthSerialized { + signal_auth_credential_presentation_get_pni_ciphertext($0, $1, buffer) } } } + public func getRedemptionTime() throws -> Date { + let secondsSinceEpoch = try withUnsafeBorrowedBuffer { buffer in + try invokeFnReturningInteger { + signal_auth_credential_presentation_get_redemption_time($0, buffer) + } + } + return Date(timeIntervalSince1970: TimeInterval(secondsSinceEpoch)) + } + } diff --git a/swift/Sources/LibSignalClient/zkgroup/AuthCredentialWithPni.swift b/swift/Sources/LibSignalClient/zkgroup/AuthCredentialWithPni.swift new file mode 100644 index 000000000..3b122cdcd --- /dev/null +++ b/swift/Sources/LibSignalClient/zkgroup/AuthCredentialWithPni.swift @@ -0,0 +1,13 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation +import SignalFfi + +public class AuthCredentialWithPni: ByteArray { + public required init(contents: [UInt8]) throws { + try super.init(contents, checkValid: signal_auth_credential_with_pni_check_valid_contents) + } +} diff --git a/swift/Sources/LibSignalClient/zkgroup/AuthCredentialWithPniResponse.swift b/swift/Sources/LibSignalClient/zkgroup/AuthCredentialWithPniResponse.swift new file mode 100644 index 000000000..20deccabe --- /dev/null +++ b/swift/Sources/LibSignalClient/zkgroup/AuthCredentialWithPniResponse.swift @@ -0,0 +1,13 @@ +// +// Copyright 2022 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation +import SignalFfi + +public class AuthCredentialWithPniResponse: ByteArray { + public required init(contents: [UInt8]) throws { + try super.init(contents, checkValid: signal_auth_credential_with_pni_response_check_valid_contents) + } +} diff --git a/swift/Sources/LibSignalClient/zkgroup/ClientZkAuthOperations.swift b/swift/Sources/LibSignalClient/zkgroup/ClientZkAuthOperations.swift index b45b4054f..934082c88 100644 --- a/swift/Sources/LibSignalClient/zkgroup/ClientZkAuthOperations.swift +++ b/swift/Sources/LibSignalClient/zkgroup/ClientZkAuthOperations.swift @@ -26,6 +26,23 @@ public class ClientZkAuthOperations { } } + /// Produces the `AuthCredentialWithPni` from a server-generated `AuthCredentialWithPniResponse`. + /// + /// - parameter redemptionTime: This is provided by the server as an integer, and should be passed through directly. + public func receiveAuthCredentialWithPni(aci: UUID, pni: UUID, redemptionTime: UInt64, authCredentialResponse: AuthCredentialWithPniResponse) throws -> AuthCredentialWithPni { + return try serverPublicParams.withUnsafePointerToSerialized { serverPublicParams in + try withUnsafePointer(to: aci.uuid) { aci in + try withUnsafePointer(to: pni.uuid) { pni in + try authCredentialResponse.withUnsafePointerToSerialized { authCredentialResponse in + try invokeFnReturningSerialized { + signal_server_public_params_receive_auth_credential_with_pni($0, serverPublicParams, aci, pni, redemptionTime, authCredentialResponse) + } + } + } + } + } + } + public func createAuthCredentialPresentation(groupSecretParams: GroupSecretParams, authCredential: AuthCredential) throws -> AuthCredentialPresentation { return try createAuthCredentialPresentation(randomness: Randomness.generate(), groupSecretParams: groupSecretParams, authCredential: authCredential) } @@ -44,4 +61,22 @@ public class ClientZkAuthOperations { } } + public func createAuthCredentialPresentation(groupSecretParams: GroupSecretParams, authCredential: AuthCredentialWithPni) throws -> AuthCredentialPresentation { + return try createAuthCredentialPresentation(randomness: Randomness.generate(), groupSecretParams: groupSecretParams, authCredential: authCredential) + } + + public func createAuthCredentialPresentation(randomness: Randomness, groupSecretParams: GroupSecretParams, authCredential: AuthCredentialWithPni) throws -> AuthCredentialPresentation { + return try serverPublicParams.withUnsafePointerToSerialized { contents in + try randomness.withUnsafePointerToBytes { randomness in + try groupSecretParams.withUnsafePointerToSerialized { groupSecretParams in + try authCredential.withUnsafePointerToSerialized { authCredential in + try invokeFnReturningVariableLengthSerialized { + signal_server_public_params_create_auth_credential_with_pni_presentation_deterministic($0, $1, contents, randomness, groupSecretParams, authCredential) + } + } + } + } + } + } + } diff --git a/swift/Sources/LibSignalClient/zkgroup/ServerZkAuthOperations.swift b/swift/Sources/LibSignalClient/zkgroup/ServerZkAuthOperations.swift index 558187b7a..6b46c7c9c 100644 --- a/swift/Sources/LibSignalClient/zkgroup/ServerZkAuthOperations.swift +++ b/swift/Sources/LibSignalClient/zkgroup/ServerZkAuthOperations.swift @@ -30,6 +30,24 @@ public class ServerZkAuthOperations { } } + public func issueAuthCredentialWithPni(aci: UUID, pni: UUID, redemptionTime: UInt64) throws -> AuthCredentialWithPniResponse { + return try issueAuthCredentialWithPni(randomness: Randomness.generate(), aci: aci, pni: pni, redemptionTime: redemptionTime) + } + + public func issueAuthCredentialWithPni(randomness: Randomness, aci: UUID, pni: UUID, redemptionTime: UInt64) throws -> AuthCredentialWithPniResponse { + return try serverSecretParams.withUnsafePointerToSerialized { serverSecretParams in + try randomness.withUnsafePointerToBytes { randomness in + try withUnsafePointer(to: aci.uuid) { aci in + try withUnsafePointer(to: pni.uuid) { pni in + try invokeFnReturningSerialized { + signal_server_secret_params_issue_auth_credential_with_pni_deterministic($0, serverSecretParams, randomness, aci, pni, redemptionTime) + } + } + } + } + } + } + public func verifyAuthCredentialPresentation(groupPublicParams: GroupPublicParams, authCredentialPresentation: AuthCredentialPresentation) throws { try serverSecretParams.withUnsafePointerToSerialized { serverSecretParams in try groupPublicParams.withUnsafePointerToSerialized { groupPublicParams in diff --git a/swift/Sources/SignalFfi/signal_ffi.h b/swift/Sources/SignalFfi/signal_ffi.h index cd7c62f91..3f377c1e7 100644 --- a/swift/Sources/SignalFfi/signal_ffi.h +++ b/swift/Sources/SignalFfi/signal_ffi.h @@ -48,6 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only #define SignalAUTH_CREDENTIAL_RESPONSE_LEN 361 +#define SignalAUTH_CREDENTIAL_WITH_PNI_LEN 265 + +#define SignalAUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN 425 + #define SignalPNI_CREDENTIAL_LEN 161 #define SignalPNI_CREDENTIAL_PRESENTATION_V1_LEN 841 @@ -98,9 +102,9 @@ SPDX-License-Identifier: AGPL-3.0-only #define SignalRESERVED_LEN 1 -#define SignalSERVER_SECRET_PARAMS_LEN 1921 +#define SignalSERVER_SECRET_PARAMS_LEN 2305 -#define SignalSERVER_PUBLIC_PARAMS_LEN 353 +#define SignalSERVER_PUBLIC_PARAMS_LEN 417 #define SignalUUID_CIPHERTEXT_LEN 65 @@ -110,6 +114,11 @@ SPDX-License-Identifier: AGPL-3.0-only #define SignalUUID_LEN 16 +/** + * Seconds in a 24-hour cycle (ignoring leap seconds). + */ +#define SignalSECONDS_PER_DAY 86400 + typedef enum { SignalCiphertextMessageType_Whisper = 2, SignalCiphertextMessageType_PreKey = 3, @@ -1131,6 +1140,10 @@ SignalFfiError *signal_auth_credential_check_valid_contents(SignalBorrowedBuffer SignalFfiError *signal_auth_credential_response_check_valid_contents(SignalBorrowedBuffer buffer); +SignalFfiError *signal_auth_credential_with_pni_check_valid_contents(SignalBorrowedBuffer buffer); + +SignalFfiError *signal_auth_credential_with_pni_response_check_valid_contents(SignalBorrowedBuffer buffer); + SignalFfiError *signal_expiring_profile_key_credential_check_valid_contents(SignalBorrowedBuffer buffer); SignalFfiError *signal_expiring_profile_key_credential_response_check_valid_contents(SignalBorrowedBuffer buffer); @@ -1244,6 +1257,13 @@ SignalFfiError *signal_server_public_params_receive_auth_credential(unsigned cha uint32_t redemption_time, const unsigned char (*response)[SignalAUTH_CREDENTIAL_RESPONSE_LEN]); +SignalFfiError *signal_server_public_params_receive_auth_credential_with_pni(unsigned char (*out)[SignalAUTH_CREDENTIAL_WITH_PNI_LEN], + const unsigned char (*params)[SignalSERVER_PUBLIC_PARAMS_LEN], + const uint8_t (*aci)[16], + const uint8_t (*pni)[16], + uint64_t redemption_time, + const unsigned char (*response)[SignalAUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN]); + SignalFfiError *signal_server_public_params_create_auth_credential_presentation_deterministic(const unsigned char **out, size_t *out_len, const unsigned char (*server_public_params)[SignalSERVER_PUBLIC_PARAMS_LEN], @@ -1251,6 +1271,13 @@ SignalFfiError *signal_server_public_params_create_auth_credential_presentation_ const unsigned char (*group_secret_params)[SignalGROUP_SECRET_PARAMS_LEN], const unsigned char (*auth_credential)[SignalAUTH_CREDENTIAL_LEN]); +SignalFfiError *signal_server_public_params_create_auth_credential_with_pni_presentation_deterministic(const unsigned char **out, + size_t *out_len, + const unsigned char (*server_public_params)[SignalSERVER_PUBLIC_PARAMS_LEN], + const uint8_t (*randomness)[SignalRANDOMNESS_LEN], + const unsigned char (*group_secret_params)[SignalGROUP_SECRET_PARAMS_LEN], + const unsigned char (*auth_credential)[SignalAUTH_CREDENTIAL_WITH_PNI_LEN]); + SignalFfiError *signal_server_public_params_create_profile_key_credential_request_context_deterministic(unsigned char (*out)[SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN], const unsigned char (*server_public_params)[SignalSERVER_PUBLIC_PARAMS_LEN], const uint8_t (*randomness)[SignalRANDOMNESS_LEN], @@ -1322,6 +1349,13 @@ SignalFfiError *signal_server_secret_params_issue_auth_credential_deterministic( const uint8_t (*uuid)[16], uint32_t redemption_time); +SignalFfiError *signal_server_secret_params_issue_auth_credential_with_pni_deterministic(unsigned char (*out)[SignalAUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN], + const unsigned char (*server_secret_params)[SignalSERVER_SECRET_PARAMS_LEN], + const uint8_t (*randomness)[SignalRANDOMNESS_LEN], + const uint8_t (*aci)[16], + const uint8_t (*pni)[16], + uint64_t redemption_time); + SignalFfiError *signal_server_secret_params_verify_auth_credential_presentation(const unsigned char (*server_secret_params)[SignalSERVER_SECRET_PARAMS_LEN], const unsigned char (*group_public_params)[SignalGROUP_PUBLIC_PARAMS_LEN], SignalBorrowedBuffer presentation_bytes); @@ -1380,7 +1414,11 @@ SignalFfiError *signal_auth_credential_presentation_check_valid_contents(SignalB SignalFfiError *signal_auth_credential_presentation_get_uuid_ciphertext(unsigned char (*out)[SignalUUID_CIPHERTEXT_LEN], SignalBorrowedBuffer presentation_bytes); -SignalFfiError *signal_auth_credential_presentation_get_redemption_time(uint32_t *out, +SignalFfiError *signal_auth_credential_presentation_get_pni_ciphertext(const unsigned char **out, + size_t *out_len, + SignalBorrowedBuffer presentation_bytes); + +SignalFfiError *signal_auth_credential_presentation_get_redemption_time(uint64_t *out, SignalBorrowedBuffer presentation_bytes); SignalFfiError *signal_profile_key_credential_request_context_get_request(unsigned char (*out)[SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN], diff --git a/swift/Tests/LibSignalClientTests/ZKGroupTests.swift b/swift/Tests/LibSignalClientTests/ZKGroupTests.swift index 1dfc05f4d..3771b4c06 100644 --- a/swift/Tests/LibSignalClientTests/ZKGroupTests.swift +++ b/swift/Tests/LibSignalClientTests/ZKGroupTests.swift @@ -205,7 +205,7 @@ class ZKGroupTests: TestCaseBase { // CLIENT // Receive credential let clientZkAuthCipher = ClientZkAuthOperations(serverPublicParams: serverPublicParams) - let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: groupSecretParams ) + let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: groupSecretParams) let authCredential = try clientZkAuthCipher.receiveAuthCredential(uuid: uuid, redemptionTime: redemptionTime, authCredentialResponse: authCredentialResponse) // Create and decrypt user entry @@ -219,12 +219,64 @@ class ZKGroupTests: TestCaseBase { // Verify presentation let uuidCiphertextRecv = try presentation.getUuidCiphertext() XCTAssertEqual(uuidCiphertext.serialize(), uuidCiphertextRecv.serialize()) - XCTAssertEqual(try presentation.getRedemptionTime(), redemptionTime) + XCTAssertNil(try presentation.getPniCiphertext()) + XCTAssertEqual(try presentation.getRedemptionTime(), + Date(timeIntervalSince1970: TimeInterval(UInt64(redemptionTime) * 86400))) try serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams: groupPublicParams, authCredentialPresentation: presentation) XCTAssertEqual(presentation.serialize(), authPresentationResult) } + func testAuthWithPniIntegration() throws { + let aci: UUID = TEST_ARRAY_16 + let pni: UUID = TEST_ARRAY_16_1 + let redemptionTime: UInt64 = 123456 * 86400 + + // Generate keys (client's are per-group, server's are not) + // --- + + // SERVER + let serverSecretParams = try ServerSecretParams.generate(randomness: TEST_ARRAY_32) + let serverPublicParams = try serverSecretParams.getPublicParams() + let serverZkAuth = ServerZkAuthOperations(serverSecretParams: serverSecretParams) + + // CLIENT + let masterKey = try GroupMasterKey(contents: TEST_ARRAY_32_1) + let groupSecretParams = try GroupSecretParams.deriveFromMasterKey(groupMasterKey: masterKey) + + XCTAssertEqual((try groupSecretParams.getMasterKey()).serialize(), masterKey.serialize()) + + let groupPublicParams = try groupSecretParams.getPublicParams() + + // SERVER + // Issue credential + let authCredentialResponse = try serverZkAuth.issueAuthCredentialWithPni(randomness: TEST_ARRAY_32_2, aci: aci, pni: pni, redemptionTime: redemptionTime) + + // CLIENT + // Receive credential + let clientZkAuthCipher = ClientZkAuthOperations(serverPublicParams: serverPublicParams) + let clientZkGroupCipher = ClientZkGroupCipher(groupSecretParams: groupSecretParams) + let authCredential = try clientZkAuthCipher.receiveAuthCredentialWithPni(aci: aci, pni: pni, redemptionTime: redemptionTime, authCredentialResponse: authCredentialResponse) + + // Create and decrypt user entry + let aciCiphertext = try clientZkGroupCipher.encryptUuid(uuid: aci) + let aciPlaintext = try clientZkGroupCipher.decryptUuid(uuidCiphertext: aciCiphertext) + XCTAssertEqual(aci, aciPlaintext) + let pniCiphertext = try clientZkGroupCipher.encryptUuid(uuid: pni) + let pniPlaintext = try clientZkGroupCipher.decryptUuid(uuidCiphertext: pniCiphertext) + XCTAssertEqual(pni, pniPlaintext) + + // Create presentation + let presentation = try clientZkAuthCipher.createAuthCredentialPresentation(randomness: TEST_ARRAY_32_5, groupSecretParams: groupSecretParams, authCredential: authCredential) + + // Verify presentation + let uuidCiphertextRecv = try presentation.getUuidCiphertext() + XCTAssertEqual(aciCiphertext.serialize(), uuidCiphertextRecv.serialize()) + XCTAssertEqual(pniCiphertext.serialize(), try presentation.getPniCiphertext()?.serialize()) + XCTAssertEqual(try presentation.getRedemptionTime(), Date(timeIntervalSince1970: TimeInterval(redemptionTime))) + try serverZkAuth.verifyAuthCredentialPresentation(groupPublicParams: groupPublicParams, authCredentialPresentation: presentation) + } + func testProfileKeyIntegration() throws { let uuid: UUID = TEST_ARRAY_16