aboutsummaryrefslogtreecommitdiff
path: root/common/src/test/java/org/conscrypt/DuckTypedHpkeSpiTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'common/src/test/java/org/conscrypt/DuckTypedHpkeSpiTest.java')
-rw-r--r--common/src/test/java/org/conscrypt/DuckTypedHpkeSpiTest.java277
1 files changed, 277 insertions, 0 deletions
diff --git a/common/src/test/java/org/conscrypt/DuckTypedHpkeSpiTest.java b/common/src/test/java/org/conscrypt/DuckTypedHpkeSpiTest.java
new file mode 100644
index 00000000..6ab538bd
--- /dev/null
+++ b/common/src/test/java/org/conscrypt/DuckTypedHpkeSpiTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package org.conscrypt;
+
+import static org.conscrypt.HpkeFixture.DEFAULT_ENC;
+import static org.conscrypt.HpkeFixture.DEFAULT_EXPORTER_CONTEXT;
+import static org.conscrypt.HpkeFixture.DEFAULT_EXPORTER_LENGTH;
+import static org.conscrypt.HpkeFixture.DEFAULT_PT;
+import static org.conscrypt.HpkeFixture.createDefaultHpkeContextRecipient;
+import static org.conscrypt.HpkeFixture.createDefaultHpkeContextSender;
+import static org.conscrypt.HpkeTestVectorsTest.getHpkeEncryptionRecords;
+import static org.conscrypt.TestUtils.encodeHex;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.Security;
+import java.util.List;
+
+import org.conscrypt.HpkeTestVectorsTest.HpkeData;
+import org.conscrypt.HpkeTestVectorsTest.HpkeEncryptionData;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for DuckTypedHpkeSpiTest. Essentially the same as the tests for HpkeContext but
+ * with a "foreign" HPKE Provider inserted ahead of Conscrypt. That is, one which returns
+ * SPI instances with all the correct methods but which don't inherit directly from "our"
+ * HpkeSpi.
+ */
+@RunWith(JUnit4.class)
+public class DuckTypedHpkeSpiTest {
+ private static final Provider conscryptProvider = TestUtils.getConscryptProvider();
+
+ @Before
+ public void before() {
+ Security.insertProviderAt(new ForeignHpkeProvider(), 1);
+ }
+
+ @After
+ public void after() {
+ Security.removeProvider(ForeignHpkeProvider.NAME);
+ }
+
+ // Copied from HpkeContextTest but with extra checks to ensure we are operating on
+ // duck typed instances.
+ @Test
+ public void sealOpen() throws Exception {
+ final HpkeContextSender ctxSender1 = createDefaultHpkeContextSender();
+ assertForeign(ctxSender1);
+ final byte[] enc1 = ctxSender1.getEncapsulated();
+ final byte[] ciphertext1 = ctxSender1.seal(DEFAULT_PT, /* aad= */ null);
+
+ final HpkeContextSender ctxSender2 = createDefaultHpkeContextSender();
+ assertForeign(ctxSender2);
+ final byte[] enc2 = ctxSender2.getEncapsulated();
+ final byte[] ciphertext2 = ctxSender2.seal(DEFAULT_PT, /* aad= */ null);
+
+ assertNotNull(enc1);
+ assertNotNull(ciphertext1);
+ assertNotNull(enc2);
+ assertNotNull(ciphertext2);
+ assertNotEquals(encodeHex(enc1), encodeHex(enc2));
+ assertNotEquals(encodeHex(DEFAULT_PT), encodeHex(ciphertext1));
+ assertNotEquals(encodeHex(ciphertext1), encodeHex(ciphertext2));
+
+ final HpkeContextRecipient ctxRecipient1 = createDefaultHpkeContextRecipient(enc1);
+ assertForeign(ctxRecipient1);
+ byte[] plaintext1 = ctxRecipient1.open(ciphertext1, /* aad= */ null);
+
+ final HpkeContextRecipient ctxRecipient2 = createDefaultHpkeContextRecipient(enc2);
+ assertForeign(ctxRecipient2);
+ byte[] plaintext2 = ctxRecipient2.open(ciphertext2, /* aad= */ null);
+
+ assertNotNull(plaintext1);
+ assertNotNull(plaintext2);
+ assertArrayEquals(DEFAULT_PT, plaintext1);
+ assertArrayEquals(DEFAULT_PT, plaintext2);
+ }
+
+ // Copied from HpkeContextTest but with extra checks to ensure we are operating on
+ // duck typed instances.
+ @Test
+ public void export() throws Exception {
+ final HpkeContextSender ctxSender = createDefaultHpkeContextSender();
+ assertForeign(ctxSender);
+ final byte[] enc = ctxSender.getEncapsulated();
+ final byte[] export1 = ctxSender.export(DEFAULT_EXPORTER_LENGTH, DEFAULT_EXPORTER_CONTEXT);
+
+ final HpkeContextRecipient ctxRecipient = createDefaultHpkeContextRecipient(DEFAULT_ENC);
+ assertForeign(ctxRecipient);
+ final byte[] export2 =
+ ctxRecipient.export(DEFAULT_EXPORTER_LENGTH, DEFAULT_EXPORTER_CONTEXT);
+
+ assertNotNull(enc);
+ assertNotNull(export1);
+ assertEquals(DEFAULT_EXPORTER_LENGTH, export1.length);
+ assertNotNull(export2);
+ assertEquals(DEFAULT_EXPORTER_LENGTH, export2.length);
+ assertNotEquals(encodeHex(DEFAULT_ENC), encodeHex(enc));
+ assertNotEquals(encodeHex(export1), encodeHex(export2));
+ }
+
+ @Test
+ public void vectors() throws Exception {
+ final List<HpkeData> records = getHpkeEncryptionRecords();
+ for (HpkeData record : records) {
+ testHpkeEncryption(record);
+ }
+ }
+
+ // Copied from HpkeTestVectorsTest but with extra checks to ensure we are operating on
+ // duck typed instances.
+ private void testHpkeEncryption(HpkeData record) throws Exception {
+ final byte[] enc = record.pkEm;
+
+ // Encryption
+ final HpkeContextSender contextSender =
+ setupBaseForTesting(record.hpkeSuite, record.pkRm, record.info, record.skEm);
+ assertForeign(contextSender);
+ final byte[] encResult = contextSender.getEncapsulated();
+ assertArrayEquals("Failed encryption 'enc' " + encodeHex(enc), enc, encResult);
+ for (HpkeEncryptionData encryption : record.encryptions) {
+ final byte[] ciphertext = contextSender.seal(encryption.pt, encryption.aad);
+ assertArrayEquals("Failed encryption 'ciphertext' on data : " + encryption,
+ encryption.ct, ciphertext);
+ }
+
+ // Decryption
+ final HpkeContextRecipient contextRecipient =
+ HpkeContextRecipient.getInstance(record.hpkeSuite.name());
+ assertForeign(contextRecipient);
+ contextRecipient.init(enc, record.skRm, record.info);
+ for (HpkeEncryptionData encryption : record.encryptions) {
+ final byte[] plaintext = contextRecipient.open(encryption.ct, encryption.aad);
+ assertArrayEquals(
+ "Failed decryption on data : " + encryption, encryption.pt, plaintext);
+ }
+ }
+
+ private HpkeContextSender setupBaseForTesting(
+ HpkeSuite suite, PublicKey publicKey, byte[] info, byte[] sKem) throws Exception {
+ String algorithm = suite.name();
+ HpkeContextSender sender = HpkeContextSender.getInstance(algorithm);
+ sender.initForTesting(publicKey, info, sKem);
+ return sender;
+ }
+
+ // Asserts that an HpkeContext is duck-typed and configured as we expect it.
+ private static void assertForeign(HpkeContext context) {
+ // Context's SPI should be duck typed, because the foreign Provider returns instances
+ // which use HpkeForeignSpi which *doesn't* implement HpkeSpi.
+ assertTrue(context.getSpi() instanceof DuckTypedHpkeSpi);
+ DuckTypedHpkeSpi duckTyped = (DuckTypedHpkeSpi) context.getSpi();
+
+ // Verify the SPI is indeed foreign.
+ assertTrue(duckTyped.getDelegate() instanceof HpkeForeignSpi);
+
+ // And that it is delegating to a real HpkeImpl, so we can test it.
+ HpkeForeignSpi foreign = (HpkeForeignSpi) duckTyped.getDelegate();
+ assertTrue(foreign.realSpi instanceof HpkeImpl);
+ }
+
+ // Provides HpkeContext instances that use a "foreign" SPI, that is one that isn't
+ // know to inherit HpkeSpi but implements the same methods and so can be used via
+ // duck typing.
+ //
+ // Some complexity here: We want to test end-to-end, so the foreign SPI delegates to a
+ // real Conscrypt SPI for its implementation so there are two levels of delegation. That is:
+ // * ForeignHpkeProvider provides instances of HpkeForeignSpi. This *doesn't* inherit from
+ // HpkeSpi and so is representative of the case where the SPI comes from a different
+ // Conscrypt variant or indeed some other provider. HpkeContext created a
+ // DuckTypedHpkeSpi to wrap this SPI and that is what we're testing above.
+ // * HpkeForeignSpi finds its equivalent SPI from the real Conscrypt Provider and
+ // delegates all operations to it (by direct method calls not duck typing). This is just
+ // a test setup quirk so we can test end-to-end.
+ private static class ForeignHpkeProvider extends Provider {
+ private static final String NAME = "Foreign_Hpke";
+
+ protected ForeignHpkeProvider() {
+ super( NAME, 1.0, "HPKE unit test usage only");
+ put("ConscryptHpke.DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/AES_128_GCM",
+ HpkeForeignSpi.X25519_AES_128.class.getName());
+ put("ConscryptHpke.DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/AES_256_GCM",
+ HpkeForeignSpi.X25519_AES_256.class.getName());
+ put("ConscryptHpke.DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/CHACHA20POLY1305",
+ HpkeForeignSpi.X25519_CHACHA20.class.getName());
+ }
+ }
+ public static class HpkeForeignSpi {
+ private final HpkeSpi realSpi;
+
+ public HpkeForeignSpi(String hpkeSuite) throws NoSuchAlgorithmException {
+
+ Provider.Service service =
+ conscryptProvider.getService("ConscryptHpke", hpkeSuite);
+ assertNotNull(service);
+ realSpi = (HpkeSpi) service.newInstance(null);
+ assertNotNull(realSpi);
+ }
+
+ public void engineInitSender(PublicKey recipientKey, byte[] info, PrivateKey senderKey,
+ byte[] psk, byte[] psk_id) throws InvalidKeyException {
+ realSpi.engineInitSender(recipientKey, info, senderKey, psk, psk_id);
+ }
+
+ public void engineInitSenderForTesting(PublicKey recipientKey, byte[] info,
+ PrivateKey senderKey, byte[] psk, byte[] psk_id, byte[] sKe)
+ throws InvalidKeyException {
+ realSpi.engineInitSenderForTesting(recipientKey, info, senderKey, psk, psk_id, sKe);
+ }
+
+ public void engineInitRecipient(byte[] enc, PrivateKey recipientKey, byte[] info,
+ PublicKey senderKey, byte[] psk, byte[] psk_id) throws InvalidKeyException {
+ realSpi.engineInitRecipient(enc, recipientKey, info, senderKey, psk, psk_id);
+ }
+
+ public byte[] engineSeal(byte[] plaintext, byte[] aad) {
+ return realSpi.engineSeal(plaintext, aad);
+ }
+
+ public byte[] engineExport(int length, byte[] exporterContext) {
+ return realSpi.engineExport(length, exporterContext);
+ }
+
+ public byte[] engineOpen(byte[] ciphertext, byte[] aad) throws GeneralSecurityException {
+ return realSpi.engineOpen(ciphertext, aad);
+ }
+
+ public byte[] getEncapsulated() {
+ return realSpi.getEncapsulated();
+ }
+
+ public static class X25519_AES_128 extends HpkeForeignSpi {
+ public X25519_AES_128() throws NoSuchAlgorithmException {
+ super("DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/AES_128_GCM");
+ }
+ }
+
+ public static class X25519_AES_256 extends HpkeForeignSpi {
+ public X25519_AES_256() throws NoSuchAlgorithmException {
+ super("DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/AES_256_GCM");
+ }
+ }
+
+ public static class X25519_CHACHA20 extends HpkeForeignSpi {
+ public X25519_CHACHA20() throws NoSuchAlgorithmException {
+ super("DHKEM_X25519_HKDF_SHA256/HKDF_SHA256/CHACHA20POLY1305");
+ }
+ }
+ }
+}