diff --git a/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java b/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java new file mode 100644 index 0000000000..3b110a4f4c --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2020 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 android.net.cts; + +import static android.net.IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305; +import static android.net.cts.PacketUtils.CHACHA20_POLY1305; +import static android.net.cts.PacketUtils.CHACHA20_POLY1305_BLK_SIZE; +import static android.net.cts.PacketUtils.CHACHA20_POLY1305_ICV_LEN; +import static android.net.cts.PacketUtils.CHACHA20_POLY1305_IV_LEN; +import static android.net.cts.PacketUtils.CHACHA20_POLY1305_KEY_LEN; +import static android.net.cts.PacketUtils.CHACHA20_POLY1305_SALT_LEN; +import static android.net.cts.PacketUtils.ESP_HDRLEN; +import static android.net.cts.PacketUtils.IP6_HDRLEN; +import static android.net.cts.PacketUtils.getIpHeader; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; + +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assume.assumeTrue; + +import android.net.IpSecAlgorithm; +import android.net.IpSecManager; +import android.net.IpSecTransform; +import android.net.Network; +import android.net.TestNetworkInterface; +import android.net.cts.PacketUtils.BytePayload; +import android.net.cts.PacketUtils.EspAeadCipher; +import android.net.cts.PacketUtils.EspAuth; +import android.net.cts.PacketUtils.EspAuthNull; +import android.net.cts.PacketUtils.EspCipher; +import android.net.cts.PacketUtils.EspHeader; +import android.net.cts.PacketUtils.IpHeader; +import android.net.cts.PacketUtils.UdpHeader; +import android.platform.test.annotations.AppModeFull; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +@AppModeFull(reason = "Socket cannot bind in instant app mode") +public class IpSecAlgorithmImplTest extends IpSecBaseTest { + private static final InetAddress LOCAL_ADDRESS = + InetAddress.parseNumericAddress("2001:db8:1::1"); + private static final InetAddress REMOTE_ADDRESS = + InetAddress.parseNumericAddress("2001:db8:1::2"); + + private static final int REMOTE_PORT = 12345; + private static final IpSecManager IPSEC_MANAGER = + InstrumentationRegistry.getContext().getSystemService(IpSecManager.class); + + private static class CheckCryptoImplTest implements TestNetworkRunnable.Test { + private final IpSecAlgorithm mIpsecEncryptAlgo; + private final IpSecAlgorithm mIpsecAuthAlgo; + private final IpSecAlgorithm mIpsecAeadAlgo; + private final EspCipher mEspCipher; + private final EspAuth mEspAuth; + + public CheckCryptoImplTest( + IpSecAlgorithm ipsecEncryptAlgo, + IpSecAlgorithm ipsecAuthAlgo, + IpSecAlgorithm ipsecAeadAlgo, + EspCipher espCipher, + EspAuth espAuth) { + mIpsecEncryptAlgo = ipsecEncryptAlgo; + mIpsecAuthAlgo = ipsecAuthAlgo; + mIpsecAeadAlgo = ipsecAeadAlgo; + mEspCipher = espCipher; + mEspAuth = espAuth; + } + + private static byte[] buildTransportModeEspPayload( + int srcPort, int dstPort, int spi, EspCipher espCipher, EspAuth espAuth) + throws Exception { + final UdpHeader udpPayload = + new UdpHeader(srcPort, dstPort, new BytePayload(TEST_DATA)); + final IpHeader preEspIpHeader = + getIpHeader( + udpPayload.getProtocolId(), LOCAL_ADDRESS, REMOTE_ADDRESS, udpPayload); + + final PacketUtils.EspHeader espPayload = + new EspHeader( + udpPayload.getProtocolId(), + spi, + 1 /* sequence number */, + udpPayload.getPacketBytes(preEspIpHeader), + espCipher, + espAuth); + return espPayload.getPacketBytes(preEspIpHeader); + } + + @Override + public void runTest(TestNetworkInterface testIface, TestNetworkCallback tunNetworkCallback) + throws Exception { + final TunUtils tunUtils = new TunUtils(testIface.getFileDescriptor()); + tunNetworkCallback.waitForAvailable(); + final Network testNetwork = tunNetworkCallback.currentNetwork; + + final IpSecTransform.Builder transformBuilder = + new IpSecTransform.Builder(InstrumentationRegistry.getContext()); + if (mIpsecAeadAlgo != null) { + transformBuilder.setAuthenticatedEncryption(mIpsecAeadAlgo); + } else { + if (mIpsecEncryptAlgo != null) { + transformBuilder.setEncryption(mIpsecEncryptAlgo); + } + if (mIpsecAuthAlgo != null) { + transformBuilder.setAuthentication(mIpsecAuthAlgo); + } + } + + try (final IpSecManager.SecurityParameterIndex outSpi = + IPSEC_MANAGER.allocateSecurityParameterIndex(REMOTE_ADDRESS); + final IpSecManager.SecurityParameterIndex inSpi = + IPSEC_MANAGER.allocateSecurityParameterIndex(LOCAL_ADDRESS); + final IpSecTransform outTransform = + transformBuilder.buildTransportModeTransform(LOCAL_ADDRESS, outSpi); + final IpSecTransform inTransform = + transformBuilder.buildTransportModeTransform(REMOTE_ADDRESS, inSpi); + // Bind localSocket to a random available port. + final DatagramSocket localSocket = new DatagramSocket(0)) { + IPSEC_MANAGER.applyTransportModeTransform( + localSocket, IpSecManager.DIRECTION_IN, inTransform); + IPSEC_MANAGER.applyTransportModeTransform( + localSocket, IpSecManager.DIRECTION_OUT, outTransform); + + // Send ESP packet + final DatagramPacket outPacket = + new DatagramPacket( + TEST_DATA, 0, TEST_DATA.length, REMOTE_ADDRESS, REMOTE_PORT); + testNetwork.bindSocket(localSocket); + localSocket.send(outPacket); + final byte[] outEspPacket = + tunUtils.awaitEspPacket(outSpi.getSpi(), false /* useEncap */); + + // Remove transform for good hygiene + IPSEC_MANAGER.removeTransportModeTransforms(localSocket); + + // Get the kernel-generated ESP payload + final byte[] outEspPayload = new byte[outEspPacket.length - IP6_HDRLEN]; + System.arraycopy(outEspPacket, IP6_HDRLEN, outEspPayload, 0, outEspPayload.length); + + // Get the IV of the kernel-generated ESP payload + final byte[] iv = + Arrays.copyOfRange( + outEspPayload, ESP_HDRLEN, ESP_HDRLEN + mEspCipher.ivLen); + + // Build ESP payload using the kernel-generated IV and the user space crypto + // implementations + mEspCipher.updateIv(iv); + final byte[] expectedEspPayload = + buildTransportModeEspPayload( + localSocket.getLocalPort(), + REMOTE_PORT, + outSpi.getSpi(), + mEspCipher, + mEspAuth); + + // Compare user-space-generated and kernel-generated ESP payload + assertArrayEquals(expectedEspPayload, outEspPayload); + } + } + + @Override + public void cleanupTest() { + // Do nothing + } + + @Override + public InetAddress[] getTestNetworkAddresses() { + return new InetAddress[] {LOCAL_ADDRESS}; + } + } + + @Test + public void testChaCha20Poly1305() throws Exception { + assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305)); + + final byte[] cryptKey = getKeyBytes(CHACHA20_POLY1305_KEY_LEN); + final IpSecAlgorithm ipsecAeadAlgo = + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305, + cryptKey, + CHACHA20_POLY1305_ICV_LEN * 8); + final EspAeadCipher espAead = + new EspAeadCipher( + CHACHA20_POLY1305, + CHACHA20_POLY1305_BLK_SIZE, + cryptKey, + CHACHA20_POLY1305_IV_LEN, + CHACHA20_POLY1305_ICV_LEN, + CHACHA20_POLY1305_SALT_LEN); + + runWithShellPermissionIdentity( + new TestNetworkRunnable( + new CheckCryptoImplTest( + null /* ipsecEncryptAlgo */, + null /* ipsecAuthAlgo */, + ipsecAeadAlgo, + espAead, + EspAuthNull.getInstance()))); + } +} diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java index 5da0d26fcc..27c9f3b00c 100644 --- a/tests/cts/net/src/android/net/cts/PacketUtils.java +++ b/tests/cts/net/src/android/net/cts/PacketUtils.java @@ -43,6 +43,7 @@ public class PacketUtils { static final int UDP_HDRLEN = 8; static final int TCP_HDRLEN = 20; static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12; + static final int ESP_HDRLEN = 8; static final int ESP_BLK_SIZE = 4; // ESP has to be 4-byte aligned static final int ESP_TRAILER_LEN = 2; @@ -61,8 +62,10 @@ public class PacketUtils { // AEAD parameters static final int AES_GCM_IV_LEN = 8; static final int AES_GCM_BLK_SIZE = 4; + static final int CHACHA20_POLY1305_KEY_LEN = 36; static final int CHACHA20_POLY1305_BLK_SIZE = ESP_BLK_SIZE; static final int CHACHA20_POLY1305_IV_LEN = 8; + static final int CHACHA20_POLY1305_SALT_LEN = 4; static final int CHACHA20_POLY1305_ICV_LEN = 16; // Authentication parameters @@ -77,6 +80,11 @@ public class PacketUtils { // Encryption algorithms static final String AES = "AES"; static final String AES_CBC = "AES/CBC/NoPadding"; + + // AEAD algorithms + static final String CHACHA20_POLY1305 = "ChaCha20/Poly1305/NoPadding"; + + // Authentication algorithms static final String HMAC_SHA_256 = "HmacSHA256"; public interface Payload { @@ -372,6 +380,11 @@ public class PacketUtils { if (cipher instanceof EspCipherNull && auth instanceof EspAuthNull) { throw new IllegalArgumentException("No algorithm is provided"); } + + if (cipher instanceof EspAeadCipher && !(auth instanceof EspAuthNull)) { + throw new IllegalArgumentException( + "AEAD is provided with an authentication" + " algorithm."); + } } private static EspCipher getDefaultCipher(byte[] key) { @@ -387,8 +400,10 @@ public class PacketUtils { } public short length() { + final int icvLen = + cipher instanceof EspAeadCipher ? ((EspAeadCipher) cipher).icvLen : auth.icvLen; return calculateEspPacketSize( - payload.length, cipher.ivLen, cipher.blockSize, auth.icvLen * 8); + payload.length, cipher.ivLen, cipher.blockSize, icvLen * 8); } public byte[] getPacketBytes(IpHeader header) throws Exception { @@ -426,12 +441,11 @@ public class PacketUtils { public static short calculateEspPacketSize( int payloadLen, int cryptIvLength, int cryptBlockSize, int authTruncLen) { - final int ESP_HDRLEN = 4 + 4; // SPI + Seq# final int ICV_LEN = authTruncLen / 8; // Auth trailer; based on truncation length - payloadLen += cryptIvLength; // Initialization Vector // Align to block size of encryption algorithm payloadLen = calculateEspEncryptedLength(payloadLen, cryptBlockSize); + payloadLen += cryptIvLength; // Initialization Vector return (short) (payloadLen + ESP_HDRLEN + ICV_LEN); } @@ -464,20 +478,28 @@ public class PacketUtils { } public abstract static class EspCipher { + protected static final int SALT_LEN_UNUSED = 0; + public final String algoName; public final int blockSize; public final byte[] key; public final int ivLen; + public final int saltLen; protected byte[] iv; - public EspCipher(String algoName, int blockSize, byte[] key, int ivLen) { + public EspCipher(String algoName, int blockSize, byte[] key, int ivLen, int saltLen) { this.algoName = algoName; this.blockSize = blockSize; this.key = key; this.ivLen = ivLen; + this.saltLen = saltLen; this.iv = getIv(ivLen); } + public void updateIv(byte[] iv) { + this.iv = iv; + } + public static byte[] getPaddedPayload(int nextHeader, byte[] payload, int blockSize) { final int paddedLen = calculateEspEncryptedLength(payload.length, blockSize); final ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen); @@ -514,7 +536,7 @@ public class PacketUtils { private static final EspCipherNull INSTANCE = new EspCipherNull(); private EspCipherNull() { - super(CRYPT_NULL, ESP_BLK_SIZE, KEY_UNUSED, IV_LEN_UNUSED); + super(CRYPT_NULL, ESP_BLK_SIZE, KEY_UNUSED, IV_LEN_UNUSED, SALT_LEN_UNUSED); } public static EspCipherNull getInstance() { @@ -530,7 +552,7 @@ public class PacketUtils { public static class EspCryptCipher extends EspCipher { public EspCryptCipher(String algoName, int blockSize, byte[] key, int ivLen) { - super(algoName, blockSize, key, ivLen); + super(algoName, blockSize, key, ivLen, SALT_LEN_UNUSED); } @Override @@ -554,7 +576,50 @@ public class PacketUtils { } } - // TODO: Implement EspAeadCipher in the following CL + public static class EspAeadCipher extends EspCipher { + public final int icvLen; + + public EspAeadCipher( + String algoName, int blockSize, byte[] key, int ivLen, int icvLen, int saltLen) { + super(algoName, blockSize, key, ivLen, saltLen); + this.icvLen = icvLen; + } + + @Override + public byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum) + throws GeneralSecurityException { + // Provided key consists of encryption/decryption key plus salt. Salt is used + // with ESP payload IV to build IvParameterSpec. + final byte[] secretKey = Arrays.copyOfRange(key, 0, key.length - saltLen); + final byte[] salt = Arrays.copyOfRange(key, secretKey.length, key.length); + + final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, algoName); + + final ByteBuffer ivParameterBuffer = ByteBuffer.allocate(saltLen + iv.length); + ivParameterBuffer.put(salt); + ivParameterBuffer.put(iv); + final IvParameterSpec ivParameterSpec = new IvParameterSpec(ivParameterBuffer.array()); + + final ByteBuffer aadBuffer = ByteBuffer.allocate(ESP_HDRLEN); + aadBuffer.putInt(spi); + aadBuffer.putInt(seqNum); + + // Encrypt payload + final Cipher cipher = Cipher.getInstance(algoName); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + cipher.updateAAD(aadBuffer.array()); + final byte[] encryptedTextAndIcv = + cipher.doFinal(getPaddedPayload(nextHeader, payload, blockSize)); + + // Build ciphertext + final ByteBuffer cipherText = + ByteBuffer.allocate(iv.length + encryptedTextAndIcv.length); + cipherText.put(iv); + cipherText.put(encryptedTextAndIcv); + + return getByteArrayFromBuffer(cipherText); + } + } public static class EspAuth { public final String algoName; diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java index 7887385234..d8e39b4646 100644 --- a/tests/cts/net/src/android/net/cts/TunUtils.java +++ b/tests/cts/net/src/android/net/cts/TunUtils.java @@ -147,6 +147,10 @@ public class TunUtils { return espPkt; // We've found the packet we're looking for. } + public byte[] awaitEspPacket(int spi, boolean useEncap) throws Exception { + return awaitPacket((pkt) -> isEsp(pkt, spi, useEncap)); + } + private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) { // Check SPI byte by byte. return pkt[espOffset] == (byte) ((spi >>> 24) & 0xff)